重要 2022-01-30 追記
この記事で解説していた警告の出力は 2022-01-21 に取り消されました(参照 Revert deprecation of which)。そのため Debian which が GNU which に変わることは(少なくとも近い未来では)ないと思います。しかしながら which
を使うよりは POSIX で規定されている command
と type
を使う方を推奨します。
はじめに
which
コマンドはシステムにインストールされてるとは限りません。実際に最小構成でインストールされてない環境として CentOS があります。一方 command -v
は POSIX 規定されているので POSIX に準拠したどのシェルでも問題なく使えます。シェル上では which
コマンドを使っても良いと思いますが、シェルスクリプトでは command -v
を使うようにしたほうが良いでしょう。
関連記事として which
や command
については「シェルスクリプトの type, command, which 等の違いと注意点」で詳しくまとめています。
Debian which は GNU which に変わります(たぶん)
Debian / Ubuntu 系にインストールされている which
コマンドは GNU 版 whichではありません。Debian 版 whichです。Debian 版 which コマンドは見ての通りシェルスクリプトで実装されていますが、おそらくこれは人知れず GNU 版 which (C 言語実装)に変わります。変更になる理由は Debian 版 which をパッケージの一部として提供していた debianutils が which
コマンドを非推奨としたからです。
もしかしたら将来の Debian では which
コマンドを使った時に以下のようなエラーが出力されるようになるかもしれません。
which: this version of `which' is deprecated; use `command -v' in scripts instead.
which: this version of 'which' is deprecated and should not be used.
パッケージ: debianutils (4.11.2) [必須]
Debian に特化した各種ユーティリティ
このパッケージは、主に Debian パッケージのインストールスクリプトによって 使われる小さなユーティリティを多数提供します。あなたはこれらのスクリプト を直接使うこともできます。このパッケージに含まれる具体的なユーティリティ: add-shell installkernel ischroot remove-shell run-parts savelog tempfile which
このパッケージに含まれている which
コマンドは直接使えると書いてはいるものの、本来は Debian パッケージのインストールスクリプトに使うために提供しているのだと思われます。このパッケージに which
コマンドが含まれている理由はインストールスクリプトで必要だったからに他ありませんが、最初から command -v
を使っていれば which
コマンドを含める必要はなかったはずです。
それができなかったのは以前の POSIX (Issue 6 以前)では type
コマンドまたは command
コマンドは XSI 拡張オプションであったためシェルに実装されてない可能性があったからです。しかし POSIX Issue 7 以降ではオプションではなくなりました。POSIX に準拠したシェルには必ず command -v
が実装されているという前提でよくなったため which
コマンドはもはや不要になったと判断し非推奨にしたというのが話の発端です。今後各パッケージのインストールスクリプトでは which
コマンドを使わないように書き換え作業が行われるでしょう。
しかし書き換え作業を要求されるのは Debian パッケージのインストールスクリプトだけであるべきです。その他多くの which
コマンドを使っているユーザースクリプトを壊すわけにはいきません。そこで代替パッケージを作るという方針で作業が進んでいるようです。その代替パッケージの有力候補が今の所 GNU 版 which なわけです。(注意 よりシンプルな BSD 版 which の方が良いではないかという話も出ているので最終的にはどうなるのかわかりません。)
GNU 版 which に変わったとしても影響は少ないと思いますが必須パッケージではなくオプションパッケージとなるようなので、将来(2 年後ぐらい?) Debian をアップデートするときには which
コマンドに依存しているシェルスクリプトは動かなくなるかもしれないので注意が必要です。もっともパッケージを追加するだけで動くようになるとは思います。
command -v
を使ったコードへの修正方法
今はほぼすべてのシェルで command -v
が実装されているので、わざわざシェルスクリプトで which
コマンドを使う理由はありません。将来のために which
をコマンド依存をなくしたい場合は command -v
を使うように変更するだけです。ほとんどの場合は単純に置き換えるだけですむでしょう。
補足
-
command -v
は何も見つからなかった時に標準エラー出力に何も出力しません。 - Debian 版
which
は何も見つからなかった時に標準エラー出力に何も出力しません。 - GNU 版
which
は何も見つからなかった時に標準エラー出力にエラーを出力します。
# コマンドのパスを標準出力に出力する場合
which cmd
↓
command -v cmd
# コマンドのパスを変数に入れる場合
cmdpath=$(which cmd)
↓
cmdpath=$(command -v cmd)
# コマンドの存在チェック(パスは不要)
if which cmd >/dev/null; then
↓
if command -v cmd >/dev/null; then
または
if type cmd >/dev/null 2>&1; then
ただし単純に置き換えられない場合があるので注意(下記参照)
which
と command -v
の違い
which
コマンドを command -v
に置き換えればたいていうまくいくと思いますが、この 2 つには違いがあるので置き換える際には少し注意が必要です。
which
コマンドは外部コマンドを PATH
から検索します。それに対して command -v
はシェルビルトインコマンド、シェル関数、エイリアスからも探します。それらが見つかった時は外部コマンド版よりも優先されます。外部コマンド版が見つかった場合はフルパスを返しますが、シェルビルトインコマンドとシェル関数が見つかった時は名前だけ、エイリアスが見つかった場合はエイリアスの定義を返します。
なにかひらめきました?ちょっと待って下さい、先に echo
の場合はどうなるのかの話をしましょう。echo
はシェルビルトインコマンドと外部コマンド版の両方があります。which echo
であれば外部コマンドのパス(/bin/echo
)を返しますが **command -v
は echo
の外部コマンド版のパスを取得できません。**つまり外部コマンドと同じ名前のシェルビルトイン、シェル関数、エイリアスが定義されている場合に command -v
では外部コマンドのパスを取得することができないのです。
ユースケースの多くはコマンドが使えるかどうかを調べるだけだと思うので which
コマンドを command -v
(コマンドの存在チェックだけなら type
でもよい)に置き換えればたいてい大丈夫だと思いますが、本当に外部コマンドのパスを知りたい場合には command -v
は使えません。つまり**command -v
を使って完全な互換性を持った which
コマンドを実装することはできません。**そこでこの記事の後半で紹介するシェルスクリプト版 which
では PATH
環境変数から検索することで確実に外部コマンドのパスを見つけています。
その他の違いとしては which
の(ほとんどの)実装には PATH
環境変数のすべてのパスからコマンドを探す -a
オプションがあります。個人的にこれが便利だと思っていて、シェルスクリプトでは command -v
を使いますがターミナルではこれからも which
コマンドを使うと思います。
zsh 版 which
に注意
which
コマンドは外部コマンドのパスを取得するものというのが一般的な認識だと思いますが、zsh では which
コマンドがシェルビルトインで、command -v
と同様にシェルビルトインコマンドも取得できてしまいます。
$ which echo
echo: shell built-in command
$ which -a echo
echo: shell built-in command
/bin/echo
これもシェルスクリプトでは which
コマンドを使うべきではない理由の一つです。シェルスクリプトを dash、bash、zsh のどのシェルでも動くようにしようと思う人はあまりいないと思いますが、例えば bash 用シェルスクリプトを zsh 用に置き換えたときなど which
の挙動の違いにハマる可能性があります。
シェルスクリプト版 which コマンド
Debian 版 which コード解説
Debian のシェルスクリプト版 which
のコードは 67 行と短いので、分かりづらい箇所を少し解説をしたいと思います。(コピペして気づきましたが、ひでぇ、タブ文字とスペースが混在してやがる。インデントもおかしいし 1 文字スペースのインデントってなんや・・・。見づらいのでそこだけ直します)。解説するもう一つの理由はライセンスがパブリックドメインだからです。
which is in the public domain.
将来どうしても Debian 版 which が必要になった時に使用実績のある which
コマンドをそのまま or 独自の修正を加えて使うことが可能なので、自分でわざわざ互換コマンドを再実装する必要はありません。その際にはこの解説が参考になれば幸いです。
解説するのは最新の master ではなく -s
オプションが実装されたときのバージョンです。-s
オプションは BSD 版 which が実装しているオプションでコマンドが見つかったかどうかを終了ステータスで返すだけで画面には何も出力しないオプションです。BSD 版との互換性向上のために追加されましたが、debianutils から which
コマンドを削除すべきという方針となったため revert されて debianutils から削除されました。せっかく実装されたのに残念ですね。ちなみに現時点では GNU which に -s
オプションは実装されていません。
set -ef
set -f
はパス名展開を防ぐための処置です。後のコードで for ELEMENT in $PATH; do
のように $PATH
をダブルクォートで括らずに使用しています。これは :
区切りで複数のパスが詰め込まれている PATH
環境変数を個々のパスに単語分割するためです。
単語分割をするためにダブルクォートを外しているわけですが、そうすると単語分割の他いnパス名展開が行われてしまいます。具体的には PATH
環境変数に *
などが含まれるとワイルドカードとして扱われて複数のパスに展開されてしまいます。これを防ぐために set -f
を使用しています。
単語分割やパス名展開については「シェルスクリプトの変数はダブルクォートしなければいけない!という話」を参照してください。
puts
if test -n "$KSH_VERSION"; then
puts() {
[ "$SILENT" -eq 1 ] && return
print -r -- "$*"
}
else
puts() {
[ "$SILENT" -eq 1 ] && return
printf '%s\n' "$*"
}
fi
ksh の場合は print
(シェルビルトインコマンド)を使うようにコードが修正されています。このコードは changelog より 2007 年 6 月に追加されたようです。
debianutils (2.21.1) unstable; urgency=low * which: patch from Thorsten Glaser to use 'print' when $KSH_VERSION is set (thus implying a Korn shell). closes: #340219. -- Clint Adams <schizo@debian.org> Mon, 25 Jun 2007 10:46:09 -0400
当時の Debian 4.0 では ksh は AT&T の ksh ではなく pdksh でした。その pdksh を /bin/sh
として使っている場合の対応のようです。pdksh では printf
はシェルビルトインコマンドではありません。ちなみに pdksh の後継である mksh は今も printf
をシェルビルトインコマンドとして実装していません(mksh も KSH_VERSION
変数にバージョン番号を入れているので mksh 用の対応の可能性もあります)。従って外部コマンド版の printf
が呼び出されていました。これによって少々遅くなるという問題があるのですが、それよりも重要なのは外部コマンドは PATH
環境変数からコマンドを探して実行するという点です。
which
コマンドは PATH
からコマンドを探すものであるため、PATH
を暗黙的にデータとして使用していると言えます。つまり PATH
を変更して指定したパスの中から検索するという使い方が考えられます。printf
は /usr/bin/printf
にコマンドが存在しますが、もし /usr/bin/
以外のパスから特定のコマンドを探すために which
コマンドを使用するとしたら PATH
から /usr/bin
を(一時的に)取り除いて which
コマンドを実行することになるでしょう。そうすると /usr/bin/printf
が見つからずにエラーになってしまいます。ゆえに**which
コマンドの実装は外部コマンドに依存するべきではありません。**
これが本当の理由かどうかはわからないですが、外部コマンド依存させない、もしくはパフォーマンス上の理由で ksh 関連のシェルでは print
コマンドを使用するようにしたと考えられます。当時の情報を探したらなにか出てくるかもしれませんが、そこまでする気力はわきません。
PATH="$PATH:"
case $PATH in
(*[!:]:) PATH="$PATH:" ;;
esac
知っての通り PATH
環境変数は :
区切りで複数のパスが設定されています。この後のコードで IFS=:
をしてPATH 環境変数を単語分割をすることにより、それぞれのパスを位置パラメーターに設定しています。
上記のコードは PATH 環境変数が :
で終わってる場合に :
を追加する処理です。例えば foo:bar:baz:
であればこれを foo:bar:baz::
にしています。なぜこのような処理が必要かと言うと単語分割のとある仕様に関係しています。
foo:bar:baz:
を :
で単語分割した時、set -- "foo" "bar" "baz" ""
となるであろうと期待すると思います。しかしそうはなりません。最後の文字が単語分割に使用する文字そのものである場合、その後に項目はなかったものとされてしまいます。つまり set -- "foo" "bar" "baz"
としたのと同じ状態になるのです。この問題を防ぐために foo:bar:baz::
のように二重の :
で終わるようにしています。(詳しくは「シェルスクリプトの単語分割 (IFS) は罠だらけ」参照)
which 簡易版の実装
より短いコードが欲しい人のための機能を限定した簡易版 which
シェル関数を実装しました。これをシェルスクリプトの冒頭に入れておけば which
コマンドがインストールされてない環境でも問題なく動作するようになるはずです。
which
コマンドと同じく PATH
環境変数から探索するバージョンと参考として command -v
を使った実装(2 タイプ)の合計 3 タイプです。ライセンスは特に主張しない(CC0 扱い)ので自由に使用して構いませんが、どれも十分にテストしてないので注意してください。
PATH
探索版
-a
等のオプションや複数ファイル検索などには対応していませんが、ちゃんと PATH
環境変数から外部コマンドのパスを探索しています。POSIX 準拠かつ変数を使わずに実装しているので変数の衝突の心配なく使うことができます。
# PATH から探索する
which() {
set -- "$1" "${PATH:-}:"
while [ "$2" ]; do
set -- "$1" "${2#*:}" "${2%%:*}"
set -- "$1" "$2" "$3${3:+/}$1"
if [ -f "$3" ] && [ -x "$3" ]; then
printf '%s\n' "$3"
return 0
fi
done
return 1
}
command -v
版
次に紹介する command -v
版は上の方で説明したとおり完全な互換性は無いので特に注意してください。**外部コマンドと同名のシェルビルトインコマンド、シェル関数、エイリアスがある場合は、外部コマンドのパスは取得できません。**コマンド名が確実に外部コマンドという前提であれば使えないことはないなという考えのものです。また command -v
の出力は特定のシェル依存で想定外のフォーマットで返すものがあるのではないかと思っていますが確認していません。
# command -v 版 外部コマンドの場合に見つけたと判断する
which() {
set -- "$(command -v "$1")"
[ "${1#/}" = "$1" ] && return 1 || printf '%s\n' "$1"
}
# command -v 版 alias 以外の場合に見つけたと判断する
which() {
set -- "$(command -v "$1")"
case $1 in (alias\ * | '') return 1; esac
printf '%s\n' "$1"
}
さいごに
ネタとして面白かったので記事にまとめましたが、将来 which
コマンドが Debian で使えなくなるから早く command -v
に移行しなきゃと急かす意図はありません。おそらく which
コマンドのパッケージをインストールするだけで解決しますし、シェルスクリプト版の which
コマンドの実装もあるので後からどうとでもなります。ただ方針としてこれからは command -v
を使うようにした方がいいという話です。