which と type の違い|コマンドの正体を調べる

which と type の違い|コマンドの正体を調べる

「このコマンド、いま実際にどれを呼んでいるんだろう」と手が止まることがあります。

私はとりあえず which を打つ癖がついていました。

ところが which は、エイリアスもシェル関数もビルトインも教えてくれません。

同じ用途に見える type と command -v がわざわざ別に用意されているのは、そこに理由があるからです。

この記事では3つの違いを、実際の出力を並べながら整理していきます。

3つは立ち位置がそもそも違う

最初に押さえたいのが、which だけが外部コマンドで、type と command はシェルの組み込みだという点。

外部コマンドは別プロセスとして起動されます。

つまり which は、呼び出し元のシェルが持っているエイリアスや関数の情報を受け取れないのです。

以下の確認はすべて Ubuntu 22.04.5 LTS / bash 5.1.16 で実行しました。

$ type which
which is /usr/bin/which

$ type ls
ls is /usr/bin/ls

which は PATH 上の実行ファイルしか見ない

which ls は素直に /usr/bin/ls を返します。

一方で cd を渡すと、何も出力しないまま終了ステータス 1 で終わる。

$ which ls
/usr/bin/ls

$ which cd
$ echo $?
1

cd が存在しないわけではなく、cd はシェルのビルトインなのでファイルとして PATH 上に無いだけです。

which の答えは「PATH を探した結果」であって、「シェルが実際に何を実行するか」ではないのですね。

ついでに言うと、Ubuntu の /usr/bin/which は debianutils が入れている /bin/sh のスクリプトでした。

つまり which の挙動そのものがディストリビューション依存で、移植性を当てにできない道具なのです。

type はシェルが知っていることを全部見せる

type はシェル自身が答えるので、ビルトインもエイリアスも関数も区別して返してくれます。

$ type cd
cd is a shell builtin

$ alias ll='ls -alF'
$ type ll
ll is aliased to `ls -alF'

$ mygrep() { echo "func"; }
$ type -t mygrep
function

エイリアスなら展開後の中身、関数なら定義そのものまで表示される。

種別だけ知りたいときは type -t が手軽で、file / builtin / alias / function / keyword のいずれかが1語で返ります。

同名のものが複数あるときは type -a、エイリアスを無視して PATH 上の実体だけ見たいときは type -P を使います(Bash Reference Manual)。

$ type -a echo
echo is a shell builtin
echo is /usr/bin/echo
echo is /bin/echo

$ type -P ls
/usr/bin/ls

echo がビルトインと実行ファイルの両方で存在していることも、これで一目で分かるのです。

スクリプトに書くなら command -v

手元で確かめるだけなら type で十分ですが、スクリプトに書くとなると話が変わります。

POSIX に規定があるのは command のほうで、which は POSIX の仕様に含まれていません(POSIX 仕様: command)。

$ command -v ls
/usr/bin/ls

$ command -v cd
cd

$ command -v ll
alias ll='ls -alF'

command -v は見つかったものを1行で返し、無ければ終了ステータスを 0 以外にします。

依存コマンドの存在確認は、この性質を使った次の形が定番。

# スクリプト冒頭で依存コマンドを確認する
if command -v jq >/dev/null 2>&1; then
  echo "jq is available"
else
  echo "jq not found" >&2
  exit 1
fi

この書き方なら dash のような軽量シェルでも挙動は同じです。

type が hashed と答えたときは要注意

type を打つと、たまに is hashed という見慣れない返事が来ることがあります。

bash は一度実行したコマンドの場所を記憶していて、次からは PATH を探し直さずにその記憶を使うのです。

この記憶が古いままだと、ファイルを移動したあとに「そんなファイルはない」と怒られる状況が起きます。

$ export PATH=/tmp/h1:/tmp/h2:$PATH
$ hello
v1
$ rm /tmp/h1/hello
$ hello
bash: /tmp/h1/hello: No such file or directory

$ hash -r
$ hello
v2
$ type hello
hello is hashed (/tmp/h2/hello)

本当は /tmp/h2 に同名のコマンドが残っているのに、消えたほうのパスを掴んだままエラーになっていました。

記憶を捨てる hash -r を1回打てば、PATH を探し直して正しいほうに繋がります。

「さっき直したはずなのに反映されない」ときの犯人が、この hash であることは珍しくありません。

使い分けの結論

手元で「これ何だっけ」と確かめたいときは type。

スクリプトで「入っているか」を判定するなら command -v を選びます。

which はこの2つでほぼ置き換えられるので、私はもう積極的には使わなくなりました。

ついでに、PATH の並び順で呼ばれるものが変わることも押さえておくと事故が減ります。

$ mkdir -p /tmp/binx
$ printf '#!/bin/sh\necho fake\n' > /tmp/binx/date
$ chmod +x /tmp/binx/date
$ PATH=/tmp/binx:$PATH bash -c 'type date; date'
date is /tmp/binx/date
fake

同名のファイルを /tmp/binx に置いただけで、date の実体が入れ替わりました。

PATH は前から順に探索されるため、意図しないディレクトリが先頭に紛れていないかは type で確認できるのです。

PATH そのものの読み方は、別記事にまとめています。

PATHを通すとは?環境変数の仕組みとexportの意味

コマンドではなくファイルそのものの正体を知りたいときは、file の出番。

fileコマンド入門|拡張子に頼らずファイルの正体を知る

最後に

which・type・command -v は似た顔をしていますが、答えている質問がそれぞれ違いました。

which は「PATH のどこにあるか」、type は「シェルが何と解釈するか」、command -v は「移植性のある形で存在するか」。

迷ったら type を打ち、スクリプトには command -v を書く。

この2本立てを覚えておけば、コマンドの正体で悩む場面はぐっと減ります。

以上です。

コメントを残す

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

CAPTCHA