Skip to content
VLSI Mentor

Linux & Bash · Part I · Module 10 · The Environment, PATH and Shell Startup

PATH: How the Shell Finds a Program

You type a name and something runs. Which something is decided by one environment variable holding an ordered list of directories — and because the search stops at the first match, the order of that list quietly decides which version of a tool your whole team is using.

Intermediate18 min readLinuxPart IFundamentalsEnvironmentPATH

Part I · Module 10 · Chapter 10.2 · PATH: How the Shell Finds a Program

Chapter 1.7 said the shell finds ls by looking through PATH, and deferred the detail. This is the detail, and it matters more than it sounds: on a machine with three versions of a tool installed, PATH is the only thing deciding which one you get.

1. An Ordered List of Directories

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ echo "$PATH" | tr ':' '\n' | head -8
/opt/homebrew/bin
/opt/homebrew/sbin
/Users/engineer/.nvm/versions/node/v24.8.0/bin
/Library/Frameworks/Python.framework/Versions/3.13/bin
/usr/local/bin
/System/Cryptexes/App/usr/bin
/usr/bin
/bin

One string, colons between entries, and tr turning them into lines so a human can read it. The colon convention is exactly the one Chapter 10.1 described: the environment holds only strings, so a list is a string with a separator that everybody agrees on.

The order is the whole point.

How the shell finds a program. It takes the directories listed in the search path in order, looks in each one for a file of that name that can be executed, and stops at the very first one it finds. Later directories are never consulted, which is why the order of the list decides which copy of a command you actually run.01SPLIT THE SEARCH PATH ON COLONSthe result is an ordered list of directories02WALK THAT LIST FROM LEFT TO RIGHTone directory at a time, in order03STOP AT THE FIRST EXECUTABLE MATCHthe search ends the moment one is found04LATER DIRECTORIES ARE NEVER CONSULTEDso the order of the list decides which copy runs05NO MATCH ANYWHERE IN THE LISTcommand not found
Figure 1 — how the shell finds a program. The search path is split on colons into an ordered list of directories, which is then walked from left to right looking for an executable file of that name. The search stops at the very first match, and every later directory is never consulted — which is why the order of the list, and not anything about the directories themselves, decides which copy of a command you actually run.

Two conditions a file must meet to be chosen: it must have the name you typed, and it must be executable by you — the x bit from Chapter 5.3. A readable file with no execute bit is skipped and the search continues.

And one thing PATH does not do: it is not searched recursively. Only the directories listed, never their subdirectories.

2. Proving Which Copy You Get

Three commands, and the difference between them matters.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ which python3
/Library/Frameworks/Python.framework/Versions/3.13/bin/python3

which reports the first match — the one that will run.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ which -a python3
/Library/Frameworks/Python.framework/Versions/3.13/bin/python3
/usr/local/bin/python3
/usr/bin/python3

which -a reports all of them, in search order. Three python3 installations on an ordinary machine, which is entirely typical — and the one being used is the one that happens to be listed first.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ type -a python3
python3 is /Library/Frameworks/Python.framework/Versions/3.13/bin/python3
python3 is /usr/local/bin/python3
python3 is /usr/bin/python3

3. First Match Wins — Demonstrated

Worth doing once, because it converts an abstract rule into something you have seen.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ mkdir -p /tmp/fake/bin
$ printf '#!/bin/bash\necho "I am the FAKE ls at $0"\n' > /tmp/fake/bin/ls
$ chmod +x /tmp/fake/bin/ls

Put that directory first:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ export PATH="/tmp/fake/bin:$PATH"

$ type -a ls
ls is /tmp/fake/bin/ls
ls is /bin/ls

$ ls
I am the FAKE ls at /tmp/fake/bin/ls

ls now runs a script. The real one is still installed, still listed by type -a, and never reached.

Put the same directory last instead and nothing changes:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ export PATH="$PATH:/tmp/fake/bin"
$ type ls
ls is /bin/ls

4. Extending It Without Breaking It

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
export PATH="/opt/tool/bin:$PATH"

The $PATH on the right is what makes this safe — you are building a new value out of the old one. Leave it out and you have replaced the list:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ export PATH="/opt/tool/bin"
$ ls
bash: ls: command not found

Nothing is broken permanently; the next shell gets a fresh PATH from the startup files in Chapter 10.3. But this shell can now run almost nothing, and the error names the wrong thing — ls is exactly where it always was.

Two habits worth having:

Quote it. PATH can contain a space, and Chapter 9.2 applies here like anywhere else.

Avoid stacking duplicates. A setup script sourced twice prepends twice:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ echo "$PATH" | tr ':' '\n' | sort | uniq -d
/opt/tool/bin

Duplicates are harmless to correctness and make the list unreadable. The usual guard is to add only if it is not already there:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
case ":$PATH:" in
  *":/opt/tool/bin:"*) ;;
  *) export PATH="/opt/tool/bin:$PATH" ;;
esac

The colons around both sides are what make the test exact — without them, /opt/tool would match /opt/tool-old. (case is Chapter 16.6.)

5. The Empty Entry

This one is a genuine security problem and it is easy to create by accident.

An empty field in PATH means the current directory. All three of these contain one:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
:/usr/bin:/bin          leading colon
/usr/bin::/bin          doubled colon
/usr/bin:/bin:          trailing colon

So does an explicit .. Here is what that allows:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ cd /tmp/untrusted
$ printf '#!/bin/bash\necho DANGER\n' > ./date
$ chmod +x ./date

$ PATH=":/usr/bin:/bin" date
DANGER

A file dropped in a directory, and date runs it. On a shared machine — a scratch directory, a downloads folder, anywhere another person can write — this is how a command gets hijacked.

6. The Shell Remembers Where It Found Things

Searching the list on every command would be wasteful, so Bash caches each result:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ hash
hits	command
   1	/usr/bin/grep

The cache is normally invisible. It becomes visible when it is wrong:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ which -a mytool
/usr/local/bin/mytool

$ # ... you install a newer one earlier on PATH ...

$ mytool --version
1.0          # still the old one

Bash is using the remembered location. Clear it:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
hash -r             # forget everything
hash -d mytool      # forget one command

hash -r is the thing to try when a command insists on being a version you have just replaced, and it looks like the shell is ignoring you. Starting a new shell does the same thing.

7. Reading Somebody Else's PATH

A practical skill, because a broken environment is usually a PATH someone built up over years.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
echo "$PATH" | tr ':' '\n' | nl

Numbered, one per line, in priority order. Then:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# which entries do not exist?
echo "$PATH" | tr ':' '\n' | while IFS= read -r d; do
  [ -d "$d" ] || echo "missing: $d"
done

# which entries are duplicated?
echo "$PATH" | tr ':' '\n' | sort | uniq -d

# which entries are writable by anyone but you?
echo "$PATH" | tr ':' '\n' | while IFS= read -r d; do
  [ -d "$d" ] && [ -w "$d" ] && [ ! -O "$d" ] && echo "others can write: $d"
done

Those use the while IFS= read -r loop from Chapter 8.6 and the test syntax from Chapter 16.2. The third one is the security audit: a directory on your PATH that somebody else can write to is the same hazard as §5, with extra steps.

The engineering case

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
export VCS_HOME=/opt/synopsys/vcs/2024.09
export PATH="$VCS_HOME/bin:$PATH"

Prepended, because the point is to select a tool version. On a machine with four simulator releases installed, the one your regression uses is decided entirely by which bin directory is earliest — and "the regression used the wrong compiler" is almost always this, found with one which -a.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ which -a vcs
/opt/synopsys/vcs/2024.09/bin/vcs
/opt/synopsys/vcs/2023.03/bin/vcs

Practice

1 — Read yours.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
echo "$PATH" | tr ':' '\n' | nl

2 — Find your duplicate installations.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
for c in python3 grep ls make; do echo "== $c"; type -a "$c"; done

3 — Shadow a command, then undo it. In a throwaway shell:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
mkdir -p /tmp/fake/bin
printf '#!/bin/bash\necho "FAKE ls"\n' > /tmp/fake/bin/ls
chmod +x /tmp/fake/bin/ls
PATH="/tmp/fake/bin:$PATH" bash -c 'type ls; ls'
PATH="$PATH:/tmp/fake/bin" bash -c 'type ls'

Using PATH=... bash -c scopes the change to that one command, so your shell is never affected.

4 — Break it on purpose, in a subshell.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
( export PATH="/opt/nothing"; ls )
echo "your shell is fine: $(ls /tmp >/dev/null && echo yes)"

5 — Create and detect an empty entry.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
unset TOOLBIN
( export PATH="$PATH:$TOOLBIN"; case "$PATH" in (*:) echo "trailing empty entry created";; esac )

6 — See the hash cache.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
hash -r
grep --version >/dev/null; sort --version >/dev/null 2>&1
hash

7 — Audit for missing and duplicated entries. Run the two loops from §7.

8 — Add a directory the safe way.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
mkdir -p ~/bin
case ":$PATH:" in *":$HOME/bin:"*) echo "already there" ;; *) export PATH="$HOME/bin:$PATH" ;; esac
echo "$PATH" | tr ':' '\n' | head -2

Check Your Understanding

1. which -a python3 lists three. Which one runs, and why?

2. You add export PATH="$PATH:/opt/tool-2.0/bin" to get a newer tool and still get the old one. What went wrong?

3. export PATH="/opt/tool/bin" and now almost nothing works. What happened, and how do you recover?

4. Why is a leading colon in PATH a security problem?

5. You install a new version of a command earlier on PATH and the shell keeps running the old one. What is happening?

6. Why is type -a a better first question than which?

8. Remember

  • PATH is an ordered, colon-separated list of directories, searched left to right.
  • The first executable match wins and later directories are never consulted.
  • It is not recursive — only the listed directories, never their subdirectories.
  • A file needs the right name and the execute bit to be chosen.
  • which -a and type -a list every match in search order. Most machines have several copies of the common tools.
  • Prefer type -a: it is a builtin and sees aliases, functions and builtins too.
  • Prepend to override, append to supplement. Appending when you meant to override fails silently.
  • Always build on $PATH: export PATH="/new:$PATH". Omitting it replaces the list.
  • An empty field means the current directory — a leading, doubled or trailing colon, or a bare .. Never allow one.
  • An unset variable in a PATH line creates an empty entry. export PATH="$PATH:$MAYBE_UNSET" is a real hazard.
  • Bash caches lookups. hash -r when a command insists on being the version you just replaced.
  • No such file or directory means a stale cache; command not found means the search failed.
  • Guard against duplicates with a case test using colons on both sides.
  • Which simulator your regression ran is a PATH question, answerable with one which -a.
  • Two installations of one tool are normal in EDA, and PATH order picks the version — Chapter 25.2 measures the same command reporting 2024.09 and 2025.03, library included.

You can now say which program a name resolves to. The next question is where PATH was set in the first place — which depends entirely on what kind of shell you are in, and is the single most common reason a setting works in one place and not another.

Where this fits

Part of the Linux & Bash curriculum.