To find where a command lives, use command -v ssh. It is built into every POSIX shell, it is the right tool in scripts, and it returns the path that the shell would actually run. Reach for type ssh when you want the full story (is it an alias, a function, a builtin, or a file on disk?), and avoid which entirely: it is an external program that is not specified by POSIX, behaves differently across distros, and does not know about your shell's aliases or functions. The short version of which vs type vs command -v is: command -v for scripts, type for interactive debugging, which for nothing.
command -v ssh
# /usr/bin/ssh
type ssh
# ssh is /usr/bin/ssh
which ssh
# /usr/bin/ssh (usually, but see below for why "usually" is the problem)All three printed the same path here. The differences only surface at the edges, and the edges are exactly where scripts break.
Why which is the wrong default
which is a separate executable, typically /usr/bin/which, not part of the shell. That single fact causes most of its problems:
- It is not a shell builtin, so it cannot see your shell state. Aliases, shell functions, and builtins are invisible to an external process. If
llis an alias and you runwhich ll, you get nothing useful even thoughllworks fine when you type it. - It is not specified by POSIX. The POSIX standard defines
command -vandtype(typeas part of the shell), but there is no standardwhich. What you get depends on the distro. - Implementations diverge. Debian and Ubuntu historically shipped a small shell-script
whichfrom thedebianutilspackage; many other systems ship the GNUwhichC program; macOS ships a BSD version; zsh and tcsh provide their ownwhichbuiltins that behave differently again. Same command name, different output, different exit-code conventions. - It can lie about the exit code. Some
whichvariants return success even when the command is not found, or print the "not found" message to stdout instead of stderr. A script that checksif which foocan then take the wrong branch silently.
Debian made this concrete: as of debianutils 5.x the which script is deprecated and prints a warning steering you to command -v. That is the maintainers of one of the most common which implementations telling you to stop using it.
So which works often enough on an interactive prompt that people keep reaching for it, and then it betrays them inside a script. Don't build the habit.
command -v: the portable, scriptable answer
command -v NAME is the POSIX way to ask "what would the shell run if I typed NAME, and does it exist?" It is a builtin, so it sees everything the shell sees, and it sets a clean exit code: 0 if the name resolves, non-zero if it does not.
command -v ssh # /usr/bin/ssh (external command -> absolute path)
command -v cd # cd (builtin -> just the name)
command -v ll # alias ll='ls -alF' (alias -> the alias text)For an external command you get the absolute path. For a builtin, alias, or function you get a description rather than a path, because there is no file to point at. That is correct behavior, not a bug: the question "where does cd live" has no filesystem answer.
The single most useful pattern is the "is this installed?" guard at the top of a script:
if ! command -v jq >/dev/null 2>&1; then
echo "This script needs jq. Install it and re-run." >&2
exit 1
fiRedirect stdout to /dev/null because you only care about the exit code, not the printed path. This is the idiom you will see in every well-written install script and CI bootstrap, and it is portable to dash, bash, zsh, and sh on BusyBox without modification.
There is also command -V NAME (capital V), which prints a human-readable sentence ("ssh is /usr/bin/ssh") instead of the bare path. Use lowercase -v in scripts, capital -V when you are reading the output yourself.
type: the one that tells you the most
type is a shell builtin (in bash, zsh, and as a POSIX shell feature) that classifies a name. It is the right tool when something runs and you do not understand why it runs the way it does.
type ll
# ll is aliased to `ls -alF'
type cd
# cd is a shell builtin
type ssh
# ssh is /usr/bin/ssh
type type
# type is a shell builtinThe bash-specific flags are where type earns its place:
type -a python3 # ALL matches, in resolution order
# python3 is /usr/local/bin/python3
# python3 is /usr/bin/python3
type -t git # just the kind: alias | keyword | function | builtin | file
# file
type -p git # path only, and ONLY if it resolves to a file on disk
# /usr/bin/gittype -a is the one I reach for constantly: it shows every match on the PATH plus any alias or function shadowing them. When you run python3 and the wrong version executes, type -a python3 shows you the shadowing entry that is winning. type -t is the scripting-friendly form when you need to branch on "is this a function or a real binary." type -p behaves like a path-only lookup but, unlike which, it still respects shell state when deciding whether to print anything.
Side-by-side
| Tool | What it is | Sees aliases / functions / builtins | POSIX | Best for |
|---|---|---|---|---|
command -v | Shell builtin | Yes | Yes (the standard answer) | Scripts: "is X installed?" and resolving the path the shell would use |
type | Shell builtin | Yes | Yes (type is in the standard; -a/-t/-p are bash extensions) | Interactive debugging: what kind of thing is this, and what shadows it |
which | External program | No | No | Nothing portable; only when an external tool genuinely needs an absolute path and you know the platform |
A worked example: when they disagree
Set up an alias and a function that shadow a real binary, then ask each tool where grep lives:
alias grep='grep --color=auto'
ls() { echo "I am not the real ls"; }
command -v grep
# alias grep='grep --color=auto'
type -a grep
# grep is aliased to `grep --color=auto'
# grep is /usr/bin/grep
which grep
# /usr/bin/grepcommand -v and type both report the alias, because the alias is what the shell actually runs. which, being an external process, never learns the alias exists and reports the binary as if nothing were in front of it. If you were debugging "why does my grep have colors I didn't ask for", which actively points you at the wrong answer while type -a hands you the whole resolution chain.
For ls, the function wins:
type ls
# ls is a function
# ls ()
# {
# echo "I am not the real ls"
# }type even prints the function body. which ls would just print /bin/ls and leave you confused about why ls prints nonsense.
Honest caveats
- macOS and zsh have a builtin
which. On zsh,whichis a shell builtin and does see aliases and functions, so the worst objection above does not apply there. But the behavior still differs from the GNU and BSDwhichprograms, which is the whole portability problem: you cannot rely on whichwhichyou get.command -vremoves that uncertainty. command -von a builtin gives you a name, not a path. If your script genuinely needs an absolute filesystem path (to pass to another tool, tostatit, to check ownership),command -v cdreturningcdis not enough. Usetype -P NAMEin bash (capital-Pforces aPATHsearch and prints a path even when a builtin or alias would otherwise match), or accept that builtins have no path.- Exit codes are the contract in scripts. Test the exit status (
if command -v foo), not the printed text. Parsing the human-readable output oftypeorcommand -Vacross shells and locales is fragile. hashandwhereisare different questions.hash -rclears bash's remembered command locations (useful right after installing something into a directory already onPATH).whereisfinds binaries, sources, and man pages by filename convention, not by resolving what the shell would run. Neither replacescommand -v.
The rule that has never let me down: command -v in scripts, type -a when I am standing at the prompt trying to figure out what just ran, and which never.
FAQ
See also
- How to make a file executable on Linux once
command -vconfirms the script is on yourPATH, the next thing it needs is the execute bit. - Bash functions: arguments, return values, and scope the install-guard idiom from this article almost always sits inside a function or at the top of a function-driven script.
Sources
Authoritative references this article was fact-checked against.





