“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
done

As 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/which

I suppose a basic visualization wasn’t that difficult after all.