TechEarl

which vs type vs command -v: Find a Command Path

which vs type vs command -v for finding where a command lives: which is an external program with portability problems, command -v is the POSIX-standard choice for scripts, and type tells you the most.

Ishan Karunaratne⏱️ 10 min readUpdated
Share thisCopied
which vs type vs command -v compared: which is a non-standard external program, command -v is the POSIX way to resolve a command path in scripts, and type reports aliases, functions, and builtins.

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.

bash
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 ll is an alias and you run which ll, you get nothing useful even though ll works fine when you type it.
  • It is not specified by POSIX. The POSIX standard defines command -v and type (type as part of the shell), but there is no standard which. What you get depends on the distro.
  • Implementations diverge. Debian and Ubuntu historically shipped a small shell-script which from the debianutils package; many other systems ship the GNU which C program; macOS ships a BSD version; zsh and tcsh provide their own which builtins that behave differently again. Same command name, different output, different exit-code conventions.
  • It can lie about the exit code. Some which variants return success even when the command is not found, or print the "not found" message to stdout instead of stderr. A script that checks if which foo can 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.

bash
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:

bash
if ! command -v jq >/dev/null 2>&1; then
    echo "This script needs jq. Install it and re-run." >&2
    exit 1
fi

Redirect 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.

bash
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 builtin

The bash-specific flags are where type earns its place:

bash
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/git

type -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

ToolWhat it isSees aliases / functions / builtinsPOSIXBest for
command -vShell builtinYesYes (the standard answer)Scripts: "is X installed?" and resolving the path the shell would use
typeShell builtinYesYes (type is in the standard; -a/-t/-p are bash extensions)Interactive debugging: what kind of thing is this, and what shadows it
whichExternal programNoNoNothing 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:

bash
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/grep

command -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:

bash
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, which is 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 BSD which programs, which is the whole portability problem: you cannot rely on which which you get. command -v removes that uncertainty.
  • command -v on a builtin gives you a name, not a path. If your script genuinely needs an absolute filesystem path (to pass to another tool, to stat it, to check ownership), command -v cd returning cd is not enough. Use type -P NAME in bash (capital -P forces a PATH search 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 of type or command -V across shells and locales is fragile.
  • hash and whereis are different questions. hash -r clears bash's remembered command locations (useful right after installing something into a directory already on PATH). whereis finds binaries, sources, and man pages by filename convention, not by resolving what the shell would run. Neither replaces command -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

Sources

Authoritative references this article was fact-checked against.

Tagswhichcommand -vtypeBashShell ScriptingLinuxPOSIX

Found this useful? Pass it on.

Copied

Ishan Karunaratne

Tech Architect · Software Engineer · AI/DevOps

Tech architect and software engineer with 20+ years building software, Linux systems, and DevOps infrastructure, and lately working AI into the stack. Currently Chief Technology Officer at a healthcare tech startup, which is where most of these field notes come from.

Keep reading

Related posts

docker exec: Run Commands Inside a Running Container

Shell into a running container, run one-off commands, drop down to root when you need to install something, and the difference between an interactive session and a single command. With the alpine sh vs bash gotcha and -u for breaking out of non-root containers.