tools/path-why-a-terminal-says-command-not-found-for-a-program-you-installed.md

PATH: why a terminal says command not found for a program you installed

Tools · 5 min read ·

You installed a tool, but the terminal says `command not found`, or it runs an older version. The cause is almost always `PATH`: a short list of folders the shell searches, left to right, stopping at the first match.

Switch folders on and off, reorder them, and run a command to watch the search happen. See why the order decides which version wins, how `which -a` shows every match, and why `./script` skips the list entirely.

A search list: the folder /usr/bin is checked and has no match, then /opt/py313/bin is highlighted as the place the command was found

You install a newer version of Python. You open a terminal, type python --version, and it still prints the old one. Or you install a tool and the terminal answers command not found, even though you can see the file on your disk. Nothing is broken. The terminal is following a short list of folders called PATH, and your program is not in the right place on it.

What PATH is

When you type a word such as python in a terminal, that word is the name of a command: a program stored as a file somewhere on your computer. The terminal (the shell, the program that reads what you type) has to find that file. It does not search your whole disk, which would be slow and unpredictable. It looks only in the folders listed in an environment variable called PATH. An environment variable is a named piece of text that the shell hands to every program it starts.

On macOS and Linux, PATH is one line of text with folders separated by colons. On Windows the separator is a semicolon. You can print it:

$ echo $PATH
/tmp/w/a:/tmp/w/b:/usr/bin:/bin

(Yours will show different folders. That output is from a small test setup, with two folders of my own put in front.) The rule has two parts. The shell checks the folders from left to right, and it runs the first match it finds. It stops there. Folders further right are never looked at for that command.

Try it: build a PATH and run a command

Below is a pretend computer with four folders. Two of them hold a program named python, an old one and a new one. Switch folders on or off, move them up and down, then run a command and watch the search.

Things to try. Run python with the default settings: you get 3.9, found in /usr/bin. Switch on /opt/py313/bin: nothing changes, because /usr/bin still comes first. Move the new folder above it and run again: now you get 3.13. Then run backup: it fails until you switch on ~/scripts. Last, type ./backup and see why a path with a slash behaves differently.

Why "command not found" happens

If no folder on the list holds a file with that name, the shell gives up. In bash the message looks like this, and the program's exit code is 127, which is the shell's way of saying "I could not find it":

$ greet
bash: line 1: greet: command not found
exit=127

The file may exist. It just lives in a folder that is not on PATH. Two fixes work: put the file in a folder that is on PATH, or add its folder to PATH.

Adding a folder, and why order matters

To add a folder for the current terminal session, put it at the front or the back of the existing value:

export PATH="$HOME/scripts:$PATH"   # front: checked first
export PATH="$PATH:$HOME/scripts"   # back: checked last

Here $PATH means "the current value", so you are building a new line out of the old one. The front is where you want a newer tool to win over an older one with the same name. The back is the safer place for something you only want when nothing else matches. To make it stick for every new terminal, the same line goes into your shell's startup file (for example ~/.zshrc or ~/.bashrc), but that file differs by shell and system, so check which one yours reads.

I tested the front version with two folders that each contain a program called greet. With folder a first, the command printed hello from a. With folder b first, the same command printed hello from b. Same name, same two folders, different winner. Only the order changed.

Seeing every match

When the wrong version runs, ask the shell for every match, not just the winner. which -a lists them in the order they are searched:

$ which -a greet
/tmp/w/a/greet
/tmp/w/b/greet

The first line is the one that runs. If the line you wanted is second, the folder order is your problem.

Python can do the same lookup, which is handy for seeing the rule in code. shutil.which takes a name and a PATH string:

import shutil
print(shutil.which('greet', path='/tmp/w/a:/tmp/w/b'))
print(shutil.which('greet', path='/usr/bin:/bin'))

Running it prints:

/tmp/w/a/greet
None

The first call finds the file in folder a and stops. The second finds nothing and returns None, which is the same situation as "command not found".

The slash exception

If what you type contains a slash, the shell does not use PATH at all. It treats the text as the location of the file. ./greet means "the file called greet in the folder I am in right now", and it ran fine in my test even with a PATH that had no greet in it. That is why ./script.sh works from the folder where the script lives, while plain script.sh says command not found.

What to do with this

Next time a command is missing or the wrong version runs, work through three questions. What does echo $PATH show? What does which -a name list? Is the folder holding the file on the list, and is it before the older copy? A shell may also remember where it found a command earlier in the same session, so after you change something and it still behaves oddly, opening a fresh terminal rules that out. Most "it's installed but it won't run" problems turn out to be a folder missing from one short line of text.

$ ls tools/

see all →
tools/gitignore-how-git-decides-which-files-to-skip-and-the-one-rule-that-su.md Gitignore: how Git decides which files to skip, and the one rule that surprises everyone Why does a file show up in git status when you wanted it hidden, and why does a file you ignored sometimes stay tracked anyway? Five short… 5 min read · 27 Oct tools/pipes-how-the-in-a-terminal-turns-small-commands-into-one-big-answer.md Pipes: how the | in a terminal turns small commands into one big answer How do you find the most popular page in a file of 15 million visits? In a terminal, you join small programs with the vertical bar `|`… 5 min read · 23 Oct tools/git-branches-why-making-one-doesn-t-copy-your-project.md Git branches: why making one doesn't copy your project When someone says "make a branch for that," it sounds like Git must copy your whole project. It doesn't. A branch is just a name stuck on… 4 min read · 20 Oct tools/git-staging-area-why-saving-your-work-takes-git-add-and-git-commit.md Git staging area: why saving your work takes git add and git commit Why does saving your work in Git take two commands, `git add` and `git commit`? Because a change travels through three places: your working… 4 min read · 17 Oct tools/git-merge-vs-rebase-same-code-two-different-histories.md Git merge vs rebase: same code, two different histories Git can combine two lines of work in two ways. Merge ties them together with a new commit and leaves every old commit alone. Rebase replays… 4 min read · 11 Oct tools/what-does-1-2-3-mean-in-package-json-version-rules-you-can-try.md What does ^1.2.3 mean in package.json? Version rules you can try Open any package.json and you'll see versions like `^4.17.21` or `~1.2.3`. That little symbol is a rule for which updates npm may install… 5 min read · 8 Oct