Bashでコマンドの存在チェックはwhichよりhashの方が良いかも→いやtypeが最強→command -vも


コマンドのパスを知りたいんじゃなく、コマンドの存在をチェックしたいだけならwhichよりhashを使ったほうが良いかもっていう話。→追記: typeが最強っぽい。
追記: command -vも良い。プログラムの存在チェックorパスを探したいだけなら互換性を考えると一番良いかも。

比較してみる

whichよりhashよりtype=command -vが高速→typeまたはcommand -vの勝ち

whichは実ファイルという実体があるプログラムです。hashとtypeはbashの組み込みコマンドです。なので当然ですがプログラムの起動コストがない分hashやtypeの方が速いです。

$ time bash -c 'for((i=0;i<10000;i++));do which perl; done >/dev/null'
real    0m7.739s
user    0m2.928s
sys     0m5.923s

$ time bash -c 'for((i=0;i<10000;i++));do hash perl; done >/dev/null'
real    0m0.633s
user    0m0.240s
sys     0m0.391s

$ time bash -c 'for((i=0;i<10000;i++));do type perl; done >/dev/null'
real    0m0.603s
user    0m0.168s
sys     0m0.434s

$ time bash -c 'for((i=0;i<10000;i++));do command -v perl; done >/dev/null'
real    0m0.602s
user    0m0.172s
sys     0m0.400s

1万回回してますがhashやtypeはwhichの10倍以上速い。そして僅差ですがtypeの方がhashより速いっぽいです、これは何回やっても同じくらい速い。ってことでtypeの勝ち。command -vも計測してみたところ何回やってもほぼtypeと同じ結果でした。

探せるものの違い→typeの勝ち

コマンドとして使えそうなものとしては、プログラム・組み込みコマンド・エイリアス・関数・あとはifなどの予約後があり、whichとhashとtypeでは探せるものに違いがあります。

which hash type
プログラム /usr/bin/perl
組み込みコマンド cd, alias, read, hash, ...
エイリアス alias ll='ls -l'
関数 f(){ echo func; }, nvm
予約後 if, for, ...

とりあえずtype最強のようです。

nvmとかのように関数として定義されるコマンドをチェックしたい場合は基本、whichでは探せません。

補足(whichで関数を探す)

一応whichでも、非常にアクロバティックな書き方ですが関数定義があるかどうかのチェックができなくはないです。

$ f() { echo func; }
$ declare -f | command which --read-functions f
f ()
{
    echo func
}
$ echo $?
0

declare -f で関数定義をダンプします。その関数定義のダンプを標準入力から読み込んだ上で --read-functions オプションをつけると、その読み込んだ関数定義内に引数で渡された関数が存在するかのチェックができます。これはman whichに書いてありますが、さすがにこんなの普通は使わないと思うwww

環境非依存度→hash,typeの勝ち

Bashスクリプトを書こうとしている以上は組み込みコマンドであるhashtypeは常に存在を保証されています。それに対してwhichは外部プログラムであるがゆえにインストールされていないこともあり環境非依存度という点では劣ります。(まぁ、組み込みとかでない限り大抵のディストリビューションでは普通入ってますけどね)
というわけで、hashとtypeの勝ち。

キャッシュの影響による正確さ→whichの勝ち?

hashとtypeは組み込みコマンドだけあって毎回ゼロから起動されるwhichよりも賢いです。
実はbashは、初めてperlとかを実行した際にPATHを前から順にチェックして /usr/bin/perlがあることを発見したらperl=/usr/bin/perlということをメモリ内に学習して、2度目以降perlを実行する際にはPATHのチェックせずにいきなり/usr/bin/perlを実行します。そしてhashtypeはまずこのbash内部のハッシュテーブルをチェックして、エントリが無ければPATH環境変数を元にコマンドを探し始めます。組み込みコマンドな上にメモリキャッシュも活用してるからwhichより早いのは当然ですね。

ただしあらゆるキャッシュにつきもののトラブルですが、この学習が邪魔になることもあります。

# 前準備
mkdir /tmp/test
cp /bin/cat /tmp/test/copycat
export PATH="/tmp/test:$PATH"

# これは当然どっちも問題なし
which copycat # 見つかる→正解
hash copycat  # 見つかる→正解
type copycat  # 見つかる→正解

# さっきまであったコマンドを消してみる
rm -f /tmp/test/copycat
which copycat # 見つからない→正解
hash copycat  # 見つかる→不正解
type copycat  # 見つかる→不正解

# type -aだとハッシュを見なくなる
type -a copycat  # 見つからない→正解

# ハッシュをクリアすると再度PATHをチェックする
hash -r
hash copycat  # 見つからない→正解
type copycat  # 見つからない→正解

というわけでコマンドの存在チェックとしての正確さでは、毎回律儀にチェックする(むしろそれしか出来ない)whichに軍配が上がりそう?それに対してhashとtypeはハッシュクリア対応可能、またtypeは追加オプションでも対応可能ってことで優劣はつけがたいかも。

複数コマンドを纏めて探す→引き分け

which perl python rubyhash perl python rubytype perl python rubycommand -v perl python rubyという感じで、複数コマンドのありかを纏めてチェックするのはwhichでもhashでもtypeでもcommand -vでも出来ます。どちらもこの場合、全てが見つかったら成功で一つでも見つからなかったら失敗という判定。

hashは標準出力が出ないのでちょっとだけ記述が楽→hashの勝ち

そもそも別の用途のコマンドなんで仕様上当然なんですが以下の様な出力の違いがあります。

$ which perl; echo $?
/usr/bin/perl
0
$ which aaa; echo $?
/usr/bin/which: no aaa in (/usr/local/bin:/bin:/usr/bin)
1

$ hash perl; echo $?
0
$ hash aaa; echo $?
-bash: hash: aaa: not found
1

$ type perl; echo $?
0
$ type aaa; echo $?
-bash: type: aaa: not found
1

なので、if文で使うときは邪魔な標準出力やエラー出力を隠すために↓こうなります。

if which python >/dev/null 2>&1; then
  # some code...
fi

if hash python 2>/dev/null; then
  # some code...
fi

if type python >/dev/null 2>&1; then
  # some code...
fi

2>&1 が不要になった分、>/dev/null2>/dev/nullになってますがトータルでhashの方が3文字少ないのでhashの勝ち。

まとめ

こんな感じに、思いついた争点を列挙してみたわけで結果は以下のとおり。

which hash type
速度 ×
探せるもの
環境非依存度
正確さ
タイプ数
複数同時チェック 引き分け 引き分け 引き分け

異論あるかもしれんが、総じてhashの勝ち→異論あった結果type最強っぽいです!