シェルは1行ずつコマンドを理解して処理を実行する。最初は少々退屈かもしれないが、どういった規則で入力文字列を解釈してコマンドの実行へ持っていくのか、その構造を理解してしまおう。

字句構造

シェルはコマンド入力やファイルからの入力(シェルスクリプト)を1行ずつ処理していく。入力された文字列は「空白」「タブ」「演算子」と呼ばれる文字列を区切り文字と解釈し、入力を分割処理していく。

演算子には「制御演算子」と「リダイレクト演算子」という2種類に分類される。それぞれ次のようなものだ。

演算子 対象
制御演算子 &、&&、(、)、改行、;;、;&、;、|、||
リダイレクト演算子 <、>、<<、>>、<>、<&、>&、<<-、>|

入力される行はまず最初に「空白」「タブ」「演算子(制御演算子、リダイレクト演算子)」によって分けられる、と覚えておけばよい。

コメント

「空白」「タブ」「演算子(制御演算子、リダイレクト演算子)」によって分けられることで、行データは単語の集まりとして処理される。このとき、単語の先頭が「#」になっていると、それ以降はコメントとして処理されるようになる。

$ echo Hello World!
Hello World!
$ echo Hello # World!
Hello
$ # echo Hello World!
$

インタラクティブシェルとしてshを使っている場合には明示的にコメントを使うことはほとんどないと思う。シェルスクリプトとしてshを使うようになると、「#」をよく使うようになる。行頭に配置してその行をまるごとコメントとして使うことが多いが、上記サンプルのように行の途中に置いても機能する。コマンドの説明をコマンドの後ろに「#」を書いてコメントとして書き込んでおくといったような使い方がある。

クォート

最初に入力された行データは「空白」「タブ」「演算子(制御演算子、リダイレクト演算子)」を区切り文字として使って分割されると説明したが、クォートと呼ばれる処理をすると「空白」「タブ」「演算子」などをただの文字として扱うことができるようになる。特定の文字から特殊な意味を削除するというものだ。

shでは空白、タブ、演算子、キーワード、エイリアスなどが特殊な文字として機能するが、クォートを使うとこれらを特殊な文字として機能しないようにすることができる。shでは基本的に「バックスラッシュ」「シングルクォート」「ダブルクォート」という3種類のクォートが提供されている。

クォート 内容
バックスラッシュ その後に続く1文字から特殊な意味を取る。ただし、\が行末にある場合(つまり\のあとに改行がある場合)には、改行せずに行が連続しているという処理になる
シングルクォート 囲った文字列から特殊な意味を取る。ただし、シングルクォートで囲む文字列の中にシングルクォートを含めることはできない
ダブルクォート 囲った文字列から特殊な意味を取る。ただし、$、`、\に関しては特殊な意味を保持する。ダブルクォートの中の\は、その後に$、`、”、\、改行、がこない限りは文字としての\として機能する。しかし、\のあとに$、`、”、\、改行、がある場合、上記のようにクォートとしてのバックスラッシュとして機能するようになる

実際に使って表示を確認したほうが理解が早いかもしれない。

$ echo a b c
a b c
$ echo 'a ' 'b ' ' c'
a  b   c
$ echo 'a $(date)' "b $(date)" ' c'
a $(date) b 2019年 6月18日 火曜日 10時58分21秒 JST  c
$

シングルクォートはわかりやすいと思う。基本的にシングルクォートで囲った中身はただの文字列として扱われる。バックスラッシュもわかりやすい。バックスラッシュのあとの1文字はただの文字として扱われる。

わかりにくいのはダブルクォーテーションだ。ダブルクォーテーションの中では$、`、\は特殊文字として機能するが、それ以外はただの文字列として扱われる。ただし、ダブルクォーテーションの中のバックスラッシュは状況によって振る舞いが異なる。この辺りは慣れの問題でもあるので、確認しながら使っているうち使いこなせるようになるだろう。

キーワード (予約語)

行の先頭または制御演算子の直後には「キーワード」または「予約語」と呼ばれる言葉が置かれることがある。キーワードはプログラミング言語でいうところの制御構文にあたるもので、shでは次の言葉が使われている。

キーワード
!、{、}、case、do、done、elifelseesac、fi、for、if、thenuntil、while

キーワードを使うことでシェルスクリプトではプログラミング言語的な書き方が可能になる。

エイリアス

shではコマンドに対してエイリアスと呼ばれる別名を設定しておくことができる。shは入力されるデータを空白や演算子をベースに分割したあとで、コマンドがあるべき場所にエイリアスがあった場合、そのエイリアスを本来のコマンドに置き換える処理を行う。

例えば、「ll」という「ls -l」へのエイリアスが設定されていたとする。

$ alias ll='ls -l'
$ alias
ll='ls -l'
$

このとき、次のようにコマンドを実行したとしよう。

$ ll /tmp/

すると、shは行データを読み込み、分解を行った後、「ll」の部分を「ls -l」へ置き換えるという処理を行ってからコマンドの実行へと移ることになる。

ls -l /tmp/

これは特にインタラクティブシェルとしてshを利用する場合によく使う機能だ。頻繁に入力するコマンドは、オプションも含めて短い文字列でエイリアスを作っておく。入力文字数を少なくすることでミスを減らし、作業効率を上げることができる、というわけだ。

シンプルコマンド

ここまで展開が進むと、基本的には行頭にある単語または制御演算子の後にある単語がキーワードではなかった場合、その入力を「シンプルコマンド」と解釈する。シンプルコマンドは次の順序で処理される。

  1. 先頭から続く「名前=値」というフォーマットの単語は取り除かれるともに、シンプルコマンドの環境に割り当てられる(コマンドの環境変数として設定されることになる)。リダイレクトも取り除かれ、後の処理のために保存される
  2. ワード展開が実施され、その結果として残っている最初の単語が「コマンド」と解釈される。コマンドに続く単語はそのコマンドの引数と解釈される。この段階でコマンドが存在しない、かつ、「名前=値」という指定があった場合、この変数は現在実行中のシェルの変数として設定される
  3. リダイレクトを処理する


これがシェルにおける最も基本的なコマンド実行までの主な流れとなる。

じっくり理解していこう

言葉にして書くと、何か難しいことをしているように見えるかもしれないが、実際にはシェルにコマンドを入力して処理を行っていることをちょっと整理してまとめただけのものだ。感覚的に覚えていることは、実際にはこういったロジックに従って処理されている。

面倒なところはあるのだが、今回説明したようなことをきっちり頭の中に入れておけると、シェルスキルはグンとアップする。シェルは理解に時間をかけても損のないインタフェースだ。長い目で見て地道に学習すれば、最終的に最も楽ができるようになる部分だとも言える。ゆっくりとでもよいので、着実に理解を深めていってみよう。

参考資料