Embercode
✕
The TerminalLesson 13 of 14

40 min read

PATH and shell startup files

Why this matters

Every time you open a terminal window, the shell starts from nothing and sets itself up again. It sets the folders it searches for programs, the short names you defined, and any settings your tools need. These settings come from text files that the shell reads at the start. If you put a line in the wrong file, or in the right file with a small mistake, the new tool does not appear, and the error message does not say why. This lesson shows which file to use and how to change PATH without breaking it.

Lesson one showed how the shell searches PATH for a program. Lesson five showed export and the basic echo >> ~/.bashrc example. This lesson covers the order in which the files are read, what .profile is for, and how to add a folder safely.

Two kinds of shell start

A login shell is the shell you get when you log in to a machine. Examples are a text login on a server, or an ssh connection. A login shell reads its startup files in a fixed order.

An interactive shell is a shell that waits for you to type. A terminal window is usually an interactive shell. It is not always a login shell. Many desktop terminals start an interactive shell that is not a login shell. A non-interactive shell runs one command and exits, such as the shell that runs a script.

You can check which kind you have. Bash has a setting called login_shell that is on for a login shell:

$ bash -l -c 'shopt login_shell'
login_shell         	on
$ bash -c 'shopt login_shell'
login_shell         	off

The -l flag asks bash to start as a login shell. The -c flag gives bash one command to run. The shopt command reads a setting, and the setting shows as on or off.

The table below lists what each kind of bash start reads. The rest of the lesson explains each row.

How the shell startsFiles bash reads
Login shell (bash -l, or an SSH login).bash_profile or .profile, the first one that exists
Interactive, not login (most terminal windows).bashrc
Neither (a script, or bash -c)Nothing, unless the variable BASH_ENV names a file

A variable is a named value, such as PATH. The BASH_ENV variable is a rare case, and you will not need it in everyday work, so the rest of this lesson ignores it.

The bash order, step by step

Login shells read .profile

Files whose names start with a dot are hidden from a plain ls, but they exist. The file ~/.profile is read by a login shell. Bash looks for these files in this order and reads only the first one it finds: ~/.bash_profile, then ~/.bash_login, then ~/.profile.

This matters because a file with a higher priority hides the others. Here is a test in a scratch home folder. The file .bash_profile prints [.bash_profile read] when it runs, and the file .profile prints [.profile read]. Create both files first:

$ mkdir -p ~/test-profile
$ echo 'echo "[.bash_profile read]"' > ~/test-profile/.bash_profile
$ echo 'echo "[.profile read]"' > ~/test-profile/.profile
$ HOME=~/test-profile bash -l -c 'echo command ran'
[.bash_profile read]
command ran

The .profile file was not read, because .bash_profile took priority. If you create .bash_profile by mistake, .profile silently stops running. That is one of the most confusing causes of a missing setting.

Ubuntu's default .profile reads .bashrc

On Ubuntu, a new user gets a .profile file that contains this block. The lines are copied from the default file:

if [ -n "$BASH_VERSION" ]; then
    # include .bashrc if it exists
    if [ -f "$HOME/.bashrc" ]; then
        . "$HOME/.bashrc"
    fi
fi

The first line checks that the shell is bash. The next lines run .bashrc if the file exists. The dot at the start of the line is the same as the source command from lesson five: it runs the file in the current shell. So on Ubuntu, a login shell reads .profile, and .profile then reads .bashrc. A login shell therefore gets your .bashrc settings too.

The same default file has two more blocks. They add ~/bin and ~/.local/bin to the front of PATH, but only when those folders exist. So on Ubuntu, ~/bin may already be on PATH after you log in. Check with echo "$PATH" | tr ':' '\n' before you add it yourself.

There is a catch. The .bashrc link lives in .profile. If .profile does not exist, a login shell does not read .bashrc. This test uses a home folder with a .bashrc that exports a variable and no .profile. Create it first:

$ mkdir -p ~/no-profile
$ echo 'export LAB_FROM_BASHRC=yes' > ~/no-profile/.bashrc
$ HOME=~/no-profile bash -l -c 'echo "login no profile: ${LAB_FROM_BASHRC:-unset}"'
login no profile: unset

The variable was not set, because no login file read .bashrc.

Interactive shells read .bashrc

A terminal window that is not a login shell reads ~/.bashrc directly. The test in the next section shows this case, along with the login case.

The ${LAB_FROM_BASHRC:-unset} form in these tests prints the value of the variable, or the word unset when the variable is empty or missing.

Why .bashrc starts with a check

The default .bashrc on Ubuntu starts with this block:

# If not running interactively, don't do anything
case $- in
    *i*) ;;
      *) return;;
esac

The variable $- lists the options that are on for the current shell, and the letter i means interactive. If the shell is not interactive, the file stops at return. This protects scripts. A script that reads .bashrc would otherwise run your prompt settings and aliases, which can change its output. Keep this block at the top of .bashrc if you copy the file.

The three cases in one test

Here is a single test with one .bashrc that exports a variable, and a .profile that only includes .bashrc. The test runs bash three ways. The output shows which case reads the file:

$ mkdir -p ~/test-home
$ echo 'export LAB_FROM_BASHRC=yes' > ~/test-home/.bashrc
$ echo '. "$HOME/.bashrc"' > ~/test-home/.profile
$ HOME=~/test-home bash -l -c 'echo "login: ${LAB_FROM_BASHRC:-unset}"'
login: yes
$ HOME=~/test-home bash -i -c 'echo "interactive: ${LAB_FROM_BASHRC:-unset}"'
interactive: yes
$ HOME=~/test-home bash -c 'echo "plain: ${LAB_FROM_BASHRC:-unset}"'
plain: unset

The line export LAB_FROM_BASHRC=yes was written in .bashrc. A login shell reads it through .profile. An interactive shell reads it directly. A plain bash -c reads neither file, so the variable is missing there.

The plain bash -c case matters for scripts. A script started with bash script.sh does not see the settings in your .bashrc. If a script needs a value, set it inside the script, or pass it in as a variable.

The zsh order

Zsh, the default shell on macOS, has four startup files. Each has its own job:

FileRead by
~/.zshenvEvery zsh, including scripts
~/.zprofileLogin shells, before .zshrc
~/.zshrcInteractive shells, such as terminal windows
~/.zloginLogin shells, after .zshrc

The test below creates one file for each name, and each file prints its own name when zsh reads it. Then it starts zsh three ways:

$ mkdir -p ~/zsh-test
$ echo 'echo "  [.zshenv]"' > ~/zsh-test/.zshenv
$ echo 'echo "  [.zprofile]"' > ~/zsh-test/.zprofile
$ echo 'echo "  [.zshrc]"' > ~/zsh-test/.zshrc
$ echo 'echo "  [.zlogin]"' > ~/zsh-test/.zlogin
$ HOME=~/zsh-test zsh -l -i -c 'echo "  command ran"'
  [.zshenv]
  [.zprofile]
  [.zshrc]
  [.zlogin]
  command ran
$ HOME=~/zsh-test zsh -i -c 'echo "  command ran"'
  [.zshenv]
  [.zshrc]
  command ran
$ HOME=~/zsh-test zsh -c 'echo "  command ran"'
  [.zshenv]
  command ran

Read it as a rule: .zshenv runs every time, including for a script, so it is the place for variables that every program needs. .zshrc runs for terminal windows, so it is the place for aliases, functions and the prompt. .zprofile and .zlogin run only for login shells.

Where to put what

For bash, put the settings you need everywhere in ~/.bashrc. On Ubuntu this file is read by a login shell through .profile, and it is read directly by a terminal window. Put export lines and PATH changes there. Put aliases and functions there too, because they only work in an interactive shell.

Do not put aliases only in .profile. A login shell reads .profile, but a terminal window that is not a login shell does not. An alias written only in .profile would work after an SSH login and not in a normal window.

For zsh, put PATH changes and exported variables in ~/.zshenv, so that every zsh sees them, including scripts. Put aliases, functions and the prompt in ~/.zshrc.

Programs started from a desktop icon, or by a scheduled job such as cron, may not read any of these files. Set what such a program needs in the program itself or in the job's own settings.

Adding a folder to PATH safely

Here is a common task: you have a folder ~/bin that holds your own commands, and you want to type their names from anywhere. The steps below make that change without breaking the rest of PATH.

Step 1: know which side to add to

PATH is searched from left to right, and the first match wins (lesson one). Adding a folder to the front or the back changes which program runs when two programs have the same name.

Adding to the front (PATH="$HOME/bin:$PATH") means your folder is searched first. A file called ls in ~/bin would run instead of the system ls. Suppose the folder ~/work/bin holds a file called ls that prints a message. Make one with the commands below, then put that folder first:

$ mkdir -p ~/work/bin
$ printf '#!/bin/sh\necho "FAKE ls in current folder"\n' > ~/work/bin/ls
$ chmod +x ~/work/bin/ls
$ PATH=$HOME/work/bin:/usr/bin:/bin ls
FAKE ls in current folder

Adding to the back (PATH="$PATH:$HOME/bin") means the system programs are found first. Your folder is used only when no system program has the name. Adding to the back is the safer choice for your own folders. Add to the front only when you need your version of a program to win on purpose.

Step 2: a missing folder is harmless

If a folder in PATH does not exist, the shell skips it without an error. The test below puts a folder that does not exist at the front. The system python3 is still found:

$ PATH=/home/you/no-such-folder:/usr/bin:/bin command -v python3
/usr/bin/python3

This means a wrong folder does not stop your tools from working. It also means a typo in the folder name is silent. Check the name when a program is not found.

Step 3: avoid adding the same folder twice

Each time a startup file runs the line PATH="$HOME/bin:$PATH", the folder is added again. Sourcing the file twice gives you two copies:

$ PATH=/usr/bin:/bin; for i in 1 2 3; do PATH="/x/bin:$PATH"; done; echo "$PATH" | tr ':' '\n'
/x/bin
/x/bin
/x/bin
/usr/bin
/bin

Duplicates do no harm to the result, but PATH grows every time you reload, and it becomes hard to read. The fix is a check before adding. The function below adds a folder only when it is not already there and only when it exists. The case block asks whether the folder is already in the list. The colons on both sides make sure that a folder name is matched as a whole word, not as part of a longer one:

add_path() {
    [ -d "$1" ] || return 0
    case ":$PATH:" in
        *":$1:"*) ;;
        *) PATH="$PATH:$1" ;;
    esac
}
add_path "$HOME/bin"

The first line checks that the folder exists, and returns without doing anything if it does not. The case block adds the folder only if it is not already in the list. The empty ;; branch means "already there, do nothing".

Step 4: never leave an empty entry

An empty entry in PATH is a problem. Two colons in a row, or a colon at the start or end, make an empty entry, and the shell reads an empty entry as the current folder. So a program in the folder you are standing in can run without you noticing:

Suppose ~/downloads holds a program called mytool2:

$ cd ~/downloads
$ PATH=/usr/bin:/bin: mytool2
mytool2 ran from current folder
$ PATH=/usr/bin:/bin mytool2
bash: mytool2: command not found

With the trailing colon, the shell searched the current folder and ran mytool2. Without it, the same program is not found. A file you downloaded can be dangerous if the shell runs it by its name. Keep the current folder out of PATH, and never add a colon at either end.

Step 5: reload and check

Changes to a startup file apply to new shells. In the window where you edited the file, run source to read it again:

$ source ~/.bashrc
$ echo "$PATH" | tr ':' '\n'

The tr ':' '\n' part replaces each colon with a line break, so the list is easy to read. Then check that the program you want is the one the shell finds. The command -v command prints the path of the first match, and type -a lists every match in order:

$ command -v python3
/usr/bin/python3
$ type -a python3
python3 is /home/you/bin/python3
python3 is /usr/bin/python3
python3 is /bin/python3

The first line of type -a is the one that runs. The folder names in the example are placeholders, and your list shows your own. If the first line is not the program you expected, the folder order is wrong.

Worked example: a personal bin folder

This example puts your own commands in ~/bin and makes them available in every new window. The example uses a scratch copy of a home folder to show the real output. Use your real home folder when you do it yourself.

First, create the folder and a small command in it:

$ mkdir -p ~/bin
$ printf '#!/bin/bash\necho "hi from mytool"\n' > ~/bin/mytool
$ chmod +x ~/bin/mytool

The printf line writes two lines to the file. The first line, #!/bin/bash, tells the system to run the file with bash. The chmod +x line marks the file as runnable (lesson five covers permissions).

Next, add the add_path function and its call to the end of ~/.bashrc. Open the file in a text editor, such as nano ~/.bashrc, and paste the function from step 3. Save the file.

Then test it in a new interactive shell. The scratch copy gives this output:

$ HOME=~/lab-home bash -i -c 'mytool; command -v mytool'
hi from mytool
/home/you/lab-home/bin/mytool

Your real output shows your own home folder in place of lab-home. The program ran from anywhere, because ~/bin is now on PATH.

Now check that a second source does not add the folder again. Running source twice and counting the copies of the folder in PATH gives one:

$ HOME=~/lab-home bash -i -c 'source ~/.bashrc; source ~/.bashrc; echo "$PATH" | tr ":" "\n" | grep -c "/lab-home/bin"'
1

grep -c counts the lines that match, and the answer is 1.

Common mistakes

Replacing PATH instead of changing it. A line such as PATH=/home/you/bin removes every other folder, so basic commands stop working. This example starts a bash with that value:

$ PATH=/home/you/bin /bin/bash -c 'ls'
/bin/bash: line 1: ls: command not found

The fix is to include the old value: PATH="$HOME/bin:$PATH". If you already broke it in a window, close that window and open a new one. The new window starts with the normal PATH.

Editing the file and expecting the open window to change. A shell reads its startup files once, when it starts. A window that was open before your edit still has the old settings. Run source ~/.bashrc in that window, or open a new window.

Setting a variable without export. A plain assignment in .bashrc sets a variable only for the shell. Programs the shell starts do not receive it. The test below shows the difference:

$ bash -c 'PLAIN=yes; bash -c "echo child sees: [\${PLAIN:-empty}]"'
child sees: [empty]
$ bash -c 'export EXPORTED=yes; bash -c "echo child sees: [\${EXPORTED:-empty}]"'
child sees: [yes]

Use export for any value that a program you start must read. Lesson five covered this.

Writing a file that is not read. A .profile file is ignored by a shell that finds .bash_profile first. A .bashrc file is not read by a login shell unless .profile includes it. If your change seems to do nothing, check the table in this lesson for the kind of shell you are in, then check login_shell with shopt.

A missing file. Sourcing a file that does not exist gives this error:

$ source ~/.bashrc
bash: /home/you/.bashrc: No such file or directory

This error means the file is missing, not that the content is wrong. Create the file or check the name.

A broken line in the file. A missing closing quote is a common typo. Bash reports where it found the problem:

$ source ~/broken.bashrc
/home/you/broken.bashrc: line 1: unexpected EOF while looking for matching `"'

EOF means "end of file". Bash reached the end of the file while it was still waiting for the closing quote. The line the message names is the place to look. The variable on that line was not set. Fix the quote, then run source again.

Using > instead of >>. The line echo 'export PATH="$HOME/bin:$PATH"' >> ~/.bashrc adds to the end of the file. A single > replaces the whole file with the one line, and every other setting is lost. Use >> to add, and open the file in an editor to change it.

Debugging walkthrough: "command not found" after I added a folder

You created ~/bin/mytool, added ~/bin to PATH, and typing mytool still says command not found. Work through these checks in order:

  1. Look at PATH. Run echo "$PATH" | tr ':' '\n' and find your folder in the list. If it is missing, the startup file did not run, or the line is wrong.
  2. Check the folder name. Run ls -ld ~/bin. A message No such file or directory means the folder is not where your PATH line says.
  3. Check the file is runnable. Run ls -l ~/bin/mytool. A runnable file has an x in its permission string. If it has none, run chmod +x ~/bin/mytool.
  4. Check which file you edited. Run shopt login_shell. If the window is a login shell, check .profile and then .bashrc through it. If the window is not a login shell, .bashrc is the file that counts.
  5. Reload or open a new window. Run source ~/.bashrc, then type -a mytool. The first line shows what the shell finds.

Each step rules out one cause. The first step tells you whether the file was read at all. The later steps tell you why not.

Practice

Exercise 1: which file runs first?

A login shell finds both ~/.bash_profile and ~/.profile. Which one is read, and what happens to the other?

Show the answer

Bash reads ~/.bash_profile and skips ~/.profile, because it reads only the first of .bash_profile, .bash_login and .profile that exists.

Exercise 2: append or prepend?

Your PATH contains /usr/bin and /bin. You add ~/tools, which contains a program called grep. Should you add ~/tools to the front or the back? Why?

Show the answer

Add it to the back, PATH="$PATH:$HOME/tools". A front position would make your grep run instead of the system grep every time. The back position uses the system program and runs yours only if no system program has the name.

Exercise 3: fix the duplicate

Your .bashrc has the line PATH="$HOME/bin:$PATH". After you run source ~/.bashrc five times, echo "$PATH" | tr ':' '\n' lists ~/bin five times. Write a version of the line that adds the folder only once.

Show the answer

Use the function from step 3: define add_path, which checks the list with a case block and adds the folder only when it is missing, then call add_path "$HOME/bin". Each source then leaves one copy.

Exercise 4: where does an alias go?

You want the alias ll='ls -lh' in every terminal window on Ubuntu, including windows that are not login shells. Which file do you use, and why is .profile alone not enough?

Show the answer

Use ~/.bashrc. A window that is not a login shell reads .bashrc directly and does not read .profile. Putting the alias only in .profile would work after an SSH login and not in a normal window.

Exercise 5: a scheduled job cannot find a variable

A scheduled job reads the variable REPORT_DIR, which you exported in ~/.profile. The job prints an empty value. Explain why, and say where the value should go.

Show the answer

A scheduled job runs without a login shell and reads none of these files by default. Set REPORT_DIR in the job's own configuration, or in the script the job runs, so it does not depend on the shell's startup files.

Summary

  • A login shell reads ~/.bash_profile or ~/.profile (only the first one that exists), and an interactive shell reads ~/.bashrc.
  • On Ubuntu, .profile reads .bashrc for login shells, so ~/.bashrc is the file to edit for settings that every window needs.
  • Zsh reads ~/.zshenv every time, ~/.zshrc for terminal windows, and the two login files for login shells.
  • Add a folder to PATH at the back, check that it is not already there, and never leave an empty entry such as a colon at either end.
  • A shell reads its files when it starts, so run source or open a new window after you edit.

Stuck? Ask the tutor

Ask about anything in this lesson. Answers are short and use simple words.

Sign in to ask the tutor.

← History, tab completion and aliases