The PATH Variable

Every time you type ls, python3, or the name of a script you wrote yourself, Bash has to figure out which program you actually mean and where it lives on disk. It does this by consulting the PATH environment variable, a list of directories the shell searches, in order, until it finds an executable with that name. Without PATH, you would have to type the full location of every program every time you wanted to run it, like /usr/bin/ls instead of just ls. Understanding how PATH works — and how to extend it safely — is essential for installing software, running your own scripts by name, and diagnosing the dreaded command not found error.

Overview: What the PATH Variable Is and How It Works

PATH is an environment variable: a named piece of data that the shell keeps in memory and passes down to every program it launches. Its value is a single string containing a list of absolute directory paths separated by colons (:), for example /usr/local/bin:/usr/bin:/bin. There is nothing magical about the variable itself — it is just text — but Bash treats it specially whenever you type a command that is not a shell builtin, an alias, or a function.

When you press Enter after typing a command name, Bash goes through a resolution order before it decides what to run:

  1. Aliases — short text substitutions defined with alias.
  2. Shell keywords and functions — things like if, for, or a function you defined in your shell.
  3. Builtins — commands compiled into Bash itself, like cd, echo, and export. These do not exist as separate files on disk.
  4. External commands found via PATH — everything else, such as ls, grep, python3, or your own scripts.

For that last category, Bash splits PATH on the colons and checks each directory, left to right, for a file with the matching name that has the executable permission bit set. The first match wins — if two directories in your PATH both contain a program called python3, only the one found in the earlier directory ever runs, and the other is silently shadowed. This is why the order of directories in PATH matters, not just which directories are present.

Once Bash finds the file, it does not “run” it in the abstract — it asks the kernel to fork() a new child process (a copy of the shell) and then exec() that child, which replaces the child’s memory image with the program found on disk. The new process inherits a copy of the parent shell’s environment, including PATH, which is exactly why a script can itself call other commands by name: it has its own copy of PATH to search with.

To avoid re-scanning every directory on every single invocation of a frequently used command, Bash keeps an internal hash table that remembers the full path it found for a command name the first time it was looked up. This is why running the same command twice in a row is faster the second time, and also why installing a new program in an already-searched directory sometimes does not show up until you open a new terminal or clear the cache — the shell is still trusting its old answer.

Notably, the current directory (.) is not included in PATH by default on Linux. This is a deliberate security decision: if it were, running a command inside an untrusted directory could silently execute a malicious file with the same name as a common command, instead of the real one on your system.

Syntax: Viewing and Modifying PATH

The general shape of a PATH value is a colon-separated list of directories:

PATH="<dir1>:<dir2>:<dir3>:..."

In practice you almost never write the whole list out from scratch — you read the current value and add to it. The key commands and concepts:

Command / Concept Purpose
echo "$PATH" Print the current value of PATH for this shell session.
export PATH="newdir:$PATH" Prepend a directory so it is searched first (session only).
export PATH="$PATH:newdir" Append a directory so it is searched last (session only).
which cmd Show the full path of the executable that would run for cmd, based on the current PATH.
type -a cmd Show every place cmd resolves to — builtin, alias, function, and every matching file on PATH.
hash -r Clear the shell’s cached command locations, forcing a fresh PATH search next time.

Changes made with export PATH=... at the prompt only last for the current shell session (and any child processes it starts) — they vanish when you close the terminal. To make a PATH change permanent, you add the same export line to a shell startup file such as ~/.bashrc (read for interactive shells) so it runs automatically every time a new shell starts.

Examples

Example 1: Finding where a command actually lives

which python3

Output:

/usr/bin/python3

which searches the directories in PATH, in order, and prints the full path of the first executable file named python3 that it finds. If nothing on PATH matches, which prints nothing and exits with a non-zero status — a quick way to check whether a program is installed and reachable.

Example 2: Adding a personal scripts directory to PATH

Suppose you write a small helper script and want to run it by name from anywhere, instead of typing its full path every time.

mkdir -p ~/scripts
cat > ~/scripts/greet.sh << 'EOF'
#!/usr/bin/env bash
echo "Hello, $(whoami)!"
EOF
chmod +x ~/scripts/greet.sh
export PATH="$HOME/scripts:$PATH"
greet.sh

Output:

Hello, alex!

This creates ~/scripts, writes a small script into it, marks the script executable with chmod +x (without this, Bash finds the file but refuses to run it — permission denied), and then prepends ~/scripts to PATH so it is searched before the system directories. After that, typing greet.sh works exactly like typing ls — Bash finds it during its PATH search and executes it.

Example 3: Making the change permanent

echo 'export PATH="$HOME/scripts:$PATH"' >> ~/.bashrc
source ~/.bashrc
echo "$PATH"

Output:

/home/alex/scripts:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

The echo ... >> ~/.bashrc line appends (note the double >>, not a single >) the export line to the end of the startup file, so it will run automatically in every future interactive shell. The single quotes around the text stop Bash from expanding $HOME and $PATH immediately — we want the literal text written to the file, to be expanded later each time .bashrc runs. source ~/.bashrc re-reads the file into the current shell so you see the effect immediately, without opening a new terminal.

How It Works Step by Step

Here is what happens, in order, when you type greet.sh and press Enter, once it is set up as in Example 2:

  1. Bash checks whether greet.sh is an alias or a shell function — it is not.
  2. Bash checks whether it is a builtin like cd or echo — it is not.
  3. Bash checks its internal hash table for a remembered location — if this is the first time, there is nothing cached yet.
  4. Bash splits PATH on : and checks each directory in order for a file called greet.sh that has the execute bit set. It finds ~/scripts/greet.sh and remembers this location in the hash table for next time.
  5. Bash asks the kernel to fork() a new child process, then exec() the script inside it. Because the file starts with #!/usr/bin/env bash, the kernel actually launches bash and hands it the script as an argument.
  6. The script runs to completion and exits with a status code — 0 for success by convention, non-zero for failure — which the parent shell can inspect via $?.

You can inspect steps 3 and 4 directly:

type -a greet.sh

Output:

greet.sh is /home/alex/scripts/greet.sh

If you move or delete a script and replace it, but the shell keeps running the old cached location, clear the hash table:

hash -r

Common Mistakes

1. Overwriting PATH instead of extending it

Wrong:

export PATH=/usr/local/bin

This replaces the entire PATH with a single directory. Every other command — ls, cd as a builtin still works, but grep, cat, git, and almost everything else stops resolving, because their directories (/usr/bin, /bin, and so on) are no longer in the search list. This is one of the most common ways to accidentally break a terminal session. Fixed by always including the existing value:

export PATH="/usr/local/bin:$PATH"

2. Adding the current directory to PATH

Wrong:

export PATH=".:$PATH"

This makes Bash search the current working directory before the standard system directories, which is a real security risk: if you cd into an untrusted directory that happens to contain a malicious file named ls or cat, typing that command name would run the attacker’s file instead of the real system utility. Run local scripts explicitly with their relative path instead, and leave PATH alone:

./backup.sh

3. Forgetting to export the change

Wrong:

PATH="$HOME/scripts:$PATH"

Without export, this only sets a local shell variable named PATH — it does not become part of the environment passed to child processes. Any script or program you launch from that shell will not see the updated PATH, only the shell itself will (and only for variable expansion, not command lookup in new subshells). Always use export when the change needs to affect programs you run:

export PATH="$HOME/scripts:$PATH"

Best Practices

  • Always extend PATH by referencing its current value ("$PATH") — never assign a bare replacement unless you genuinely intend to wipe it.
  • Prepend directories you want checked first (like personal script folders you actively use) and append directories that should only be a fallback.
  • Keep personal scripts in a dedicated directory such as ~/bin or ~/.local/bin — many Debian/Ubuntu installs already add ~/.local/bin to PATH automatically if it exists.
  • Put PATH changes in ~/.bashrc (or ~/.bash_profile for login shells) so they persist across sessions, rather than re-typing export every time.
  • Never add . or any world-writable directory to PATH — both are common privilege-escalation vectors.
  • Always quote variable expansions in PATH assignments, like "$HOME/scripts:$PATH", to avoid unexpected word-splitting if a directory name ever contains a space.
  • In cron jobs and non-interactive scripts, PATH is often much shorter than your interactive shell’s PATH — use full paths to commands, or explicitly set PATH near the top of the script, rather than assuming everything you use interactively is reachable.
  • Use type -a rather than just which when debugging, since it also reveals aliases and functions that might be shadowing the command you expect to run.

Practice Exercises

Exercise 1: Create a directory called ~/tools, write a small script inside it that prints the current date, make it executable, and add ~/tools to your PATH for the current session only. Confirm it works by running the script using just its filename from your home directory.

Exercise 2: Using which and type -a, find out which directory on your PATH currently provides the python3 and bash executables on your system. Note the order of directories in echo "$PATH" and explain, in your own words, why that order determines the result.

Exercise 3: Deliberately create two scripts with the same name, hello, in two different directories that are both on your PATH, with different content in each. Predict which one will run before testing, then run hello and use type -a hello to confirm — or correct — your prediction.

Summary

  • PATH is a colon-separated list of directories the shell searches, in order, to find external commands.
  • Command resolution order in Bash is: aliases, functions, builtins, then PATH-based external commands, with the first PATH match winning.
  • Running a command found via PATH involves the kernel forking a new process and exec-ing the matching file; the shell caches the result in a hash table for speed.
  • Use echo "$PATH" to view, export PATH="newdir:$PATH" to extend for the session, and a line in ~/.bashrc to make the change permanent.
  • Never overwrite PATH outright, never add the current directory (.) to it, and always use export when the change must reach child processes.
  • which, type -a, and hash -r are your main diagnostic tools for PATH-related confusion.