shell · difficulty ◆
which — where is that program, really?
One line, and you know exactly which binary the shell is about to run.
You thought you ran python3. The box ran a totally different python3. Here's the one command that would have caught it.
AI-generated draft · reviewed and editedThis text was generated with AI as a first draft, then reviewed and edited by the author, who holds editorial responsibility for it.$ whichWhat it does
which walks the PATH directories in order and prints the absolute path of the first executable it finds matching the name you give it. No command, you get nothing; a name that's not on PATH, you get silence and an exit code of 1; and when more than one binary with that name is on PATH, plain `which` shows only the first match. It answers the single most useful question in debugging: when I type this name, which file is actually going to run?
Why it matters
Version mismatch bugs are the classic: you `pip install`ed a newer tool, kept typing the name, and it kept running the old binary because the new one landed earlier in PATH — or later. `which python3` (or `which -a python3`) shows you exactly which paths are in play and in what order. It's also why every script that starts with `if ! which docker; then...` is politely telling you the dependency isn't installed. Two minutes of `which` beats an hour of blaming the wrong version.
Example
$ which python3 git docker curl/usr/bin/python3
/usr/bin/git
/usr/bin/docker
/usr/bin/curlThe everyday ask: give it several names at once and it returns each absolute path on its own line. If one name in the list is missing, which prints the rest and returns 1 overall — so a non-zero exit doesn't necessarily mean every lookup failed.
$ which -a python3/usr/bin/python3
/usr/bin/python3
/usr/bin/python3
/usr/bin/python3
/home/kmail/.hermes/installs/7c9a0487…/venv/bin/python3
/home/kmail/.hermes/tools/python-3.14.7…/bin/python3
/home/kmail/.hermes/hermes-agent/venv/bin/python3
/bin/python3-a shows EVERY match along PATH, not just the first: four copies under /usr/bin, two virtual environments, a tool build and /bin on top. When a version manager, a venv and a system install shadow each other, this is the command that explains half of your 'wrong version' bugs — and the first line is the one that wins, because PATH is walked in order. Two details worth knowing: the repeats are PATH's fault, not which's (this PATH lists /usr/bin four times; FreeBSD's manual documents the same duplicates when a directory appears twice), and /bin/python3 is the same file as /usr/bin/python3 on a merged-/usr system. Long paths are abbreviated here with …; your own list will look different.
$ which definitely-not-installedA name that's nowhere on PATH prints nothing at all and returns exit status 1 — the shell's silent 'not found'. This is the exact check wrapping scripts use: `if ! which tool; then echo "install tool first"; exit 1; fi`.
$ which cd; echo "shell: $?"
command -v cd; echo "command -v: $?"shell: 1
cd
command -v: 0The trap that bites everyone: `which cd` finds nothing because cd is a shell BUILTIN, not a file on disk. `command -v cd` handles both worlds — it reports builtins too (printing the name) and returns 0. Use command -v for the accurate full test; use which when you specifically want the disk path.
$ which ssh scp rsync/usr/bin/ssh
/usr/bin/scp
/usr/bin/rsyncMulti-tool sanity check before a script that shells out to remote-copy tools: if any of these comes back empty, your provisioning script is about to fail at line 3 instead of line 40.
$ if which -s docker; then echo "docker present"; else echo "docker missing"; fidocker present-s gives you the answer without printing the path — the shape you want inside a provisioning script, where all that matters is whether the tool resolves. Verified here, where docker is installed; on a host without it the same line prints 'docker missing' and the whole if-statement exits 1.
Common flags
- which NAME...
- Print the first PATH match for each NAME, one per line. Exit 0 if every name is found, 1 if any is missing.
- -a
- Show ALL matches along PATH, in order — reveals every shadowed copy of the same binary.
- -s, --silent
- -s: No output, just the exit status — 0 if found, 1 if not. The version for scripts: test `which -s` in an if without scattering paths across the screen. Debian's which and FreeBSD's both take -s; the long form --silent is not portable (GNU's which package rejects it).
- command -v
- Not a flag but the accurate alternative: `command -v` reports builtins and functions too, not only files on PATH, and it is the one POSIX specifies — `command -V` spells the answer out in words. The right tool when you must know whether the name resolves at all.
- type -a
- The verbose superset of which: shows aliases, functions, builtins, and every PATH match with labels. Best for truly confusing name collisions.
- PATH order matters
- which only reports the first match by default — which one wins depends entirely on directory order in your PATH, not on newest install.
- which which
- Yes, this works, and the meta-answer is the which binary itself — a fun first smoke test that your PATH is intact.
History
A tiny utility with a long Unix lineage
which is a small command with a lineage people usually get backwards. It came up alongside the C shell — csh had aliases and a search path, and needed a way to answer 'where does this name come from' — and it first appears in 3BSD at the end of 1979, which makes it about the same age as V7's Bourne shell and not older. There has never been one canonical implementation: on this box /usr/bin/which is Debian's debianutils build, a small compiled program behind /etc/alternatives; FreeBSD ships its own; and GNU's which is a separate package that was never part of coreutils. That variety is exactly why the flags differ from system to system, and why the modern shells grew richer lookups of their own (type, command -v) that understand aliases and builtins — but plain which stays the go-to when you want the literal file path.
The -a flag exists because of shadowing
The single-match default was fine when every machine had one Python, one sed, one awk. Then version managers (pyenv, rbenv, nvm), virtualenvs, and container host paths started putting several copies of the same name on PATH. `which -a` was the escape hatch: show every copy so you can see the shadowing that plain which hides. The rise of per-project environments is exactly why `which -a python3` became a debugging ritual.
Fun facts
Pros & cons
pros
- + Instant answers: one line, absolute path, sorted by PATH priority — no guessing about which binary wins.
- + Script-friendly exit codes (0/1) for dependency checks in provisioning and install scripts.
- + Trivial to remember and copy-paste into any debugging session where a name resolves to the wrong thing.
cons
- − Ignores shell builtins and aliases, so it can report 'not found' for something your shell actually runs fine.
- − By default hides shadowed copies of the same binary — you must remember -a to see the full picture.
- − Returns 1 if any one of several names is missing, which can trip naive `&&` chains that assume all-or-nothing.
Takeaways
- 1`which NAME` prints the first path on PATH that matches — that's the binary that will actually run when you type the name.
- 2Use `which -a` before blaming a 'wrong version' bug — it reveals every shadowed copy and the order the shell consults.
- 3For a definitive 'does this name resolve' test, use `command -v NAME` — it sees builtins and aliases that which silently ignores.
- 4`which -s` gives you a clean 0/1 exit for scripts without printing paths to the log.
- 5`which which` is a working one-line smoke test that your PATH is intact.
