Linux & Bash · Part I · Module 10 · The Environment, PATH and Shell Startup
Startup Files: Which One Actually Runs
You add a line to a file in your home directory, open a new terminal, and it works. You run the same thing over SSH, or from a script, or on a build machine, and it does not. The reason is that Bash reads a different file depending on how it was started, and there are three cases that do not overlap.
Intermediate18 min readLinuxPart IFundamentalsEnvironmentBash
Part I · Module 10 · Chapter 10.3 · Startup Files: Which One Actually Runs
Chapter 10.1 ended with "exported in the right place" as one of four reasons a setting fails to appear. This chapter is that reason, in full.
1. Three Kinds of Shell
Before the files, the categories — because the files follow from them.
| Kind | What it is | You get one from |
|---|---|---|
| Login | The first shell of a session; it is where you sign in | SSH, a text console, bash -l, macOS Terminal tabs |
| Interactive | A shell with a prompt, reading what you type | Opening a terminal, typing bash |
| Non-interactive | A shell running a script, with no prompt | ./script.sh, bash script.sh, cron, a build system |
They are not mutually exclusive: an SSH session gives you a shell that is both login and interactive. The useful question is not "which one is this" but "which of these properties does it have", because each property brings its own files.
Ask the shell directly:
# is it interactive? $- contains i
case "$-" in *i*) echo interactive ;; *) echo not interactive ;; esac
# is it a login shell?
shopt -q login_shell && echo "login" || echo "not login"$- holds the current single-letter options, and an interactive shell has i among them — verified: a non-interactive Bash reports hBc and an interactive one hiBHc.
2. What Each Kind Reads — Measured
These three results were produced by building a home directory containing all five candidate files, each printing its own name, and starting Bash three ways against it.
A login shell:
$ env -i HOME=$FAKE PATH=/usr/bin:/bin bash -l -c 'echo "(login shell body)"'
>> read .bash_profile
(login shell body)An interactive shell:
$ env -i HOME=$FAKE PATH=/usr/bin:/bin bash -i -c 'echo "(interactive body)"'
>> read .bashrc
(interactive body)A script:
$ env -i HOME=$FAKE PATH=/usr/bin:/bin bash /tmp/s.sh
(script body)Nothing. No startup file at all.
3. Only One Login File Runs
The three login candidates are alternatives, not a sequence. Measured by removing them one at a time:
$ ls $FAKE
.bash_profile .bash_login .profile .bashrc
$ bash -l -c ':'
>> read .bash_profile
$ mv $FAKE/.bash_profile $FAKE/.bash_profile.off
$ bash -l -c ':'
>> read .bash_login
$ mv $FAKE/.bash_login $FAKE/.bash_login.off
$ bash -l -c ':'
>> read .profileThe first one that exists is read, and the other two are ignored entirely.
This is a frequent and baffling failure. Somebody adds a line to ~/.profile following an instruction written for a different shell, it has no effect, and the reason is that ~/.bash_profile exists — so ~/.profile is never opened. Nothing reports it; the file simply sits there.
Check which of yours exists before editing anything:
ls -la ~/.bash_profile ~/.bash_login ~/.profile ~/.bashrc 2>&14. The Line Everybody Ends Up Writing
Given the table, the obvious problem: you want your aliases and prompt in .bashrc (interactive shells) and your PATH in .bash_profile (login shells), and over SSH you get a login shell, so your aliases are missing.
The standard fix is one line in .bash_profile:
# ~/.bash_profile
[ -f ~/.bashrc ] && . ~/.bashrcMeasured working:
$ bash -l -c 'echo "(body)"'
>> read .bash_profile
>> read .bashrc
(body)Now a login shell reads both, and the division of labour becomes workable.
5. Scripts Read Nothing
The third row of the table is the one that surprises people most.
$ env -i HOME=$FAKE bash /tmp/s.sh
(script body)A script reads no startup file. Not .bashrc, not .bash_profile. It inherits the environment from whatever started it, and that is all it gets.
This is correct and deliberate — a script should behave the same way whoever runs it, rather than depending on the personal configuration of the person who happened to launch it. It is also why:
- a script cannot use your aliases (Chapter 10.5);
- a
cronjob fails to find a tool that works perfectly in your terminal, becausecronstarts with a minimal environment and no startup file runs to fix it; - a build system needs its environment set explicitly rather than assumed.
There is one hook:
$ env -i HOME=$FAKE BASH_ENV=$FAKE/.bashenv bash /tmp/s.sh
>> read BASH_ENV file
(script body)BASH_ENV names a file that a non-interactive Bash will source before running the script. It exists, it works, and it is almost always the wrong tool — it makes scripts depend on an invisible setting, which is precisely what row three is designed to prevent. Know it so you can recognise it in somebody else's environment.
6. Logging Out, and Other Systems
~/.bash_logout runs when a login shell exits — occasionally used to clear a screen or remove scratch files. It is the only file in this chapter that runs at the end rather than the beginning, and most people never need it.
7. Finding Out What Actually Happened
When a setting is not taking effect, stop guessing and instrument it. Add a line to the top of each candidate file:
echo " [read $BASH_SOURCE]" >&2$BASH_SOURCE holds the name of the file currently being read, and >&2 sends it to error output so it survives redirection. Then open a terminal, SSH in, and run a script, and read which lines appear in each case. That is exactly the experiment this chapter is built on, and it takes two minutes.
Remove the lines afterwards.
The other quick checks:
# what kind of shell am I in?
case "$-" in *i*) echo interactive ;; *) echo not ;; esac
shopt -q login_shell && echo login || echo "not login"
# which files exist?
ls -la ~/.bash_profile ~/.bash_login ~/.profile ~/.bashrc 2>&1
# what does a login shell end up with?
bash -lc 'echo "$PATH"' | tr ':' '\n'
# ...versus a non-interactive one?
bash -c 'echo "$PATH"' | tr ':' '\n'That last pair is the fastest way to see the problem: if the two PATHs differ, the difference is coming from a startup file, and now you know which category to look in.
Practice
Do the measurement yourself — it is the only way this stops being folklore.
1 — Build a fake home.
FAKE=/tmp/fakehome; rm -rf "$FAKE"; mkdir -p "$FAKE"
for f in .bash_profile .bash_login .profile .bashrc; do
printf 'echo " >> read %s"\n' "$f" > "$FAKE/$f"
done2 — Run all three kinds.
printf 'echo "(script body)"\n' > /tmp/s.sh
echo "--- login:"; env -i HOME=$FAKE PATH=/usr/bin:/bin bash -l -c 'echo "(login body)"'
echo "--- interactive:"; env -i HOME=$FAKE PATH=/usr/bin:/bin bash -i -c 'echo "(interactive body)"' 2>/dev/null
echo "--- script:"; env -i HOME=$FAKE PATH=/usr/bin:/bin bash /tmp/s.sh3 — Prove only one login file runs.
mv $FAKE/.bash_profile $FAKE/off1; env -i HOME=$FAKE PATH=/usr/bin:/bin bash -l -c ':'
mv $FAKE/.bash_login $FAKE/off2; env -i HOME=$FAKE PATH=/usr/bin:/bin bash -l -c ':'4 — Apply the standard fix.
mv $FAKE/off1 $FAKE/.bash_profile
printf 'echo " >> read .bash_profile"\n[ -f ~/.bashrc ] && . ~/.bashrc\n' > $FAKE/.bash_profile
env -i HOME=$FAKE PATH=/usr/bin:/bin bash -l -c 'echo "(body)"'5 — Find the one hook a script has.
printf 'echo " >> read BASH_ENV file"\n' > $FAKE/.bashenv
env -i HOME=$FAKE BASH_ENV=$FAKE/.bashenv PATH=/usr/bin:/bin bash /tmp/s.sh6 — Identify your own shell.
ps -p $$ -o comm=
case "$-" in *i*) echo interactive ;; *) echo not ;; esac
shopt -q login_shell && echo login || echo "not login"7 — Compare two PATHs.
diff <(bash -lc 'echo "$PATH"' | tr ':' '\n') <(bash -c 'echo "$PATH"' | tr ':' '\n')8 — Clean up. rm -rf /tmp/fakehome /tmp/s.sh
Check Your Understanding
1. You add an alias to ~/.bash_profile. It works in a new terminal on your Linux desktop but not when you SSH in — or the other way round. Why is either possible?
2. You add a line to ~/.profile and nothing happens. What should you check first?
3. Why does a cron job fail to find a tool that works in your terminal?
4. Where should export PATH="$HOME/bin:$PATH" go, and why not the other file?
5. What does [ -f ~/.bashrc ] && . ~/.bashrc in .bash_profile achieve?
6. You are on macOS and nothing you put in ~/.bashrc takes effect. What is the most likely reason?
8. Remember
- Three kinds of shell: login, interactive, non-interactive — and the first two can be true at once.
- A login shell reads
/etc/profilethen the first of~/.bash_profile,~/.bash_login,~/.profile. - An interactive non-login shell reads
~/.bashrc. - A script reads nothing at all.
- A login shell does not read
.bashrc, and an interactive one does not read.bash_profile. That single sentence explains most "works here, not there" reports. - Only one of the three login files runs — the first that exists. The others are ignored however much is in them.
[ -f ~/.bashrc ] && . ~/.bashrcin.bash_profileis the standard reconciliation.- Exported variables go in the login file; aliases, functions, prompt and
shoptgo in.bashrc— because the first kind is inherited and the second is not. PATHin.bashrcaccumulates duplicates in nested shells.cronand build systems fail because of row three. Set the environment explicitly in the script.BASH_ENVis the one hook a non-interactive shell has, and using it is usually a mistake.$-containsiwhen interactive;shopt -q login_shellanswers the other question.- macOS defaults to
zsh, which reads different files, and its Terminal starts login shells where Linux usually does not. - Instrument rather than guess:
echo "[read $BASH_SOURCE]" >&2at the top of each candidate. - A scheduled job reads almost none of them, so functions and
PATHedits made here do not exist in a flow — Chapter 22.6 §2 is the one-command test for it. - Write the environment down at the time of the run. Chapter 25.8 captures it in four files whose diff isolated exactly five real differences out of 61 lines.
The reconciliation line in §4 used . to pull one file into another, and §5 mentioned sourcing a tool's setup file. That verb has been doing quiet work since Chapter 7.4. The next chapter makes it the subject.
Where this fits
Part of the Linux & Bash curriculum.
