LoginSignup
4

More than 1 year has passed since last update.

posted at

updated at

POSIXシェルスクリプトではwhichではなくcommand -vを使うべき理由(+シェルスクリプト版which)

重要 2022-01-30 追記

この記事で解説していた警告の出力は 2022-01-21 に取り消されました(参照 Revert deprecation of which)。そのため Debian which が GNU which に変わることは(少なくとも近い未来では)ないと思います。しかしながら which を使うよりは POSIX で規定されている commandtype を使う方を推奨します。

はじめに

which コマンドはシステムにインストールされてるとは限りません。実際に最小構成でインストールされてない環境として CentOS があります。一方 command -v は POSIX 規定されているので POSIX に準拠したどのシェルでも問題なく使えます。シェル上では which コマンドを使っても良いと思いますが、シェルスクリプトでは command -v を使うようにしたほうが良いでしょう。

関連記事として whichcommand については「シェルスクリプトの type, command, which 等の違いと注意点」で詳しくまとめています。

Debian which は GNU which に変わります(たぶん)

Debian / Ubuntu 系にインストールされている which コマンドは GNU 版 whichではありません。Debian 版 whichです。Debian 版 which コマンドは見ての通りシェルスクリプトで実装されていますが、おそらくこれは人知れず GNU 版 which (C 言語実装)に変わります。変更になる理由は Debian 版 which をパッケージの一部として提供していた debianutilswhich コマンドを非推奨としたからです。

もしかしたら将来の 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; thenif command -v cmd >/dev/null; then
または
if type cmd >/dev/null 2>&1; then

ただし単純に置き換えられない場合があるので注意(下記参照)

whichcommand -v の違い

which コマンドを command -v に置き換えればたいていうまくいくと思いますが、この 2 つには違いがあるので置き換える際には少し注意が必要です。

which コマンドは外部コマンドを PATH から検索します。それに対して command -v はシェルビルトインコマンド、シェル関数、エイリアスからも探します。それらが見つかった時は外部コマンド版よりも優先されます。外部コマンド版が見つかった場合はフルパスを返しますが、シェルビルトインコマンドとシェル関数が見つかった時は名前だけ、エイリアスが見つかった場合はエイリアスの定義を返します。

なにかひらめきました?ちょっと待って下さい、先に echo の場合はどうなるのかの話をしましょう。echo はシェルビルトインコマンドと外部コマンド版の両方があります。which echo であれば外部コマンドのパス(/bin/echo)を返しますが command -vecho の外部コマンド版のパスを取得できません。つまり外部コマンドと同じ名前のシェルビルトイン、シェル関数、エイリアスが定義されている場合に 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 を使うようにした方がいいという話です。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
4