“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
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. I’ve always assumed that if an element