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 -v
POSIX準拠。存在時の出力形式が決まっている。全項目の有無の判別可能。ただし,aliasは定義の有無しか判定できない。aliasで定義されたコマンドが実際に存在するかは判定不能。
hash
POSIX準拠。全項目の有無の判別可能。aliasの実際のコマンドも展開して判定できる。zshの組み込みコマンドの場合,組み込みコマンドと関数の判定不能。
command -Vtype
ともにPOSIXで定義されており,挙動もほぼ同じ。コマンドの存在および種類の識別ができるが,出力形式は未定義。
whence
command -vと同等。ただし,POSIX未定義なので,kshとzshでしか使えない。
which
外部コマンドしか判定できない。zshの組み込みwhichに限り,whenceと同等。

command,hash,typeはPOSIXで定義されている。この内,typeとcommand -Vは出力形式が未定義。command -vのみコマンド存在時の出力形式が決まっている。外部コマンドの絶対パスの取得に適している。

hashコマンドはコマンドが存在する場合に,何も表示しない。代わりに,aliasの定義元のコマンドまでチェックできる。ただし,zshの組み込みコマンドのhashは挙動が異なり,組み込みコマンドや関数の存在を判定できない。

zsh組み込みのhashが組み込みコマンドや関数を判別できないのは,おそらくPOSIX規格の以下の一文のためだろう。

Utilities provided as built-ins to the shell shall not be reported by hash.
hash – Shell & Utilities

シェルでは,最近実行されたコマンドをハッシュテーブルに記憶しており,これを利用することでコマンド検索速度をあげているらしい。hashコマンドは,指定したコマンドをハッシュテーブルに追加したり,現在のハッシュテーブルを表示したりできる。

hash
hits command
   1 /bin/grep
   8 /usr/bin/xset
   5 /usr/bin/vim
   1 /usr/bin/xkbcomp

上記一文は,おそらく引数を指定しないハッシュテーブル一覧に組み込みコマンドをを表示させないことを意図していたのだと思うが,zshではこれを誤って解釈して実装したため,hashコマンドで組み込みコマンドや関数が判定できなくなったのだと思われる。

コマンドの存在判定の書き方

実際にこれらのコマンドを使って,if文などでコマンドの有無を判定することを考える。大きく2種類の判定方法がある。

  1. 実行結果の終了ステータスで判定
  2. 出力文字で判定

1.の方法は,例えば以下のように出力結果を捨てて行う。

command -v which >/dev/null && echo "OK"

この方法では,確実に標準出力と標準エラー出力を捨てる必要があるので,>/dev/null 2>&1などといったリダイレクトを記述する必要がある。

2.の方法は,command -vwhencewhichコマンドは対象コマンドが存在しなければ,何も表示しないことを利用している。testコマンドでは,出力文字があればtrue,なければfalseとなるので,コマンド代入$()を使って,コマンドが存在しているかどうかを出力される文字列を使って判定する。

[ "$(command -v which)" ] && echo "OK"

testコマンドを使う場合,入力文字数が少ないという利点があるが,実行コマンドが増えるためパフォーマンスが悪くなる。

この2通りの方法で判定方法を以下の表にまとめた。

2種類のコマンドの存在判定方法
コマンド終了ステータスでの判定方法標準出力の有無で判定
command -v
command -v hoge >/dev/null
[ "$(command -v hoge)" ]
command -V
command -V hoge >/dev/null 2>&1

hash
hash hoge 2>/dev/null
hash hoge 2>&- #(zsh:エラー)
[ ! "$(hash hoge 2>&1)" ] 
type
type hoge >/dev/null 2>&1

whence
whence hoge >/dev/null
[ "$(whence hoge)" ]
which
which hoge >/dev/null
[ "$(which hoge)" ]

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がコマンド実行前にパースしてエラーを出しているようだ。

Re: precmd: write error: interrupted

上記で書かれているようにexec 2>&-を先に掛けば回避できる。しかし,これはみにくく,記述の短さの利点がなくなってしまう。

速度比較

これだけコマンドや判定方法があれば,どれを使えばいいのかわからなくなってしまう。そこで,実行速度を計測して判断材料とする。実際に以下のコードで1万回実行して実行速度を計測してみる。

コマンドの存在判定の実行速度比較コード
#!/bin/sh
## \file time_if_exe.sh

set -eu
umask 0022
export LC_ALL='C' PATH="$(command -p getconf PATH):$PATH"

export N=10000
export NULL="/dev/null"

## test vs. >/dev/null
time -p sh -c 'for i in $(yes|head -$N); do command -v which >$NULL;   done'
time -p sh -c 'for i in $(yes|head -$N); do [ "$(command -v which)" ]; done'
echo ""

## Vs. all commmand
time -p  sh -c 'for i in $(yes|head -$N); do command -v which  >$NULL; done'
time -p  sh -c 'for i in $(yes|head -$N); do command -V which  >$NULL; done'
time -p  sh -c 'for i in $(yes|head -$N); do hash       which 2>$NULL; done'
time -p  sh -c 'for i in $(yes|head -$N); do hash       which 2>&-;    done'
time -p  sh -c 'for i in $(yes|head -$N); do type       which  >$NULL; done'
time -p zsh -c 'for i in $(yes|head -$N); do whence     which  >$NULL; done'
time -p  sh -c 'for i in $(yes|head -$N); do which      which  >$NULL; done' 

計測結果を以下の表に示す。

コマンドの存在判定の実行速度比較結果


Time [s]
Commandrealusersys
[ “$(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
コマンド存在判定の実行速度比較結果
  • testコマンド[ "$()" ]は,リダイレクトの約40倍時間がかかる。
  • whichコマンドは外部コマンドのため,速度が最も遅い。
  • 最速はtypeコマンドの0.02 s,ついでcommandコマンドの0.03 s。0.01 sの差であり誤差の範囲内と思われる。

whichコマンドとtestコマンドを使う方法以外は,どれも十分速いので,速度差は無視していい。

commandコマンド

ここまででコマンドの存在の有無の判定コマンドや,その判定の記述方法について説明してきた。結局のところ,実行速度も記述量もそんなに違いがない。ではどれを選ぶべきか?答えはcommand -vだ。

なぜ,コマンドの有無の判定でcommand -vを使うべきかは,POSIXで記載されている-vVオプションが追加された経緯を読めばわかる。

The command -v and -V options were added to satisfy requirements from users that are currently accomplished by three different historical utilities: type in the System V shell, whence in the KornShell, and which in the C shell. Since there is no historical agreement on how and what to accomplish here, the POSIX command utility was enhanced and the historical utilities were left unmodified. The C shell which merely conducts a path search. The KornShell whence is more elaborate-in addition to the categories required by POSIX, it also reports on tracked aliases, exported aliases, and undefined functions.


RATIONALE – command – Shell & Utilities

上記内容を簡単に解説する。commandコマンドの-vと-Vは,もともと以下の3種類のコマンドで達成していたことを実現するために2001年に追加された。

  • System V shellのtypeコマンド
  • KornShellのwhenceコマンド
  • C shellのwhichコマンド

これらのコマンド間では,何をどのように実現するかの合意がなかったので,これらのコマンドを修正せずにcommandが拡張された。

上記3種類のコマンドには,それぞれ以下のような欠点や特徴がある。

歴史的なコマンドの有無の判定コマンドの違い
System V shellのtypeコマンド
元々実行時のコマンド名を表示するためのコマンド。出力形式は未定義。
KornShellのwhence
aliasや未定義関数などPOSIXで要求する以上に詳しい。
C shellのwhich
単純なパス検索。組み込みコマンドなどを検出不可。

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

リダイレクトの記述を省略したり,より意図がわかるように以下のように関数にしてしまうのもよいだろう。

コマンドの有無の判別関数
is_exe_enabled(){
command -v "$@" >/dev/null
}

POSIX原理主義における交換可能性を担保したシェルスクリプトを作るうえで,極めて重要なコマンドの存在の判定方法について解説した。command -vを使ってコマンドの存在を判定し,POSIX原理主義を実践していこう。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です