Why not use “which”? What to use then?

When looking for the path to an executable or checking what would happen would you enter a command name in a Unix shell, there's a plethora of different utilities (which, type, command, whence, where, whereis, whatis, hash...).
We often hear that which should be avoided. Why? What should we use instead?
ANSWER:-



The reasons why one may not want to use which have already been explained, but here are a few examples on a few systems where which actually fails.

On Bourne-like shells, we're comparing the output of which with the output of type (type being a shell builtin, it's meant to be the ground truth, as it's the shell telling us how it would invoke a command).

Many cases are corner cases, but bear in mind that which/type are often used in corner cases (to find the answer to an unexpected behaviour like: why on earth is that command behaving like that, which one am I calling?).
Most systems, most Bourne-like shells: functions

The most obvious case is for functions:

$ type ls
ls is a function
ls ()
{
[ -t 1 ] && set -- -F "$@";
command ls "$@"
}
$ which ls
/bin/ls

The reason being that which only reports about executables, and sometimes about aliases (though not always the ones of your shell), not functions.

The GNU which man page has a broken (as they forgot to quote $@) example on how to use it to report functions as well, but just like for aliases, because it doesn't implement a shell syntax parser, it's easily fooled:

$ which() { (alias; declare -f) | /usr/bin/which --tty-only --read-alias --read-functions --show-tilde --show-dot "$@";}
$ f() { echo $'\n}\ng ()\n{ echo bar;\n}\n' >> ~/foo; }
$ type f
f is a function
f ()
{
echo '
}
g ()
{ echo bar;
}
' >> ~/foo
}
$ type g
bash: type: g: not found
$ which f
f ()
{
echo '
}
$ which g
g ()
{ echo bar;
}

Most systems, most Bourne-like shells: builtins

Another obvious case is builtins or keywords, as which being an external command has no way to know which builtins your shell have (and some shells like zsh, bash or ksh can load builtins dynamically):

$ type echo . time
echo is a shell builtin
. is a shell builtin
time is a shell keyword
$ which echo . time
/bin/echo
which: no . in (/bin:/usr/bin)
/usr/bin/time

(that doesn't apply to zsh where which is builtin)
Solaris 10, AIX 7.1, HP/UX 11i, Tru64 5.1 and many others:

$ csh
% which ls
ls:   aliased to ls -F
% unalias ls
% which ls
ls:   aliased to ls -F
% ksh
$ which ls
ls:   aliased to ls -F
$ type ls
ls is a tracked alias for /usr/bin/ls

That is because on most commercial Unices, which (like in the original implementation on 3BSD) is a csh script that reads ~/.cshrc. The aliases it will report are the ones defined there regardless of the aliases you currently have defined and regardless of the shell you're actually using.

In HP/UX or Tru64:

% echo 'setenv PATH /bin:/usr/bin' >> ~/.cshrc
% setenv PATH ~/bin:/bin:/usr/bin
% ln -s /bin/ls ~/bin/
% which ls
/bin/ls

(the Solaris and AIX versions have fixed that issue by saving $path before reading the ~/.cshrc and restoring it before looking up the command(s))

$ type 'a b'
a b is /home/stephane/bin/a b
$ which 'a b'
no a in /usr/sbin /usr/bin
no b in /usr/sbin /usr/bin

Or:

$ d="$HOME/my bin"
$ mkdir "$d"; PATH=$PATH:$d
$ ln -s /bin/ls "$d/myls"
$ type myls
myls is /home/stephane/my bin/myls
$ which myls
no myls in /usr/sbin /usr/bin /home/stephane/my bin

(of course, being a csh script you can't expect it to work with arguments containing spaces...)
CentOS 6.4, bash

$ type which
which is aliased to `alias | /usr/bin/which --tty-only --read-alias --show-dot --show-tilde'
$ alias foo=': "|test|"'
$ which foo
alias foo=': "|test|"'
        /usr/bin/test
$ alias $'foo=\nalias bar='
$ unalias bar
-bash: unalias: bar: not found
$ which bar
alias bar='

On that system, there's an alias defined system-wide that wraps the GNU which command.

The bogus output is because which reads the output of bash's alias but doesn't know how to parse it properly and uses heuristics (one alias per line, looks for the first found command after a |, ;, &...)

The worst thing on CentOS is that zsh has a perfectly fine which builtin command but CentOS managed to break it by replacing it with a non-working alias to GNU which.
Debian 7.0, ksh93:

(though applies to most systems with many shells)

$ unset PATH
$ which which
/usr/local/bin/which
$ type which
which is a tracked alias for /bin/which

On Debian, /bin/which is a /bin/sh script. In my case, sh being dash but it's the same when it's bash.

An unset PATH is not to disable PATH lookup, but means using the system's default PATH which unfortunately on Debian, nobody agrees on (dash and bash have /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin, zsh has /bin:/usr/bin:/usr/ucb:/usr/local/bin, ksh93 has /bin:/usr/bin, mksh has /usr/bin:/bin, execvp() (like in env) has :/bin:/usr/bin (yes, looks in the current directory first!)).

Which is why which gets it wrong above since it's using dash's default PATH which is different from ksh93's

It's not better with GNU which which reports:

which: no which in ((null))

(interestingly, there is indeed a /usr/local/bin/which on my system which is actually an akanga script that came with akanga (an rc shell derivative where the default PATH is /usr/ucb:/usr/bin:/bin:.))
bash, any system:

The one Chris is referring to in his answer:

$ PATH=$HOME/bin:/bin
$ ls /dev/null
/dev/null
$ cp /bin/ls bin
$ type ls
ls is hashed (/bin/ls)
$ command -v ls
/bin/ls
$ which ls
/home/chazelas/bin/ls

Also after calling hash manually:

$ type -a which
which is /usr/local/bin/which
which is /usr/bin/which
which is /bin/which
$ hash -p /bin/which which
$ which which
/usr/local/bin/which
$ type which
which is hashed (/bin/which)

Now a case where which and sometimes type fail:

$ mkdir a b
$ echo '#!/bin/echo' > a/foo
$ echo '#!/' > b/foo
$ chmod +x a/foo b/foo
$ PATH=b:a:$PATH
$ which foo
b/foo
$ type foo
foo is b/foo

Now, with some shells:

$ foo
bash: ./b/foo: /: bad interpreter: Permission denied

With others:

$ foo
a/foo

Neither which nor type can know in advance that b/foo cannot be executed. Some shells like bash, ksh or yash, when invoking foo will indeed try to run b/foo and report an error, while others (like zsh, ash, csh, Bourne, tcsh) will run a/foo upon the failure of the execve() system call on b/foo.

0 comments:

Post a Comment

Don't Forget to comment