Which startup file runs¶
Level: 101 · you put a line in ~/.bashrc, opened a new terminal, and nothing changed
One line: A shell picks its startup files by two questions, whether it is a login shell and whether it is interactive, and each shell answers them with different files. bash reads ~/.bash_profile for a login shell and ~/.bashrc for an interactive one, and a shell that is both reads only ~/.bash_profile. zsh reads ~/.zshenv every time and adds ~/.zprofile and ~/.zshrc as they apply. fish reads config.fish every time and lets the file ask.
Measured¶
Seven files under a throwaway HOME each print their own name: ~/.bashrc, ~/.bash_profile, ~/.profile, ~/.zshenv, ~/.zprofile, ~/.zshrc and ~/.config/fish/config.fish. Each shell is then started four ways, always with -c true, a command that prints nothing, so every line that appears came from a startup file:
-c truealone is the kind of shell a script runs in.-lmakes it a login shell, the kind a Mac's Terminal opens (see On a Mac).-imakes it interactive, a shell that expects a person to type.-l -iis both.
stdin is /dev/null, and each shell's stderr is thrown away: an interactive bash with no terminal says it has no job control, and on Linux it also names a process-group number that changes between runs.
Verified output of startup_files_sh.sh, identical on Linux and macOS — regenerated by tools/run_examples.py, never hand-typed.
$ cat ~/.bashrc
echo ' ~/.bashrc'
$ bash -c true
(none)
$ bash -l -c true
~/.bash_profile
$ bash -i -c true
~/.bashrc
$ bash -l -i -c true
~/.bash_profile
$ rm ~/.bash_profile
$ bash -l -i -c true
~/.profile
$ cat ~/.bash_profile
echo ' ~/.bash_profile'
[ -r ~/.bashrc ] && . ~/.bashrc
$ bash -l -i -c true
~/.bash_profile
~/.bashrc
$ bash --norc -i -c true
(none)
$ bash --noprofile -l -c true
(none)
- A script reads none of them.
bash -c trueprinted(none). - bash answers each question with one file. The login shell read
~/.bash_profile, the interactive shell read~/.bashrc, and the shell that is both read only~/.bash_profile. A line that is only in~/.bashrcnever reaches a login shell. ~/.profileis a fallback. Afterrm ~/.bash_profile, the same login shell read~/.profileinstead. The bash manual ↗ gives the order:~/.bash_profile,~/.bash_login,~/.profile, and the first one that exists.- The usual fix is one line. A
~/.bash_profilethat sources~/.bashrcmade the login shell read both. --norcand--noprofileswitch the two off. The prompt page runs bash with--norcto keep a~/.bashrcout.
On a Mac¶
The files in your home folder behave the same way. The three keys on this page are shared: macOS's bash 3.2 read exactly the files that Ubuntu's bash 5.2 read, and so did the two zsh builds and the two fish builds.
The files in /etc run first, and they do differ. With a HOME that holds nothing but an empty ~/.zshrc (so zsh does not offer to write one), the prompt each shell starts with is whatever a system file set, or else the shell's own default:
Verified output of startup_system_prompt_sh.sh on Linux — regenerated by tools/run_examples.py, never hand-typed.
$ bash -i -c 'echo "$PS1"'
${debian_chroot:+($debian_chroot)}\u@\h:\w\$
$ bash -l -i -c 'echo "$PS1"'
${debian_chroot:+($debian_chroot)}\u@\h:\w\$
$ zsh -i -c 'print -r -- "$PS1"'
%m%#
$ zsh -f -i -c 'print -r -- "$PS1"'
%m%#
Verified output of startup_system_prompt_sh.sh on macOS — regenerated by tools/run_examples.py, never hand-typed.
$ bash -i -c 'echo "$PS1"'
\s-\v\$
$ bash -l -i -c 'echo "$PS1"'
\h:\W \u\$
$ zsh -i -c 'print -r -- "$PS1"'
%n@%m %1~ %#
$ zsh -f -i -c 'print -r -- "$PS1"'
%m%#
- bash on macOS sets no prompt for a shell that is not a login shell, so it keeps bash's built-in default,
\s-\v\$. A login bash gets\h:\W \u\$, the prompt macOS's/etc/bashrcsets. bash on Ubuntu gets\u@\h:\w\$, behind adebian_chroottest, the prompt Ubuntu's/etc/bash.bashrcsets, whether it is a login shell or not. - zsh on macOS gets
%n@%m %1~ %#, the prompt macOS's/etc/zshrcsets. zsh on Ubuntu has no system prompt, sozsh -istarts with zsh's own%m%#, the same prompt aszsh -f, which reads no files at all. - They do more than set a prompt. Read, not run here: macOS's
/etc/zprofilerebuildsPATHwithpath_helper, and Ubuntu's/etc/bash.bashrcprints a hint aboutsudoto members of thesudogroup unless~/.hushloginexists, which is why the bash scripts on these two pages create one. The rest of this library's examples run zsh with-fand fish with--no-config, so that no system file can change their output.
Terminal.app opens a login shell; a Linux desktop terminal usually does not. Neither is measured by CI, which has no terminal windows, so both are documented behaviour:
- On a Mac, a new Terminal window runs your shell as a login shell: About bash_profile and bashrc on macOS ↗ explains that Terminal runs
.bash_profilewhere other systems run.bashrc. On the Mac this page was written on (macOS 26.6, checked withpson 2026-09-13), Terminal's child process waslogin -pfwith the user's name, and its child was-fish. A shell whose name starts with-is a login shell (bash manual, Invoking Bash ↗). - On Linux, a terminal emulator starts a plain shell, not a login shell, unless it is told to: Greg's Wiki, DotFiles ↗ walks through xterm starting
/bin/bashwithout the-, and its-lsflag for a login shell. GNOME Terminal keeps the choice as a profile setting, Run command as a login shell (GNOME Terminal help ↗).
So a line in ~/.bashrc, the file a Linux book names, is read by a Linux terminal window and skipped by a Mac's. In bash on a Mac, put the line in ~/.bash_profile, or give ~/.bash_profile the one-line fix above. And unless you changed it, the shell in a Mac's Terminal is zsh, not bash.
In zsh and fish¶
zsh has three files where bash has two, and they add up instead of standing in for each other:
Verified output of startup_files_zsh.zsh, identical on Linux and macOS — regenerated by tools/run_examples.py, never hand-typed.
$ cat ~/.zshrc
echo ' ~/.zshrc'
$ zsh -c true
~/.zshenv
$ zsh -l -c true
~/.zshenv
~/.zprofile
$ zsh -i -c true
~/.zshenv
~/.zshrc
$ zsh -l -i -c true
~/.zshenv
~/.zprofile
~/.zshrc
$ zsh -f -l -i -c true
(none)
~/.zshenvruns every time,zsh -cincluded, and that is the kind of zsh a script runs in. A line that prints something, or takes a while, belongs in one of the other two.~/.zprofileis for a login shell and~/.zshrcfor an interactive one, and a shell that is both reads both. A Mac's Terminal opens a login shell that is also interactive, so it reads~/.zshrc: in zsh, the book's advice works with the file name changed.- zsh read neither
~/.profilenor~/.bashrcin any of the four runs. -freads nothing. It is how this library runs every zsh example.
fish reads one file every time, and the file asks what kind of shell is reading it:
Verified output of startup_files_fish.fish, identical on Linux and macOS — regenerated by tools/run_examples.py, never hand-typed.
$ cat ~/.config/fish/config.fish
echo ' ~/.config/fish/config.fish'
status is-login; and echo ' status is-login'
status is-interactive; and echo ' status is-interactive'
$ fish -c true
~/.config/fish/config.fish
$ fish -l -c true
~/.config/fish/config.fish
status is-login
$ fish -i -c true
~/.config/fish/config.fish
status is-interactive
$ fish -l -i -c true
~/.config/fish/config.fish
status is-login
status is-interactive
$ fish -c true
~/.config/fish/conf.d/prompt.fish
~/.config/fish/config.fish
$ fish --no-config -l -i -c true
(none)
config.fishran all four times,fish -cincluded.status is-loginandstatus is-interactiveare how it tells the four apart, so a line meant only for a person at a prompt goes insideif status is-interactive … end.- Files in
~/.config/fish/conf.d/run too, beforeconfig.fish. - fish read none of bash's or zsh's files.
--no-configreads nothing. It is how this library runs every fish example.- A prompt does not go in any of these.
funcsave fish_promptwrites it to~/.config/fish/functions/, and fish loads it from there by itself: seePS1belongs to bash.
The four runs, side by side:
| bash | zsh | fish | |
|---|---|---|---|
a script, -c |
nothing | ~/.zshenv |
config.fish |
login, -l |
~/.bash_profile, else ~/.profile |
~/.zshenv, ~/.zprofile |
config.fish; status is-login succeeds |
interactive, -i |
~/.bashrc |
~/.zshenv, ~/.zshrc |
config.fish; status is-interactive succeeds |
both, -l -i |
~/.bash_profile only |
all three | config.fish; both succeed |
| read nothing | --norc, --noprofile |
-f |
--no-config |
If you are coming from another library¶
- Rust. Fuzzy finding ↗ sets up fzf's key bindings with one line in
config.fish,~/.bashrcor~/.zshrc. In bash on a Mac, the~/.bashrcline needs the~/.bash_profilefix above before a Terminal window sees it. - Encodings. Locale and
LC_CTYPE↗ covers Terminal's Set locale environment variables on startup, which putsLANGinto the environment of the shell Terminal starts, before any of these files run.
See also¶
PS1belongs to bash — the prompt these files are most often asked to set- bash manual, Bash Startup Files ↗
- zsh manual, Startup/Shutdown Files ↗
- fish language, Configuration files ↗