How to check if commands enabled in POSIX shell script
#posixismadvent この記事はPOSIX原理主義Advent Calendarの17日目だ。
POSIX原理主義を実践していくうえで,POSIX規格外のコマンドを使う場合は,交換可能性を担保しなければならない。その際にコマンドが利用可能かどうかの判定が必要となる。そこで,POSIXに準拠したコマンドの利用可能判定方法を解説する。
結論としては,用途に応じて以下の2コマンドを利用すればよい。
command -v hoge >/dev/null && echo "OK"
hash hoge 2>/dev/null && echo "alias OK"
コマンドが存在するかどうかを判定するコマンドとしてwhich
コマンドが存在する。しかし,このwhichコマンドはPOSIXで未定義なので,whichコマンド自体が常に使えると保証できない。
実はコマンドの有無を判定できるコマンドはいくつか存在する。これらを説明していく。
コマンドの存在の判定コマンド
コマンドの有無の判定については以下のコマンドが利用可能である。
- command -v
- command -V
- hash
- type
- kshとzshの組み込みコマンドのwhence
- which
これらのコマンドは判定できるコマンドの種類や,出力形式がまちまちである。これらの対応を以下の表にまとめた。
コマンド | 出力形式 | 不在時 | 存在時 | ||||
---|---|---|---|---|---|---|---|
外部コマンド | 組み込みコマンド | 関数 | 予約後 | alias | |||
command -v |
定義済み。不在時は無表示。 | 無表示 | 絶対パス | 名前 | 名前 | 名前 | 定義 |
command -V |
unspecified。それぞれの種別がわかるように出力。不在時はエラー表示。 | エラー | 絶対パス | 名前 | 名前 | 名前 | 定義 |
hash |
aliasは定義もチェックする。存在時は無表示。 | エラー | 無表示 | 無表示 (zsh:検出不可) |
無表示 (zsh:検出不可) |
エラー | 無表示 |
type |
unspecified。それぞれの種別がわかるように出力。不在時はエラー表示。 | エラー | 絶対パス | 名前 | 名前 | 名前 | 定義 |
whence |
POSIX未定義。kshとzsh組み込みコマンド。不在時は無表示。 | 無表示 | 絶対パス | 名前 | 名前 | 名前 | 定義 |
which |
POSIX未定義。aliasは展開してチェックする。不在時は無表示。 | 無表示 (zsh:表示) |
絶対パス | 無表示 (検出不可) (zsh:名前) |
無表示 (検出不可) (zsh:定義) |
無表示 (検出不可) (zsh:名前) |
絶対パス (zsh:定義) |
この結果をまとめる。
command,hash,typeはPOSIXで定義されている。この内,typeとcommand -Vは出力形式が未定義。command -vのみコマンド存在時の出力形式が決まっている。外部コマンドの絶対パスの取得に適している。
hashコマンドはコマンドが存在する場合に,何も表示しない。代わりに,aliasの定義元のコマンドまでチェックできる。ただし,zshの組み込みコマンドのhashは挙動が異なり,組み込みコマンドや関数の存在を判定できない。
zsh組み込みのhashが組み込みコマンドや関数を判別できないのは,おそらくPOSIX規格の以下の一文のためだろう。
シェルでは,最近実行されたコマンドをハッシュテーブルに記憶しており,これを利用することでコマンド検索速度をあげているらしい。hashコマンドは,指定したコマンドをハッシュテーブルに追加したり,現在のハッシュテーブルを表示したりできる。
hash
hits command 1 /bin/grep 8 /usr/bin/xset 5 /usr/bin/vim 1 /usr/bin/xkbcomp
上記一文は,おそらく引数を指定しないハッシュテーブル一覧に組み込みコマンドをを表示させないことを意図していたのだと思うが,zshではこれを誤って解釈して実装したため,hashコマンドで組み込みコマンドや関数が判定できなくなったのだと思われる。
コマンドの存在判定の書き方
実際にこれらのコマンドを使って,if文などでコマンドの有無を判定することを考える。大きく2種類の判定方法がある。
- 実行結果の終了ステータスで判定
- 出力文字で判定
1.の方法は,例えば以下のように出力結果を捨てて行う。
command -v which >/dev/null && echo "OK"
この方法では,確実に標準出力と標準エラー出力を捨てる必要があるので,>/dev/null 2>&1
などといったリダイレクトを記述する必要がある。
2.の方法は,command -v
とwhence
,which
コマンドは対象コマンドが存在しなければ,何も表示しないことを利用している。testコマンドでは,出力文字があればtrue,なければfalseとなるので,コマンド代入$()
を使って,コマンドが存在しているかどうかを出力される文字列を使って判定する。
[ "$(command -v which)" ] && echo "OK"
testコマンドを使う場合,入力文字数が少ないという利点があるが,実行コマンドが増えるためパフォーマンスが悪くなる。
この2通りの方法で判定方法を以下の表にまとめた。
コマンド | 終了ステータスでの判定方法 | 標準出力の有無で判定 |
---|---|---|
command -v |
|
|
command -V |
|
|
hash |
|
|
type |
|
|
whence |
|
|
which |
|
|
POSIXの範囲内で実現する場合,hash
コマンドを使う方法が最短記法となる。しかし,hash
コマンドはzshで実装が異なっており,汎用性が若干欠けてしまう。hashコマンドで使っている2>&-
というリダイレクトは見慣れないと思うので簡単に解説する。これは標準エラー出力を閉じている。これにより,エラーメッセージの表示を強制的に禁止してしている。
同様に>&-
という記法で標準出力を閉じることができる。これにより,>/dev/null
よりも手短に標準出力を捨てられてよいと思うかもしれない。しかし,標準出力を閉じた場合,シェルによってはエラーが出てしまうので残寝ながら使えない。
sh
echo "ABC" >&-
sh: echo: I/O error
しかし,標準エラー出力を閉じる>&-
であれば,そのエラーすら禁止するので問題なく使える。
hoge 2>&- && echo OK || echo NG
NG
しかし,これをzshの組み込みhashコマンドでやる場合に限り,以下のようなエラーが出る。
hash lv 2>&-
zsh: write error
zshがコマンド実行前にパースしてエラーを出しているようだ。
上記で書かれているようにexec 2>&-
を先に掛けば回避できる。しかし,これはみにくく,記述の短さの利点がなくなってしまう。
速度比較
これだけコマンドや判定方法があれば,どれを使えばいいのかわからなくなってしまう。そこで,実行速度を計測して判断材料とする。実際に以下のコードで1万回実行して実行速度を計測してみる。
計測結果を以下の表に示す。
Time [s] | |||
---|---|---|---|
Command | real | user | sys |
[ “$(command -v )” ] | 1.17 | 0.04 | 0.42 |
command -v | 0.03 | 0.01 | 0.01 |
command -V | 0.03 | 0.02 | 0.00 |
hash 2>/dev/null | 0.05 | 0.01 | 0.03 |
hash 2>&- | 0.04 | 0.01 | 0.02 |
type | 0.02 | 0.01 | 0.00 |
whence | 0.05 | 0.02 | 0.01 |
which | 5.69 | 0.08 | 0.53 |
whichコマンドとtestコマンドを使う方法以外は,どれも十分速いので,速度差は無視していい。
command
コマンド
ここまででコマンドの存在の有無の判定コマンドや,その判定の記述方法について説明してきた。結局のところ,実行速度も記述量もそんなに違いがない。ではどれを選ぶべきか?答えはcommand -v
だ。
なぜ,コマンドの有無の判定でcommand -vを使うべきかは,POSIXで記載されている-vVオプションが追加された経緯を読めばわかる。
上記内容を簡単に解説する。commandコマンドの-vと-Vは,もともと以下の3種類のコマンドで達成していたことを実現するために2001年に追加された。
- System V shellの
type
コマンド - KornShellの
whence
コマンド - C shellの
which
コマンド
これらのコマンド間では,何をどのように実現するかの合意がなかったので,これらのコマンドを修正せずにcommandが拡張された。
上記3種類のコマンドには,それぞれ以下のような欠点や特徴がある。
command -V
はtypeコマンドに相当しており,typeコマンドと同様に出力書式は未定義だが,シェルでどのように解釈されるかユーザーにとって役に立つ情報が多く表示される。-VはSystem VのVを意味しているのだと思われる。
command -v
はKornShellのwhenceコマンドに相当しており,組み込みコマンドや関数まで判別できている。また,出力形式も規格て定義されている。
Conclusion
ここまでで,コマンドの存在の判定方法について説明してきた。結論として,コマンドの存在の判定は,原則command -v
で判定するのがよいだろう。理由は以下となる。
command
コマンドの-vV
オプションがシェル共通で使うことを念頭に作られた。- POSIXで出力書式が唯一規定されている。
- トータルの記述量はtypeコマンドで判定する場合より1文字多いだけ。
- 実行速度も十分速い。
command -v
は唯一出力書式が決まっているので,外部コマンドの絶対パスを取得したい場合はほぼこれを使うしかない。ただし,commandコマンドはaliasの本体を判定できないという欠点もある。このときのために,hashコマンドを使うのはありだろう。ただし,hashコマンドはzsh組み込みの場合に挙動が変わるので,あまり使わないほうがいいように思う。zshで使わないというのならありかもしれない。
command -v hoge >/dev/null && echo "OK"
hash hoge 2>/dev/null && echo "alias OK"
hash hoge 2>&- && echo "OK" # except for zsh
リダイレクトの記述を省略したり,より意図がわかるように以下のように関数にしてしまうのもよいだろう。
POSIX原理主義における交換可能性を担保したシェルスクリプトを作るうえで,極めて重要なコマンドの存在の判定方法について解説した。command -v
を使ってコマンドの存在を判定し,POSIX原理主義を実践していこう。