Bash & the POSIX Shell Module 13 – The interactive shell
Startup files, PATH and login shells
Which startup files Bash reads for login, interactive and non-interactive shells, where to set PATH, and why a script fails in cron or CI.
What you will learn
- Explain which startup files Bash reads for a login, an interactive and a non-interactive shell
- Set PATH and environment variables in the startup file that is actually read
- Diagnose a script that works in your terminal but fails in cron or CI
Before you start
What you’ll learn · Before you start
What you will learn
- Explain which startup files Bash reads for a login, an interactive and a non-interactive shell
- Set PATH and environment variables in the startup file that is actually read
- Diagnose a script that works in your terminal but fails in cron or CI
Before you start
On this page
You add a line to ~/.bashrc to put ~/bin on your PATH, open a new terminal, and it works. Then the same
command runs in a cron job and fails with “command not found”, exit status 127, because a cron job never reads
~/.bashrc at all.
Bash reads a different set of startup files depending on how it was started: as a login shell, as an interactive
shell, or as a plain script. Here you learn which file each kind reads, where to set PATH so every shell sees it,
and how to fix the classic “works in my terminal, fails in cron” bug.
Which file runs: login, interactive, or a script
A login shell: The shell your account starts when you log in, recorded in the SHELL variable. It need not be the shell that is running a given script. More in “Shells, terminals and which Bash you have” is the one your account starts when you log in. An interactive shell: A shell that reads commands you type at a prompt, rather than from a script, with history, aliases and line editing turned on. An interactive Bash that is not a login shell reads ~/.bashrc when it starts. is any shell that reads commands you type. A non-interactive shell runs a script and reads nothing you type. Each reads different startup file: A file Bash reads when it starts, such as ~/.bash_profile for a login shell or ~/.bashrc for an interactive shell, used to set up the environment.. The session makes three files in a sandboxed home that announce themselves, then starts each kind of shell:
from session import run
# Which startup files a login, an interactive non-login and a non-interactive Bash reads. Three files in a sandboxed
# HOME announce themselves; the login shell reads .bash_profile, the interactive shell reads .bashrc, the plain script
# reads neither until BASH_ENV names a file.
run("""
printf '%s\\n' 'echo "ran: .bash_profile"' > ~/.bash_profile
printf '%s\\n' 'echo "ran: .bashrc"' > ~/.bashrc
printf '%s\\n' 'echo "ran: .profile"' > ~/.profile
bash --login -c 'echo "(login shell ran its body)"'
bash -i -c 'echo "(interactive shell ran its body)"' 2>/dev/null
printf '%s\\n' 'echo "(plain script ran its body)"' > show.sh
bash show.sh
BASH_ENV=~/.bashrc bash show.sh
""") Output
$ printf '%s\n' 'echo "ran: .bash_profile"' > ~/.bash_profile $ printf '%s\n' 'echo "ran: .bashrc"' > ~/.bashrc $ printf '%s\n' 'echo "ran: .profile"' > ~/.profile $ bash --login -c 'echo "(login shell ran its body)"' ran: .bash_profile (login shell ran its body) $ bash -i -c 'echo "(interactive shell ran its body)"' 2>/dev/null ran: .bashrc (interactive shell ran its body) $ printf '%s\n' 'echo "(plain script ran its body)"' > show.sh $ bash show.sh (plain script ran its body) $ BASH_ENV=~/.bashrc bash show.sh ran: .bashrc (plain script ran its body)
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 startup_trace_session.py
The login shell (bash --login) printed ran: .bash_profile before its body: a login shell reads /etc/profile,
then the first file that exists of ~/.bash_profile, ~/.bash_login and ~/.profile. The interactive shell
(bash -i) printed ran: .bashrc: an interactive non-login shell reads ~/.bashrc only. The plain script
(bash show.sh) read none of them. Only when BASH_ENV named a file did the script read it first.
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- A login shell. A login shell (bash --login, or the shell you get when you log in) reads /etc/profile, then the first file that exists of ~/.bash_profile, ~/.bash_login and ~/.profile. In the session it ran ~/.bash_profile.
- An interactive non-login shell. A shell you open in an already-logged-in terminal is interactive but not a login shell. It reads ~/.bashrc only. In the session it printed "ran: .bashrc".
- A non-interactive script. Running bash show.sh starts a non-interactive shell. It reads none of these files, so a setting that lives only in ~/.bashrc is not there.
- Unless BASH_ENV names a file. A non-interactive Bash reads the file named in $BASH_ENV, if any. BASH_ENV=~/.bashrc bash show.sh ran ~/.bashrc first. (Bash started by sshd for ssh host cmd reads ~/.bashrc as a special case.)
- The usual pattern. To give both login and interactive shells the same settings, a short ~/.bash_profile sources ~/.bashrc, so PATH and the rest are set once and read by both.
| How Bash started | File it reads first |
|---|---|
Login shell (bash -l, the login terminal) |
/etc/profile, then first of ~/.bash_profile, ~/.bash_login, ~/.profile |
| Interactive, not a login shell | ~/.bashrc |
Non-interactive (bash script.sh) |
nothing, unless $BASH_ENV names a file |
One platform note: macOS has used zsh as the default login shell since macOS 10.15, so Bash reads its startup files
only when you run bash yourself. The files and the rules are the same; which shell greets you at login is what
differs.
Key idea
A login shell reads a profile file, an interactive shell reads ~/.bashrc, and a plain script reads neither unless
$BASH_ENV points at a file.
Checkpoint
Check: Which file each shell reads
Put PATH and settings in the file that is read
Because the two kinds of interactive shell read different files, a setting you put in ~/.bash_profile is missing from
the terminals you open later, and a setting in ~/.bashrc is missing from a login shell. The common cure is a short
~/.bash_profile whose only job is [[ -f ~/.bashrc ]] && source ~/.bashrc, so both read the same file.
Put PATH in ~/.bashrc (sourced by both) and you are done. PATH is a
search path: PATH: the colon-separated list of directories Bash searches left to right for a command. The first match is the program that runs.: a colon-separated list of directories that Bash looks through, left to
right, for the program you name. The first match wins, so where you add a directory decides which program runs:
from session import run
# PATH order decides which program runs. Two directories hold a program of the same name; the one earlier in PATH
# wins, type -a shows every match in order, and a duplicate entry is harmless but easy to remove.
run("""
mkdir -p ~/bin ~/extra
printf '#!/bin/sh\\necho "from bin"\\n' > ~/bin/greet
printf '#!/bin/sh\\necho "from extra"\\n' > ~/extra/greet
chmod +x ~/bin/greet ~/extra/greet
PATH="$HOME/extra:$PATH"
PATH="$HOME/bin:$PATH"
type -a greet
greet
PATH="$HOME/bin:$PATH"
printf '%s\\n' "$PATH" | tr ':' '\\n' | grep -Fxc "$HOME/bin"
PATH=$(printf '%s' "$PATH" | awk -v RS=: '!seen[$0]++' | paste -sd: -)
printf '%s\\n' "$PATH" | tr ':' '\\n' | grep -Fxc "$HOME/bin"
""") Output
$ mkdir -p ~/bin ~/extra $ printf '#!/bin/sh\necho "from bin"\n' > ~/bin/greet $ printf '#!/bin/sh\necho "from extra"\n' > ~/extra/greet $ chmod +x ~/bin/greet ~/extra/greet $ PATH="$HOME/extra:$PATH" $ PATH="$HOME/bin:$PATH" $ type -a greet greet is ~/bin/greet greet is ~/extra/greet $ greet from bin $ PATH="$HOME/bin:$PATH" $ printf '%s\n' "$PATH" | tr ':' '\n' | grep -Fxc "$HOME/bin" 2 $ PATH=$(printf '%s' "$PATH" | awk -v RS=: '!seen[$0]++' | paste -sd: -) $ printf '%s\n' "$PATH" | tr ':' '\n' | grep -Fxc "$HOME/bin" 1
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 path_session.py
Two directories each hold a program called greet. After PATH="$HOME/bin:$PATH" put ~/bin first, type -a greet
listed both matches in search order (~/bin/greet before ~/extra/greet) and running greet printed from bin, the
first one. Prepending the same directory twice left a harmless duplicate: the count of ~/bin entries was 2, and the
awk one-liner that keeps the first copy of each entry brought it back to 1.
Prepend with the old PATH on the end
Write PATH="$HOME/bin:$PATH", never PATH="$HOME/bin". The second form throws away every system directory, so
ls, grep and the rest stop being found. Never put the current directory (. or an empty entry) on PATH.
Checkpoint
Check: PATH order and the right file
Why it works in your terminal but fails in cron or CI
Your terminal is a rich place: a login shell and then ~/.bashrc have set PATH, exported variables and defined
functions. A cron job or a CI step starts a non-interactive shell with almost none of that.
cron runs commands with /bin/sh and a small environment, taking HOME and LOGNAME from
the password file; CI runners start a fresh shell per step. The session uses env -i, which starts a command with an
empty environment, to reproduce the failure:
from session import run
# A terminal carries a rich environment; cron and CI start a non-interactive shell with almost nothing. env -i mimics
# that empty environment: an exported variable is gone, and a program in ~/bin is not found until the script sets PATH.
run("""
export GREETING=namaste
mkdir -p ~/bin
printf '#!/bin/sh\\necho "backup done"\\n' > ~/bin/backup
chmod +x ~/bin/backup
PATH="$HOME/bin:$PATH"
backup
!env -i PATH=/usr/bin:/bin bash --noprofile --norc -c 'echo "GREETING=[$GREETING]"; backup'
env -i HOME="$HOME" PATH=/usr/bin:/bin bash --noprofile --norc -c 'PATH="$HOME/bin:$PATH"; backup'
""") Output
$ export GREETING=namaste $ mkdir -p ~/bin $ printf '#!/bin/sh\necho "backup done"\n' > ~/bin/backup $ chmod +x ~/bin/backup $ PATH="$HOME/bin:$PATH" $ backup backup done $ env -i PATH=/usr/bin:/bin bash --noprofile --norc -c 'echo "GREETING=[$GREETING]"; backup' GREETING=[] bash: backup: command not found [exit status 127] $ env -i HOME="$HOME" PATH=/usr/bin:/bin bash --noprofile --norc -c 'PATH="$HOME/bin:$PATH"; backup' backup done
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 cron_env_session.py
In the terminal, with ~/bin on PATH, backup printed backup done. Under env -i the exported GREETING was
gone (GREETING=[]) and backup failed with “command not found”, status 127, because the small PATH did not
include ~/bin. Setting PATH inside the command fixed it. That is the whole bug: the job does not read your
~/.bashrc, so whatever it needs must be set in the job itself.
Relying on your interactive setup in a job
Wrong: a cron line 30 2 * * * backup that assumes ~/bin is on PATH. Right: set a known PATH at the top of
the script (PATH=/usr/local/bin:/usr/bin:/bin) or call the program by its absolute path, and never depend on
~/.bashrc having run.
The rule is the same for both cron and CI: treat the job as a shell that starts from nothing.
Key idea
A job reads none of your interactive startup files, so set PATH and any variable it needs inside the script or the
crontab, or call programs by their absolute path.
Checkpoint
Check: Cron and CI environments
Practise: startup files, PATH and jobs
Each question shows a shell started a particular way, or a PATH change, like the ones in the sessions. Decide what
happens, then check your answer against the recorded output:
Practice that adapts to you
Practise this lesson
7 questions about 3 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up (fresher to 2–5 years): which startup files does Bash read for a login shell, an interactive shell and a script?
A login shell reads /etc/profile and then the first file that exists of ~/.bash_profile, ~/.bash_login and
~/.profile. An interactive shell that is not a login shell reads ~/.bashrc. A non-interactive shell that runs a
script reads none of these, except the file named in $BASH_ENV if that variable is set (and Bash started by sshd for
ssh host cmd reads ~/.bashrc as a special case). The usual arrangement is a one-line ~/.bash_profile that sources
~/.bashrc, so a login shell and an interactive shell share the same settings, and PATH lives in ~/.bashrc.
Two harder questions follow, about a PATH that works by hand but not in cron, and a tool that is found in one
terminal and not another. Decide what you would check first, then open each first step:
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Fresher to mid
A backup script runs perfectly when you type it, but the same script in a cron job fails with "backup: command not found" and exit status 127. Explain the cause and two ways to fix it.
-
Mid to senior
A developer can run a tool in the terminal they log in to, but a terminal they open inside a running desktop session reports "command not found". Both are Bash. What is the likely cause and the fix?
Flashcards · 5 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
Which startup files does a Bash login shell read?
/etc/profile first; then it looks for ~/.bash_profile, ~/.bash_login and ~/.profile, in that order, and reads only the first one it finds.
-
Which startup file does an interactive non-login shell read, and which does a script read?
An interactive shell that is not a login shell reads ~/.bashrc. A shell running a script reads neither, unless BASH_ENV names a file for it to read.
-
How do login and non-login interactive shells share one set of settings?
A one-line ~/.bash_profile that sources ~/.bashrc, so the settings live in one file that both kinds of shell end up reading.
-
How does Bash search PATH, and how should a folder be added to it?
From left to right, and the first match wins. Prepend with PATH="$dir:$PATH" so the system folders stay; type -a NAME shows every match in order.
-
Why does a command that works in your terminal fail in cron or CI?
Cron and CI run the job in a non-interactive shell, and such a shell skips your startup files, so your PATH and variables are missing. Set them inside the job, or call programs by their absolute path.
Key takeaways
- A login shell reads a profile file, an interactive non-login shell reads
~/.bashrc, and a plain script reads neither unless$BASH_ENVnames a file. - A one-line
~/.bash_profilethat sources~/.bashrclets both kinds of interactive shell share one set of settings. PATHis searched left to right and the first match wins; prepend withPATH="$dir:$PATH"so the system directories stay, andtype -ashows every match in order.- Cron and CI start a non-interactive shell that reads none of your startup files, so set
PATHand any variable the job needs inside the job itself.
Terms in this lesson
- interactive shell
- A shell that reads commands you type at a prompt, rather than from a script, with history, aliases and line editing turned on. An interactive Bash that is not a login shell reads ~/.bashrc when it starts.
- search path
- PATH: the colon-separated list of directories Bash searches left to right for a command. The first match is the program that runs.
- startup file
- A file Bash reads when it starts, such as ~/.bash_profile for a login shell or ~/.bashrc for an interactive shell, used to set up the environment.
References
- Bash Reference Manual: Bash Startup Files (Chet Ramey (GNU Bash maintainer))
- Bash Reference Manual: Interactive Shells (Chet Ramey (GNU Bash maintainer))
- crontab(5): the cron table format (Linux man-pages project)
- Apple: Use zsh as the default shell on your Mac (Apple)
Report a problem with this lesson
Kept only in this browser. Your Learn progress