Skip to content
VLSI Mentor

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.

KindWhat it isYou get one from
LoginThe first shell of a session; it is where you sign inSSH, a text console, bash -l, macOS Terminal tabs
InteractiveA shell with a prompt, reading what you typeOpening a terminal, typing bash
Non-interactiveA 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ env -i HOME=$FAKE PATH=/usr/bin:/bin bash -l -c 'echo "(login shell body)"'
  >> read .bash_profile
(login shell body)

An interactive shell:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ env -i HOME=$FAKE PATH=/usr/bin:/bin bash -i -c 'echo "(interactive body)"'
  >> read .bashrc
(interactive body)

A script:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ 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 .profile

The first one that exists is read, and the other two are ignored entirely.

What a login shell reads when it starts. First the system-wide profile, then exactly one of your three personal login files — whichever of them exists first in a fixed order. The important part is that only one of those three is read, so a setting placed in the second one is ignored whenever the first one exists.01THE SYSTEM-WIDE PROFILE RUNS FIRSTset by whoever administers the machine02THEN YOUR PERSONAL LOGIN FILE IS SOUGHTthree names are tried, in a fixed order03THE FIRST ONE THAT EXISTS IS READand reading it ends the search04THE OTHER TWO ARE IGNORED ENTIRELYhowever much is written in them05A SETTING IN THE WRONG FILE NEVER RUNSwhich is the usual cause of it having no effect
Figure 1 — what a login shell reads when it starts. The system-wide profile runs first, and then Bash looks for your personal login file by trying three names in a fixed order. The first one that exists is read and the search ends there, so whatever is written in the other two never runs at all — which is the usual reason a setting someone added has no effect.

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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
ls -la ~/.bash_profile ~/.bash_login ~/.profile ~/.bashrc 2>&1

4. 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# ~/.bash_profile
[ -f ~/.bashrc ] && . ~/.bashrc

Measured working:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ 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 cron job fails to find a tool that works perfectly in your terminal, because cron starts 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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"
done

2 — Run all three kinds.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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.sh

3 — Prove only one login file runs.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
printf 'echo "  >> read BASH_ENV file"\n' > $FAKE/.bashenv
env -i HOME=$FAKE BASH_ENV=$FAKE/.bashenv PATH=/usr/bin:/bin bash /tmp/s.sh

6 — Identify your own shell.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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/profile then 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 ] && . ~/.bashrc in .bash_profile is the standard reconciliation.
  • Exported variables go in the login file; aliases, functions, prompt and shopt go in .bashrc — because the first kind is inherited and the second is not.
  • PATH in .bashrc accumulates duplicates in nested shells.
  • cron and build systems fail because of row three. Set the environment explicitly in the script.
  • BASH_ENV is the one hook a non-interactive shell has, and using it is usually a mistake.
  • $- contains i when interactive; shopt -q login_shell answers 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]" >&2 at the top of each candidate.
  • A scheduled job reads almost none of them, so functions and PATH edits 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.