Most built-in commands commonly used to find files in $PATH
don't always work quite as expected, or are shell-specific.
Jump to the bottom of the article for a function definition that looks for files in $PATH
and is shell-agnostic, or keep reading for a full explanation of why some built-in commands don't work as desired.
The use case
I tend to not leave Docker Desktop running on my MacBook, it tends to eat battery and slow down every other process including web browsing. Because of that, I'm also tired of seeing:
$ docker ps
Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?
I wanted to create a function in my dotfiles to override the docker
command, and that function would ensure Docker Desktop is running before executing the docker
command. But I had an issue with finding the actual location of the docker
executable once it was shadowed by the function. See "Automatically Execute Code Before & After Unix Commands" for how you can intercept commands like this.
Automatically Execute Code Before & After Unix Commands
Jan 19, 2023 · 4 min read
It can be helpful to run some code automatically before or after calling a command, and it is easy to accomplish with shadowing functions.
which
The problem with The problem with which
is it's affected by aliases and functions, and will prefer those over searching in $PATH
.
If an executable has been overridden with an alias, which
will print the alias:
$ which grep
/usr/bin/grep
$ alias grep='grep --color=auto'
$ which grep
grep: aliased to grep --color=auto
If an executable has been overridden with a function, which
will print the function:
$ which sed
/usr/bin/sed
$ sed() { echo "sed?" }
$ which sed
sed () {
echo "sed?"
}
which
has similar behavior for aliases and functions that don't override an executable:
$ which foo
foo not found
$ foo() { echo "bar" }
$ which foo
foo () {
echo "bar"
}
type
and whence
The problem with The problem with type -P
and whence -p
is they're shell-specific, and I'd prefer a shell-agnostic command or function.
type -P
doesn't exist in Zsh:
$ echo $0
/bin/zsh
$ which cat
/bin/cat
$ type -P cat
type: bad option: -P
And whence -p
doesn't exist in Bash:
$ echo $0
/bin/bash
$ which cat
/bin/cat
$ whence -p cat
bash: whence: command not found
The built-in solution
If all you want to do is execute a command, bypassing any aliases or functions that might be shadowing it, then command
is probably what you want.
Here's an example of command
bypassing an alias:
$ echo goodbye
goodbye
$ alias echo="echo hello"
$ echo goodbye
hello goodbye
$ command echo goodbye
goodbye
And an example of command
bypassing a function:
$ echo hello
hello
$ echo() { command echo "before" "$@" "after" }
$ echo hello
before hello after
$ command echo hello
hello
The custom solution
If you need functionality similar to which
to get the path of an executable, we can write a function to search in $PATH
explicitly. Here's a shell-agnostic function dubbed pinpoint
that's easy to add to dotfiles:
pinpoint() {
while read -r DIR; do
if [[ -f "${DIR}/$1" ]]; then
echo "${DIR}/$1"
return 0
fi
done <<< "$(echo "${PATH}" | tr ':' '\n')"
return 1
}
And here's some example usage of the function:
$ pinpoint grep
/usr/bin/grep
$ alias grep='grep --color=auto'
$ pinpoint grep
/usr/bin/grep
$ pinpoint sed
/usr/bin/sed
$ sed() { echo "sed?" }
$ pinpoint sed
/usr/bin/sed
$ pinpoint foo || echo "not in path"
not in path
$ foo() { echo "bar" }
$ pinpoint foo || echo "not in path"
not in path
Again, this custom function is best used for when you need the path of an executable, either for further manipulation or later use. Here is a concrete example to set $EDITOR
with a full executable path to rid yourself of vi/vim:
export EDITOR=$(pinpoint nano)
Happy searching!