“Which which is which? Visualizing $PATH”

February 11th 2025

It’s always terribly annoying when an executable makes me decide where it should be installed. Normally you just sic the package manager on the tool you need and it figures out where to place it in your system, but occasionally the developer gives you a plain ol’ executable file and tells you to figure it out.

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. 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 something involving a modification to the path.I have now typed the word “path” so much it has stopped feeling like a real word. Yo dawg, I heard you like $PATH, so I put a path in your path to help path better in $PATH. Bleh.

For years in my early UNIX 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. Tsk tsk.

I’ll be the first to admit that this is one of the pieces of UNIX I’ve always touched only as little as possible, after all the least interesting part of a new tool is where it’s installed. But maybe I can challenge that assumption today.

which

Of course, the program that does all the heavy lifting here in the path lookup is which. Turns out, it’s actually just a bash script:

λ  which which ## tehe
/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. I’ve always assumed that if an element