Bashシェルでtypeコマンドを使ってコマンドの存在チェックを行う際の注意

2023-03-27Bash

はじめに

昔から、Linux 上で任意のコマンドがどこにあるかを確認するために which コマンドを使っていました。

最近では type コマンドを使ってコマンドがインストールされているかを確認することが増えてきました。
ところがつい最近、注意しないといけないことに気がつきました。

検証環境

$ uname -moi
aarch64 aarch64 GNU/Linux

$ head -n 3 /etc/os-release
PRETTY_NAME="Ubuntu Kinetic Kudu (development branch)"
NAME="Ubuntu"
VERSION_ID="22.10"

$ bash -version | head -n 1
GNU bash, バージョン 5.2.0(1)-release (aarch64-unknown-linux-gnu)

type コマンドとは

type を使うと、引数の文字列に一致するコマンドの情報を表示できます。
ここで「コマンド」と呼んでいるものは以下のようなものになります。

  • 実行可能ファイル ( シェル、Python、Ruby、Perl などのスクリプトやコードからコンパイルされた実行可能なバイナリファイル )
  • 予約語 ( keyword )
  • ビルトインコマンド ( builtin )
  • エイリアス ( alias )
  • 関数 ( function )

以下の例は rm コマンドに関する情報を表示しています。

$ type rm
rm は /usr/bin/rm です

「実行可能ファイル」だけは、 $PATH 環境変数に設定されているパスのリスト ( Linux 環境では : が区切り文字として使われます) から検索されます。

# /usr/local/sbin ディレクトリが一番最初に検索される
$ echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

# `rm` というスクリプトを /usr/local/bin に作成してみる
$ echo 'echo hello' > /usr/local/bin/rm

$ chmod 777 /usr/local/bin/rm

$ type rm
rm は /usr/bin/rm です

type コマンドの終了ステータス

type コマンド実行後の終了ステータスは以下のようになります。

  • 検索対象の「コマンド」が見つかった場合 → 0
  • 検索対象の「コマンド」が見つからなかった場合 → 1
# git コマンドを検索
$ type git
git は /usr/bin/git です

$ echo $?
0

# svn コマンドを検索
$ type svn
bash: type: svn: 見つかりません

$ echo $?
1

これを利用してコマンドが存在していた場合 ( あるいは存在していなかった場合 ) のみ何らかの処理を行う、といったことが可能です。

type コマンドが出力する内容を無視するために、 >/dev/null 2>&1 を末尾に追加しています。

# 見つかった場合、メッセージが表示される
$ type git >/dev/null 2>&1 && echo "見つかったよ"
見つかったよ

# 見つからなかった場合、メッセージが表示されない
$ type svn >/dev/null 2>&1 && echo "見つかったよ"

「実行可能ファイル」だけを検索対象とする場合は -P オプションを使う

今回紹介したかったのはここからです。

例えば git がインストールされているかどうかを確認したいとしましょう。
先の例で注意しないといけない点があります。

svn という予約語やビルトインコマンドは(おそらく)ありませんが、エイリアスや関数が存在していた場合を実験してみます。

# エイリアスを設定
$ alias svn='echo hello'

$ type svn >/dev/null 2>&1 && echo "見つかったよ"
見つかったよ

# お掃除
$ unalias svn

# 関数を設定
$ function svn () { echo world; }

$ type svn >/dev/null 2>&1 && echo "見つかったよ"
見つかったよ

# お掃除
$ unset svn

実行可能ファイルは存在しませんが、エイリアスや関数に反応してコマンドの存在チェックが成功してしまいます。

これを回避するために -P オプションを使用します。
-P オプションを付与したときは、実行可能ファイルが見つかったときはフルパスが出力されますが、見つからなかったときは何も出力されないため 2>&1 の記述は不要です。

# エイリアスを設定
$ alias svn='echo hello'

# 何も表示されない
$ type -P svn >/dev/null && echo "見つかったよ"

# お掃除
$ unalias svn

# 関数を設定
$ function svn () { echo world; }

# 何も表示されない
$ type -P svn >/dev/null && echo "見つかったよ"

# お掃除
$ unset svn

実際に実行可能ファイルが存在していたときにも正しく動作するか試してみましょう。

$ type -P git >/dev/null && echo "見つかったよ"
見つかったよ

ちなみに、当然ですが type -P 実行後に実行可能ファイルが見つかったときの終了ステータスは 0 、見つからなかった場合の終了ステータスは 1 となります。

# 見つかったファイルのフルパスが出力される
$ type -P git
/usr/bin/git

$ echo $?
0

$ type -P svn

$ echo $?
1

"type -P" で実行可能ファイルが予約語、エイリアス、関数と重複していた場合はどうなる?

実行可能ファイルが予約語、エイリアス、関数と重複していた場合は type -P コマンドの実行結果はどうなるでしょう。

ここでは time コマンドを例に試してみます。
time コマンドは Bash の予約語でありながら、 /usr/bin/time に存在する実行可能ファイルでもあります。

エイリアスと関数を用意すれば、準備完了です。

# type を実行すると予約語が優先して見つかる
$ type time
time はシェルの予約語です

# which を実行すると、実行可能ファイルが見つかる
$ which time
/usr/bin/time

# エイリアス設定
$ alias time="echo hello"

# 関数設定
$ function time () { echo world; }

type -P コマンドを実行してみます。

$ type -P time
/usr/bin/time

めでたく「実行可能ファイル」のみが検索対象として見つかりました。
type -Pwhich の実行結果は同じと考えて良さそうです。

ここから更に調査を進めてみました。

実行可能ファイル、予約語、エイリアス、関数がすべて存在した場合 type はどんな挙動をするの?

せっかく time コマンドについて実行可能ファイル、予約語、エイリアス、関数をすべて用意したのです。

このような状況下で type はどんな挙動をするのかを調べてみました。

type コマンドには -a というオプションがあります。
-a オプションを付けるとすべてのコマンドを検索し結果を表示してくれます。
試してみます。

$ type -a time
time は `echo hello' のエイリアスです
time はシェルの予約語です
time は関数です
function time ()
{
    echo world
}
time は /bin/time です

time コマンドの検索結果がすべて出力されました。

出力内容から予想が付きそうですが、 type を実行した際に利用されるコマンドの優先順位は以下のようになりそうです。

  1. エイリアス
  2. 予約語
  3. 関数
  4. 実行可能ファイル

type コマンドをオプション指定なしで実行してみます。

$ type time
time は `echo hello' のエイリアスです

予想通り、エイリアスが見つかります。

今度はエイリアスを削除してから time コマンドを検索してみます。

$ unalias time

$ type time
time はシェルの予約語です

予想通りですね。
予約語を消すことはできないので、今回はここまでにしておきました。

ひとこと

特定のツールが使える状態にあるかどうかを判定する場合は、 エイリアスや関数を誤検知しないように以下のように判定するのが良いでしょう。

if ! type -P one_tool >/dev/null; then
  echo "one_tool is not exist."
fi

2023-03-27Bash