I learn a non-trivial number of things, because there’s Debian drama about them. The fact that the which command is not part of any standard just joined the flock.

I’ve been using which since my earliest Amiga 500 days to find the path of an executable. Recent Debian turmoil taught me that I can’t take neither the existence nor the shape of the command for granted on UNIX-like OSes, because it was never part of the POSIX (or any other AFAIK) standard.

TL;DR: If you want to be on the safe side, don’t rely on which to find the location of an executable.

Use command -v instead. On an interactive command line, type is also a great option.


which is widespread but not standardized. On Debian it’s just a shell script that they considered to remove from the essential package debianutils; making it an optional command whose presence on a Debian system isn’t given anymore.

The argument being that packagers should use standard tools, and if users insist on having a which command, they can install one of the available implementations.

The lack of any standardization aside, which in its barest form fails to handle builtins and aliases:

$ which ls; echo $?
$ which ll; echo $?  # alias
$ which if; echo $?  # shell builtin

Let me be clear that my point here is not about which not ticking some bureaucratic boxes. The problem is that there’s many different whichs, each with different options and behaviors. Just compare the arguments of the most common ones:

I’m also not trying to take away your toys – feel free to use whatever you like. I’m just pointing out compatibility problems across UNIX-like operating systems that could surprise you, and that which could suddenly vanish from your Debian installation.

And I’m presenting alternatives!

command -v

POSIX-confirming shells are required to have a builtin confusingly called command. Its function is to bypass any builtins, functions, or aliases and run the executable with the passed name.

For example in zsh:

$ alias ls="echo nope"
$ ls
$ command ls
[directory listing]

This is useful in scripts if you want to ensure to get the proper command and not some convenience alias from the user1.

Pertinent to this post, though, is its option -v that doesn’t run the command, but takes a list of names and tells you how the shell would interpret each name, if you’d run it without “wrapping” it with command:

$ command -v ls ll if echo
alias ll='ls -l'

As you can see, although the main purpose of command is to literally execute a command and ignore functions and aliases, command -v recognizes builtins and expands aliases on bash and zsh.

If you pass -V (uppercase v), you get human-readable output:

$ command -V ls ll if echo
ls is /bin/ls
ll is an alias for ls -l
if is a reserved word
echo is a shell builtin

Therefore, if you want to write cross-platform shell scripts, stick to command -v that works everywhere and has machine-readable output.


Unfortunately, my favorite shell fish has only very rudimentary support for command (no -V, no support for aliasing), so here’s a bonus tidbit: type is another shell builtin that’s standardized, and does what you’d expect:

$ type ls ll if echo  # output from zsh
ls is /bin/ls
ll is an alias for ls -l
if is a reserved word
echo is a shell builtin

One thing I like about type is the (non-standard, but common) -a argument that shows you all possible resolutions for a name:

$ type -a echo
echo is a shell builtin
echo is /bin/echo

On my beloved fish shell, I also get the full function definition:

$ type j
j is a function with definition
# Defined in /Users/hynek/.config/fish/conf.d/z.fish @ line 24
function j --description 'jump around'
        __z $argv

With type -p you can limit the output to proper executables (similar to which):

$ type -p brew
$ type -p ll


Since type is human-readable, better supported on fish, and easier to type, it’s what I use day-to-day on the shell.

For scripts, command -v is better, because it’s simpler and machine-readable.

  1. If have aliased ls to lsd, for example. ↩︎