Last updated on December 10, 2016
In both bash and zsh, there are multiple methods to check whether a command exists. In this post, a set of speed tests will be performed on them to find the fastest way in each of the two shells (NOT to compare the two shells). We will test 5 different methods (foobar is the command to test for existence in the list):
type foobar &> /dev/nullhash foobar &> /dev/nullcommand -v foobar &> /dev/nullwhich foobar &> /dev/null(( $+commands[foobar] ))(zsh only)
All the methods listed above will have a return status of zero if the command foobar exists, otherwise non-zero. That is, after replacing testing-command by any of the commands listed above, you can test the existence of the command foobar by executing testing-command && echo exist || echo non-exist.
Throughout this post, ls will be the command that is used for testing existence, which does exist on the system which runs the tests. The test environment is Debian Jessie with bash 4.3.30 and zsh 5.0.7 on Intel Xeon processor E3-1240 v3 (8 MB Cache, 3.4 GHz). The test scripts are also available at the end of the post.
Bash
The testing results for bash is shown in the following table, with each testing command running for 100,000 times:
| Testing Command | Running Time (real, user, sys) |
|---|---|
type ls &>/dev/null |
0m1.476s, 0m0.632s, 0m0.836s |
hash ls &>/dev/null |
0m1.598s, 0m0.740s, 0m0.856s |
command -v ls &>/dev/null |
0m1.441s, 0m0.660s, 0m0.776s |
which ls &>/dev/null |
2m0.418s, 0m3.852s, 0m13.212s |
According to the results, type and command -v are the two fastest ways to test the existence of a command in bash. I have also run the tests with either set +h or set -h, but the results do not have obvious difference.
Zsh
The testing results for bash is shown in the following table, with each testing command running for 2,000,000 times:
| Testing Command | Running Time (real, user, sys) |
|---|---|
type ls &>/dev/null |
0m8.29s, 0m3.48s, 0m4.80s |
hash ls &>/dev/null |
0m5.92s, 0m2.49s, 0m3.42s |
command -v ls &>/dev/null |
0m8.85s, 0m3.61s, 0m5.18s |
which ls &>/dev/null |
0m8.24s, 0m3.18s, 0m5.00s |
(( $+commands[ls] )) |
0m4.01s, 0m3.99s, 0m0.00s |
According to the results, (( $+commands[ls] )) is a clear winner in zsh.
(Note that the which command in zsh is significantly faster than in bash, since the which command is a builtin command in zsh but an external command in bash.)
Test Scripts
The test script for bash:
#!/bin/bash
N=100000
cmd=ls
echo
echo "type:"
time(for i in $(eval echo "{1..$N}"); do
type $cmd &>/dev/null
done)
echo
echo "hash:"
time(for i in $(eval echo "{1..$N}"); do
hash $cmd &>/dev/null
done)
echo
echo "command -v:"
time(for i in $(eval echo "{1..$N}"); do
command -v $cmd &>/dev/null
done)
echo
echo "which:"
time(for i in $(eval echo "{1..$N}"); do
which $cmd &>/dev/null
done)
The test script for zsh:
#!/bin/zsh
N=2000000
cmd=ls
TIMEFMT=$'\nreal\t%E\nuser\t%U\nsys\t%S'
echo
echo "type:"
time (repeat $N {type $cmd &>/dev/null})
echo
echo "hash:"
time (repeat $N {hash $cmd &>/dev/null})
echo
echo "command -v:"
time (repeat $N {command -v $cmd &>/dev/null})
echo
echo "which:"
time (repeat $N {which $cmd &>/dev/null})
echo
echo '$+commands:'
time (repeat $N { (( $+commands[$cmd] )) })
I was going to say that `for i in $(eval echo “{1..$N}”)` is way more contrived than a `for ((i=0;i<N;i++));`, but benchmark on a terrible m3 shows minimal difference. Huh, cool.
Not sure if this was always the case, but `(( $+commands[foobar] ))` definitely doesn’t work within a conditional in ZSH to test whether a command suceeded. You have to convert it to a string and check the numerical output, which is a lot harder than just doing `if [[ echo=command > /dev/null ]]`.
(( $+commands[foobar] )) works for me. Could you describe a scenario in which it fails?
Pingback: How to skip the fortune command when your shell is slow to start | Stephan Sokolow's Blog