which
Never use which to test for a command or to print its path.
Use command -v instead.
Why
command -vis a POSIX-mandated shell builtin (bash, zsh, dash, ash, ksh, busybox sh).whichis an external program and may be missing, aliased, or replaced by a shell function.whichbehavior varies across implementations: BSD, GNU, Debian's shell-scriptwhich, zsh's builtin, and busybox's applet differ in flags, output format, and exit codes.command -vreports shell builtins, functions, and aliases — not just files on$PATH— which is what callers usually actually want.command -vreturns a reliable non-zero exit code when the name is not found, with no output on either stdout or stderr.
Direct substitutions
| Don't | Do |
|---|---|
which foo |
command -v foo |
which foo >/dev/null 2>&1 |
command -v foo >/dev/null |
if which foo; then … |
if command -v foo >/dev/null; then … |
The 2>&1 is dropped because command -v writes nothing to stderr when
the name is not found.
Two cases have no one-liner replacement and need the idioms below:
path=$(which foo)—command -vmay return a builtin name or alias string, not a filesystem path.which -a foo—command -vresolves only the first hit.
Idioms
Existence check (silent):
if command -v foo >/dev/null; then
# ...
fi
Existence check with a clear failure message:
command -v foo >/dev/null || {
echo "foo is required but not installed" >&2
exit 1
}
Resolve a filesystem path:
foo_path=$(command -v foo) || {
echo "foo not found" >&2
exit 1
}
case "$foo_path" in
/*) ;;
*) echo "foo is not an external binary (got: $foo_path)" >&2
exit 1 ;;
esac
command -v may output:
/usr/bin/foo— external file on$PATH;foo— shell builtin or function;alias foo='…'— alias.
The /* guard accepts only the first form.
Edge cases
Listing every match on
$PATH(which -a) has no single-call POSIX equivalent. Walk$PATHexplicitly:( IFS=: for dir in $PATH; do candidate=${dir:-.}/foo [ -f "$candidate" ] && [ -x "$candidate" ] && printf '%s\n' "$candidate" done )[ -f ]together with[ -x ]filters to regular executable files — matching whatwhich -areports;[ -x ]alone would also accept directories with the execute bit.In bash and zsh,
type -a foois shorter but not portable.Makefiles. Recipes run through
$(SHELL), default/bin/sh. If a project setsSHELLto a non-POSIX shell, pin recipes back to a POSIX shell — e.g.SHELL := /bin/sh— before relying oncommand -v.
Scope
This rule applies to anything written or run as POSIX shell:
Bash tool invocations,
snippets in SKILL.md / README.md,
scripts under scripts/,
Makefiles,
CI configs,
Dockerfiles,
install instructions,
suggested one-liners.
Out of scope:
fish,
and tooling that wraps which internally
(CMake's find_program, npm bin, etc.).
If you encounter a which call while editing a file,
replace it with the equivalent command -v form as part of the same edit.