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

**重要 2022-01-30 追記**

この記事で解説していた警告の出力は 2022-01-21 に取り消されました（参照 [Revert deprecation of which](https://salsa.debian.org/debian/debianutils/-/commit/3694cf60572a6c231f2e931f4d075950113599d4)）。そのため Debian which が GNU which に変わることは（少なくとも近い未来では）ないと思います。しかしながら `which` を使うよりは POSIX で規定されている `command` と `type` を使う方を推奨します。

## はじめに

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

関連記事として `which` や `command` については「[シェルスクリプトの type, command, which 等の違いと注意点](https://qiita.com/ko1nksm/items/0fbe6b976417cce07d1b)」で詳しくまとめています。

## Debian which は GNU which に変わります（たぶん）

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

もしかしたら将来の Debian では `which` コマンドを使った時に以下のようなエラーが出力されるようになるかもしれません。

- <code>which: this version of \`which' is deprecated; use \`command -v' in scripts instead.</code>
- <code>which: this version of 'which' is deprecated and should not be used.</code>

> パッケージ: 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` コマンドを使っているユーザースクリプトを壊すわけにはいきません。そこで[代替パッケージを作るという方針](https://lists.debian.org/debian-devel/2021/08/msg00204.html)で作業が進んでいるようです。その代替パッケージの有力候補が今の所 GNU 版 which なわけです。（注意 よりシンプルな BSD 版 which の方が良いではないかという話も出ているので最終的にはどうなるのかわかりません。）

GNU 版 which に変わったとしても影響は少ないと思いますが必須パッケージではなくオプションパッケージとなるようなので、将来（2 年後ぐらい？） Debian をアップデートするときには `which` コマンドに依存しているシェルスクリプトは動かなくなるかもしれないので注意が必要です。もっともパッケージを追加するだけで動くようになるとは思います。

## `command -v` を使ったコードへの修正方法

今はほぼすべてのシェルで `command -v` が実装されているので、わざわざシェルスクリプトで `which` コマンドを使う理由はありません。将来のために `which` をコマンド依存をなくしたい場合は `command -v` を使うように変更するだけです。ほとんどの場合は単純に置き換えるだけですむでしょう。

**補足**

- `command -v` は何も見つからなかった時に標準エラー出力に何も出力しません。
- Debian 版 `which` は何も見つからなかった時に標準エラー出力に何も出力しません。
- GNU 版 `which` は何も見つからなかった時に**標準エラー出力にエラーを出力します。**

```sh
# コマンドのパスを標準出力に出力する場合
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` に置き換えればたいていうまくいくと思いますが、この ２ つには違いがあるので置き換える際には少し注意が必要です。

`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` と同様にシェルビルトインコマンドも取得できてしまいます。

```sh
$ 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 文字スペースのインデントってなんや・・・。見づらいのでそこだけ直します）。解説するもう一つの理由は[ライセンス](https://salsa.debian.org/debian/debianutils/-/blob/8eaf897505f5d414d8b1933f13ec61ca7c517128/debian/copyright)がパブリックドメインだからです。

> which is in the public domain.

将来どうしても Debian 版 which が必要になった時に使用実績のある `which` コマンドをそのまま or 独自の修正を加えて使うことが可能なので、自分でわざわざ互換コマンドを再実装する必要はありません。その際にはこの解説が参考になれば幸いです。

解説するのは最新の master ではなく [`-s` オプションが実装されたときのバージョン](https://salsa.debian.org/debian/debianutils/-/blob/09b5efc8e9e0235f51b997714e6acef7d25bc517/which)です。`-s` オプションは BSD 版 which が実装しているオプションでコマンドが見つかったかどうかを終了ステータスで返すだけで画面には何も出力しないオプションです。BSD 版との互換性向上のために追加されましたが、debianutils から `which` コマンドを削除すべきという方針となったため revert されて debianutils から削除されました。せっかく実装されたのに残念ですね。ちなみに現時点では GNU which に `-s` オプションは実装されていません。

#### `set -ef`

`set -f` はパス名展開を防ぐための処置です。後のコードで `for ELEMENT in $PATH; do` のように `$PATH` をダブルクォートで括らずに使用しています。これは `:` 区切りで複数のパスが詰め込まれている `PATH` 環境変数を個々のパスに単語分割するためです。

単語分割をするためにダブルクォートを外しているわけですが、そうすると単語分割の他いｎパス名展開が行われてしまいます。具体的には `PATH` 環境変数に `*` などが含まれるとワイルドカードとして扱われて複数のパスに展開されてしまいます。これを防ぐために `set -f` を使用しています。

単語分割やパス名展開については「[シェルスクリプトの変数はダブルクォートしなければいけない！という話](https://qiita.com/ko1nksm/items/60b67cb24aa4ae634dd5)」を参照してください。

#### `puts`

```sh
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](https://salsa.debian.org/debian/debianutils/-/blob/master/debian/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:"`

```sh
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) は罠だらけ](https://qiita.com/ko1nksm/items/93550ca91dbeda5685f9)」参照）

### which 簡易版の実装

より短いコードが欲しい人のための機能を限定した簡易版 `which` シェル関数を実装しました。これをシェルスクリプトの冒頭に入れておけば `which` コマンドがインストールされてない環境でも問題なく動作するようになるはずです。

`which` コマンドと同じく `PATH` 環境変数から探索するバージョンと参考として `command -v` を使った実装（2 タイプ）の合計 3 タイプです。ライセンスは特に主張しない（[CC0](https://creativecommons.org/publicdomain/zero/1.0/) 扱い）ので自由に使用して構いませんが、どれも十分にテストしてないので注意してください。

#### `PATH` 探索版

`-a` 等のオプションや複数ファイル検索などには対応していませんが、ちゃんと `PATH` 環境変数から外部コマンドのパスを探索しています。POSIX 準拠かつ変数を使わずに実装しているので変数の衝突の心配なく使うことができます。

```sh
# 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` の出力は特定のシェル依存で想定外のフォーマットで返すものがあるのではないかと思っていますが確認していません。

```sh
# command -v 版 外部コマンドの場合に見つけたと判断する
which() {
    set -- "$(command -v "$1")"
    [ "${1#/}" = "$1" ] && return 1 || printf '%s\n' "$1"
}
```

```sh
# 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` を使うようにした方がいいという話です。