34

After starting a bash terminal, I noticed that the PATH variable contains duplicate entries. My terminal starts a login shell, so ~/.bash_profile is sourced, followed by ~/.profile and ~/.bashrc. Only in ~/.profile do I create the paths entries which are duplicated.

To be pedantic, this is the order in which the files that SHOULD be sourced are being sourced:

Sourced /etc/profile
Sourced /etc/bash.bashrc
Sourced .bash_profile
Sourced .profile
Sourced .bashrc

Before anyone marks this as a duplicate of "PATH variable contains duplicates", keep reading.

At first I thought this had to do with ~/.profile being sourced twice, so I had the file write to a log file whenever it was sourced, and surprisingly it only logged one entry, which tells me that it was only sourced once. Even more surprising is the fact that when I comment out the entries which were in ~/.profile, the entries still appear in the PATH variable. This has led me to three conclusions, one of which was quickly ruled out:

  1. Bash ignores valid bash comments and still executes the commented code
  2. There is a script which reads the ~/.profile and ignores any code that prints an output (the log file for example)
  3. There is another copy of my ~/.profile which is being sourced elsewhere

The first one, I quickly concluded not to be the case due to some quick testing. The second and third options are where I need help with.

How do I gather a list of scripts which are executed when my terminal starts up? I used echo in the files that I checked to know if they are sourced by bash, but I need to find a conclusive method which traces the execution up the point when the terminal is ready for me to start typing into it.

If the above is not possible, then can anyone suggest where else I can look to see which scripts are being run.


Future reference

This is the script I now use for adding to my path:

function add_to_path() {
    for path in ${2//:/ }; do
        if ! [[ "${!1}" =~ "${path%/}" ]]; then # ignore last /
            new_path="$path:${!1#:}"
            export "$1"="${new_path%:}" # remove trailing :
        fi
    done
}

I use it like this:

add_to_path 'PATH' "/some/path/bin"

The script checks if the path already exists in the variable before prepending it.

For zsh users, you can use this equivalent:

# prepends the given path(s) to the supplied PATH variable
# ex. add_to_path 'PATH' "$(go env GOPATH)/bin"
function add_to_path() {
    # (P)1 path is expanded from $1
    # ##: Removes leading :
    local -x pth="${(P)1##:}"
    # (s.:.) splits the given variable at :
    for p in ${(s.:.)2}; do
        # %%/ Remove trailing /
        # :P Behaves similar to realpath(3)
        local p="${${p%%/}:P}"
        if [[ ! "$pth" =~ "$p" ]]; then
            pth="$p:$pth"
        fi
    done
    export "$1"="${pth%%:}"
}

Edit 28/8/2018

One more thing I found I could do with this script is to also fix the path. So at the start of my .bashrc file, I do something like this:

_temp_path="$PATH"
PATH='/usr/local/sbin:/usr/local/bin:/usr/bin:/usr/sbin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin'
add_to_path 'PATH' "$_temp_path"
unset _temp_path

It is up to you what the PATH should start with. Examine PATH first to decide.

3
  • Bash only reads from ~/.profile if ~/.bash_profile does not exist...
    – jasonwryan
    Jan 2, 2017 at 20:15
  • @jasonwryan, I source ~/.profile and ~/.bashrc from ~/.bash_profile
    – smac89
    Jan 2, 2017 at 20:15
  • 2
    Reading your question and the accepted answer, it seems you don't mean "at startup" but rather "at login" Apr 11, 2022 at 21:52

3 Answers 3

60

If your system has strace then you can list the files opened by the shell, for example using

echo exit | strace bash -li |& grep '^open'

(-li means login shell interactive; use only -i for an interactive non-login shell.)

This will show a list of files which the shell opened or tried to open. On my system, they are as follows:

  1. /etc/profile
  2. /etc/profile.d/* (various scripts in /etc/profile.d/)
  3. /home/<username>/.bash_profile (this fails, I have no such file)
  4. /home/<username>/.bash_login (this fails, I have no such file)
  5. /home/<username>/.profile
  6. /home/<username>/.bashrc
  7. /home/<username>/.bash_history (history of command lines; this is not a script)
  8. /usr/share/bash-completion/bash_completion
  9. /etc/bash_completion.d/* (various scripts providing autocompletion functionality)
  10. /etc/inputrc (defines key bindings; this is not a script)

Use man strace for more information.

12
  • 6
    @smac89: That's normal for a login shell. Bash behaves as a login shell when the 1st character of $0 is a dash -, or when invoked with the option -l.
    – AlexP
    Jan 2, 2017 at 22:16
  • 1
    OK that's a bit of a relief. I have ran the command you gave and the output looks really complicated, but nonetheless all the files shown do not contain the duplicated entries. This is leading me to think that the duplicated entries happen when I first log into my account, i.e. something is initially sourcing the entries in that file and it is done again when I open the terminal? Actually I think that might be it. When I log in to my account, the entries are sourced, and again when I open the terminal the process is repeated. Does that sound possible?
    – smac89
    Jan 2, 2017 at 22:24
  • 2
    Why don't you debug the old fashioned way, by putting echo PATH=\""$PATH"\" at the beginning and end of .profile and .bashrc? And why don't you do what everybody does and set the PATH either fully, or, if adding a directory, guarded: echo ":$PATH:" | grep -q ":/path/to/dir:" || export PATH="$PATH:/path/to/dir"?
    – AlexP
    Jan 2, 2017 at 22:26
  • 6
    Use sudo bash -c "echo exit|dtruss bash -li|& less|grep '^open'" on macOS. (just replace strace with dtruss)
    – Max Coplan
    Nov 19, 2018 at 23:37
  • 3
    Since dtruss seems no longer able to trace certain things on macOS (dtrace: failed to execute /bin/bash: dtrace cannot control executables signed with restricted entitlements), I had to resort to the following: /bin/bash -x -l -i -c 'exit' 2>&1 | less
    – Wodin
    Mar 21, 2019 at 9:53
10

Reviving this question because strace is an overkill here.

Execute bash and carve it out of the output. -li is login interactively, -x prints out what bash is doing internally, and -c exit tells bash to terminate immediately. Using sed to filter out the source command or the . alias.

/bin/bash -lixc exit 2>&1 | sed -n 's/^+* \(source\|\.\) //p'
5
  • 1
    I can't get this to work. Try this instead /bin/bash -lixc exit 2>&1 | awk 'match($0, /^+* (\.|source) (.+)/, s) {print s[2]}'. I also find that strace does a better job: strace -t -e trace='openat' --decode-fds -e signal=none bash -lic exit 2>&1 | grep -P --no-color 'openat.+(?<!\(No such file or directory\))$'
    – smac89
    Apr 13, 2021 at 19:30
  • 1
    @smac89 if you want your strace to work on WSL as well, use the following strace -e trace='openat' -e signal=none bash -lic exit 2>&1 | cut -d\" -f2 | grep -Ev "locale/|langpack|/dev/null|/dev/tty|^*/$|+++ exited|logout|.*so.*" to get the proper list of config files only, without the fancy stuff.
    – ikaerom
    Jul 31, 2021 at 18:30
  • This is only half of a good idea.  Yes, -x will show all the commands that the shell executes, but the OP is interested in the initialization files that bash runs automatically, and -x does not show these as source or . commands.  A better answer would be to tell the OP to look at the full -x output, figure out what commands are being executed, and then grep everything to find out what files those commands are in. … (Cont’d) Apr 12, 2022 at 5:33
  • (Cont’d) … A slightly better approach would be to use -v and see the literal contents of the initialization files — that will show comments, and many system shell script files have header comments giving their names. … … … … … … … … … … … … … … … … … … … … P.S.  You talk about “the source command or the . alias.”  I believe that there is a command that simply has two names.  Can you provide a reference for your claim that . is only an alias for source? Apr 12, 2022 at 5:33
  • This was the only solution that worked for me (Using bash in Alpine linux).
    – Rodriguez
    Jun 9, 2022 at 19:02
2

Please consider this explanation about why ~/.profile is loaded twice.

On most Linux systems where a GUI is running the ~/.profile file is already processed at the moment you login via the GUI. When you subsequently open a terminal window, the shell should normally not be started as a login shell unless you have a very specific use-case. By default most GUIs on Linux distros have this behaviour correctly implemented.

For reference you can read the second paragraph in answer https://unix.stackexchange.com/a/119675/521859 where this is explained.

If you do start a terminal from the GUI and specify that the shell needs to use the login option, then that is very likely loading ~/.profile a second time.

You can verify if this is the case in a comparison by logging in via TTY or SSH and then check if your problem of duplicate configuration is solved. Or you can verify/disable the shell login option in your terminal program in the GUI.

Regards, Jeffrey

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .