「それ」を解釈するのは誰?(シェルスクリプト)

課題

以下のようなディレクトリ構造を考える。

$ tree
.
├── dirl1
│   ├── dirl2
│   │   └── scriptl2.sh
│   └── scriptl1.sh
└── scriptl0.sh


このディレクトリを再帰的に探索して、すべてのシェルスクリプトのファイル(拡張子が.shのファイル)を出力するため、以下のようなコマンドを実行したが、意図通りに動作しない。 つまり3ファイルの出力を想定したが1ファイルのみ出力されている。

$ find . -name *.sh
./scriptl0.sh

解決法

以下のようにコマンドを実行する。

$ find . -name '*.sh'
./dirl1/scriptl1.sh
./dirl1/dirl2/scriptl2.sh
./scriptl0.sh


2つのコマンドの違いは、探索するファイル名のパターンをクォートしているか否かである。 クォートすることで、コマンドラインに打ち込んだ「*.sh」というパターンが意図通りfindコマンドに渡される。

そもそも、コマンドラインに打ち込んだ文字列がコマンドとして実行されるまでに、シェルによる評価を受ける。 その評価のうちの1つに「パス名展開」があり、先述の「*」がこの評価に大きく関わってくる。

パス名展開によれば「*」はカレントに存在するファイル名にマッチするような文字列に展開される。 今回はカレントに「scriptl0.sh」が存在する状態で「*.sh」のパターンが指定されたので、これがシェルによって「scriptl0.sh」という文字列に展開された。 そしてfindコマンドは「scriptl0.sh」という名前のファイルを探索するように指定されたことになる。 これはもともとの意図(拡張子が.shのファイルを探索したいという意図)とは明らかに異なる。

findコマンドにもともとの意図(拡張子が.shのファイルを探索したいという意図)が反映されたパラメータ(*.sh)を渡すには、シェルによるパス名展開の「洗礼」を避ける必要がある。 これが解決法として示したクォートである。 クォートは特別な文字をシェルに解釈させないために利用される。

クォートは今回の場合は上記の例のほか、以下のようにしても良い。 ただし通常は使い分けされることが多いので注意すること。

$ find . -name "*.sh"
./dirl1/scriptl1.sh
./dirl1/dirl2/scriptl2.sh
./scriptl0.sh
$ find . -name \*.sh
./dirl1/scriptl1.sh
./dirl1/dirl2/scriptl2.sh
./scriptl0.sh

所感

シェルスクリプトを使いこなすうえで、どの文字列を誰が解釈するかを理解しておくことは非常に重要である。 たまに地獄のようなクォーテーションが使われることがあるが、臆せず立ち向かっていきたい(避けたほうが良い)。