“Which which is which?”
February 11th 2025
For the past while I’ve been wanting to write a visualization tool for the system $PATH variable, mostly targeted at new users who are learning UNIX for the first time. Today is not the day I write it, but I wanted to take a few mintues and look at the internals of which more closely. The shell looking up executables in $PATH is a fairly straightforward concept when it comes down to it, but it feels intimidating as a new user and I remember for years I internally groaned whenever I needed to modify `$PATH`.
In my early shell days I would fearfully clutter up my .bashrc with statements like:
export PATH="/usr/local/sbin:$PATH"
export PATH="/sbin:$PATH"
export PATH="$PATH:/usr/local/go/bin"
export PATH="$PATH:/home/kingsfoil/go/bin"Prepending and appending to the variable with whatever the installation docs suggested. Such blind trust.
Usually the least interesting part of a new tool is where it’s installed. But maybe I can challenge that assumption today.
which
The program doing all the heavy lifting here in the path lookup is which. Turns out, it’s just a bash script:
λ which which ## hah
/usr/bin/which
λ cat `which which`
#! /bin/sh
set -ef
if test -n "$KSH_VERSION"; then
puts() {
print -r -- "$*"
}
else
puts() {
printf '%s\n' "$*"
}
fi
ALLMATCHES=0
while getopts a whichopts
do
case "$whichopts" in
a) ALLMATCHES=1 ;;
?) puts "Usage: $0 [-a] args"; exit 2 ;;
esac
done
shift $(($OPTIND - 1))
if [ "$#" -eq 0 ]; then
ALLRET=1
else
ALLRET=0
fi
case $PATH in
(*[!:]:) PATH="$PATH:" ;;
esac
for PROGRAM in "$@"; do
RET=1
IFS_SAVE="$IFS"
IFS=:
case $PROGRAM in
*/*)
if [ -f "$PROGRAM" ] && [ -x "$PROGRAM" ]; then
puts "$PROGRAM"
RET=0
fi
;;
*)
for ELEMENT in $PATH; do
if [ -z "$ELEMENT" ]; then
ELEMENT=.
fi
if [ -f "$ELEMENT/$PROGRAM" ] && [ -x "$ELEMENT/$PROGRAM" ]; then
puts "$ELEMENT/$PROGRAM"
RET=0
[ "$ALLMATCHES" -eq 1 ] || break
fi
done
;;
esac
IFS="$IFS_SAVE"
if [ "$RET" -ne 0 ]; then
ALLRET=1
fi
doneAs a result the man page isn’t too complex either:
WHICH(1) General Commands Manual WHICH(1)
NAME
which - locate a command
SYNOPSIS
which [-a] filename ...
DESCRIPTION
which returns the pathnames of the files (or links) which would be executed in the current environment, had
its arguments been given as commands in a strictly POSIX-conformant shell. It does this by searching the PATH
for executable files matching the names of the arguments. It does not canonicalize path names.
OPTIONS
-a print all matching pathnames of each argument
EXIT STATUS
0 if all specified commands are found and executable
1 if one or more specified commands is nonexistent or not executable
2 if an invalid option is specified
Debian 29 Jun 2016 WHICH(1)
Pretty straightforward. With the addition of a single echo to STDERR it’s possible to modify it system wide to show the search path:
...
for ELEMENT in $PATH; do
echo "$ELEMENT" 1>&2
....λ which which
/home/kingsfoil/.local/bin
/run/user/1000/fnm_multishells/618164_1700005610556/bin
/sbin
/usr/local/sbin
/home/kingsfoil/.local/bin
/home/kingsfoil/.local/bin
/home/kingsfoil/.juliaup/bin
/home/kingsfoil/.cargo/bin
/home/kingsfoil/.asdf/shims
/home/kingsfoil/.asdf/bin
/run/user/1000/fnm_multishells/618164_1700005610556/bin
/sbin
/usr/local/sbin
/usr/local/sbin
/usr/local/bin
/usr/sbin
/usr/bin
/usr/bin/which
λ which which 2>/dev/null
/usr/bin/whichI suppose a basic visualization wasn’t that difficult after all.