はてブロ@ama_ch

https://twitter.com/ama_ch

シェルの処理順序

詳解 シェルスクリプト

詳解 シェルスクリプト


今読んでる詳解シェルスクリプトにシェルの処理順序が詳しく書いてあり、とっても参考になったのでメモしておきます。


p.176 7.8 処理の順序とevalコマンド より


標準入力やスクリプト中の各行からシェルへと読み込まれる1行分のデータを、パイプラインと呼ぶ。パイプラインには1つまたはそれ以上のコマンドが記述され、それぞれの間にはパイプ記号(|)やその他の記号(;, &, &&, ||)が記述される。シェルは読み込んだパイプラインをコマンド単位に分割し、それぞれについて以下の順に処理を行う。

  1. スペース、タブ、改行、;、(、)、<、>、|、&を区切り文字として、コマンドをトークンと呼ばれる単位に分割する。
  2. コマンド中の最初のトークンについて、それがクオート*1されておらずかつキーワードであるかどうかを確認する。もしこれが開始のキーワード(if, { ,()だった場合、そのコマンドは複合コマンドであると判断され、次のコマンドを読み込んで処理を継続する。最初のトークンが開始のキーワードでなかった場合(then, else, do, fi, doneなど)は、構文エラーが発生する。
  3. コマンド中で最初に現れた語について、それがエイリアスのリストに含まれているかどうかチェックする。含まれている場合、その語を置き換え、1. に戻って処理を続ける。含まれていなかった場合は4. に進む。なお、語を一部でもクオートしてある場合はエイリアスを使った置換は行われない。
  4. 語が~で始まる場合、$HOMEの値に置き換える(チルダ展開)。
  5. $で始まる部分について、変数名を実際の値に置き換える。
  6. $(string)または`string`と記述されている部分を、コマンド展開する。
  7. $((string))と記述されている部分について、算術展開を行う。
  8. 以上の処理を経た結果の文字列を、もう一度語の単位に分解する。ここでは1. で使われた文字ではなく、変数IFSで指定された文字が区切り文字として使われる。
  9. *, ?, [...]についてワイルドカード展開を行い、ファイル名を生成する。
  10. 最初の語をコマンド名とみなす。特別な組み込みコマンド、関数、通常の組み込みコマンド、変数PATHを探索して最初に見つかったものの順にコマンドを探す。*2
  11. 入出力のリダイレクトなど、必要な設定を行ってコマンドを実行する。


なんだかごちゃごちゃしているので、例で実際に処理の流れを見てみます。
ちなみに詳解シェルスクリプトのp.178には上記の処理手順がフローチャートになっており、そちらを見る方が分かりやすいです。

% mkdir temp; cd temp
% touch file{0..9}    # file0〜file9を生成
% f=file
% str="foo bar"
% echo ~/temp/${f}[12] $str $(echo iam ama-ch.) $((15 + 3 + 4)) years old.    # ここの処理を見るよ!
/Users/ama-ch/temp/file1 /Users/ama-ch/temp/file2 foo bar iam ama-ch. 22 years old.

上のコマンドの5行目(echo hoge)について、処理を追ってみます。


1. シェルで定められた構文に従って、コマンドが以下のトークンに分解される。

1 2 3 4 5 6 7
echo ~/temp/${f}[12] $str $(echo iam ama-ch.) $((15 + 3 + 4)) years old.

2. 最初の語がifやforなどの開始キーワードかどうかチェックする。echoはキーワードではないので、そのまま次へ進む。
3. 最初の語にエイリアスが存在するかどうかチェックする。echoはこれも当てはまらないので、次へ進む。
4. すべての語に対して、チルダ展開が可能かどうかチェックされる。ここでトークン2のチルダが展開され、以下のようになる。

1 2 3 4 5 6 7
echo /Uses/ama-ch/temp/${f}[12] $str $(echo iam ama-ch.) $((15 + 3 + 4)) years old.

5. 変数展開が行われ、トークン2と3が展開される。

1 2 3 4 5 6 7
echo /Uses/ama-ch/temp/file[12] foo bar $(echo iam ama-ch.) $((15 + 3 + 4)) years old.

6. コマンド展開が行われ、トークン4が展開される。*3

1 2 3 4 5 6 7
echo /Uses/ama-ch/temp/file[12] foo bar iam ama-ch. $((15 + 3 + 4)) years old.

7. トークン5の算術展開が行われる。

1 2 3 4 5 6 7
echo /Uses/ama-ch/temp/file[12] foo bar iam ama-ch. 22 years old.

8. 変数IFSの値を区切り文字として、ここまでの処理結果を分解しなおす。

1 2 3 4 5 6 7 8 9
echo /Uses/ama-ch/temp/file[12] foo bar iam ama-ch. 22 years old.

9. そしてワイルドカード展開が行われ、トークン2が2つに展開される。

1 2 3 4 5 6 7 8 9 10
echo /Uses/ama-ch/temp/file1 /Uses/ama-ch/temp/file2 foo bar iam ama-ch. 22 years old.

10. コマンドを実行する準備が整い、echoというコマンドを検索する。
11. コマンドが実行される!echoによって、引数の値が出力される。

% echo ~/temp/${f}[12] $str $(echo iam ama-ch.) $((15 + 3 + 4)) years old.
/Users/ama-ch/temp/file1 /Users/ama-ch/temp/file2 foo bar iam ama-ch. 22 years old.


なるほど、こういう風に動いてたのか〜。

*1:*や?などの特別な意味を持つ文字を、普通の文字として解釈させる方法。バックスラッシュによるエスケープや、引用符の利用がこれに当たる。

*2:だから関数で通常のコマンドを上書きできるのか!

*3:実際にはコマンド展開で1. からの手順がもう1度適用されるが、ここでは省略。