展開とホワイトスペース

前回はパラメータ展開、コマンド置換、算術展開について取り上げた。これら展開処理が行われた結果には複数の空白(ホワイトスペース)が含まれていることがあると思うが、結果が「”(ダブルクォーテーション)」で囲まれている場合と囲まれていない場合とで、そのホワイトスペース周りの挙動が異なってくる。

シェルはパラメータ展開、コマンド置換、算術展開などを実施したあと、その展開後の結果をパースする。ダブルクォーテーションで囲まれていれば全体を文字列として認識してホワイトスペースについての処理は行われないが、ダブルクォーテーションで囲まれていないのであれば、シェルは結果をパースして、次のように処理を行う。

  • 変数IFSに設定されている文字(デフォルトはスペース、タブ、改行、が設定されている)については、展開結果の先頭と末尾にあるものは削除する
  • IFSに含まれている文字が区切り文字として認識され、それ以外の文字はIFSに含まれている文字で分割される


IFSは区切り文字を指定する特別な変数で、デフォルトでは空白、タブ、改行が設定されている。IFSはシェルの展開処理やパース処理における区切り文字を指定するものであり、動作に影響を与えるものとなっている。

ダブルクォーテーションのあり/なしで動作が変わる様子は、次のサンプルを見てもらうのがわかりやすいだろう。

# cal 9 2020
      9月 2020
日 月 火 水 木 金 土
       1  2  3  4  5
 6  7  8  9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30

# echo "$(cal 9 2020)"
      9月 2020
日 月 火 水 木 金 土
       1  2  3  4  5
 6  7  8  9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30

# echo $(cal 9 2020)
9月 2020 日 月 火 水 木 金 土 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
#

「cal」はカレンダーを出力するコマンドだ。このコマンドをコマンド置換で実行した結果を、ダブルクォーテーションで囲っておくと、calコマンドを単体で実行したときと同じ結果を得ることができる。出力結果がそのまま保持されるからだ。

しかし、ダブルクォーテーションを指定しないと、カレンダー出力における空白と改行が区切り文字として認識され、それを除いて個別に分割したものを空白で区切ったものがデータとして整理される。結果として、1行にまとまったデータが出力されていることがわかると思う。

ちなみに、IFSを明示的に変更するような利用シーンがあるのかと言われると、「なくもないが、別にIFSを変更してシェルで処理するべきとも言えない」といったところではないかと思う。例えば、次のようにIFSを変更すれば、シェルのホワイトスペースを使ったパース機能で「:(コロン)」区切りのデータを切り分けることができる。

# tail /etc/passwd
nobody:*:65534:65534:Unprivileged user:/nonexistent:/usr/sbin/nologin
daichi:*:501:20:Daichi GOTO:/Users/daichi:/usr/local/bin/fish
_tss:*:601:601:TrouSerS user:/var/empty:/usr/sbin/nologin
cups:*:193:193:Cups Owner:/nonexistent:/usr/sbin/nologin
git_daemon:*:964:964:git daemon:/nonexistent:/usr/sbin/nologin
messagebus:*:556:556:D-BUS Daemon User:/nonexistent:/usr/sbin/nologin
avahi:*:558:558:Avahi Daemon User:/nonexistent:/usr/sbin/nologin
tests:*:977:65534:Unprivileged user for tests:/nonexistent:/usr/sbin/nologin
polkitd:*:565:565:Polkit Daemon User:/var/empty:/usr/sbin/nologin
sasaki:*:1001:1001:Yoshifumi SASAKI:/Users/sasaki:/bin/sh
# T=$IFS
# IFS=:
# tail /etc/passwd |
> while read user passwd id others
> do
>    echo $user $id
> done
nobody 65534
daichi 501
_tss 601
cups 193
git_daemon 964
messagebus 556
avahi 558
tests 977
polkitd 565
sasaki 1001
# IFS=$T
#

しかし、正直なところこの書き方はわかりにくい。同じことをするならawkやsedを使ったほうがわかりやすいと思うし、無理にシェルで処理する必要はないように見える。

シェルパターン

IFSを使ったホワイトスペース処理が行われた後はパス名展開が行われるのだが、その前にパターンについて説明しておく必要がある。シェルはパス名やcase制御構文、パラメータ展開などでパターンを指定することができる。シェルで使用できるパターンは次の通りだ。

パターン 内容
* 任意の文字列に一致
? 任意の1文字に一致
[文字列] 指定された文字列のうちのどれか1文字に一致。]を含める場合には[の直後に]を指定する
[!文字列] 指定された文字列のうちのどの文字にも一致しない
[^文字列] [!文字列]と同じ

シェルのパターンはとてもシンプルなものだ。正規表現ほど複雑な指定をすることはできない。以下に、シェルパターンの簡単な使用例を示しておく。

# ls
commands    tables      typescript.txt  typescript.yd
Makefile    typescript.html typescript.xml
# ls -l
total 48
drwxr-xr-x  2 daichi  staff   512  8月  7 18:04 commands
-rw-r--r--  1 daichi  staff   565  8月  7 17:04 Makefile
drwxr-xr-x  2 daichi  staff   512  8月  7 17:56 tables
-rw-r--r--  1 daichi  staff  9604  8月  7 18:07 typescript.html
-rw-r--r--  1 daichi  staff  7425  8月  7 18:07 typescript.txt
-rw-r--r--  1 daichi  staff  7071  8月  7 18:07 typescript.xml
-rw-r--r--  1 daichi  staff  7512  8月  7 18:07 typescript.yd
# echo typescript.*
typescript.html typescript.txt typescript.xml typescript.yd
# echo .*
. .. .build.pid
# echo ./ty*
./typescript.html ./typescript.txt ./typescript.xml ./typescript.yd
#

シェルパターンはコマンドが処理する正規表現と混乱しやすい。どの段階ではシェルがパターン展開を行い、どの段階でコマンドが正規表現を処理しているのか、その辺りをちゃんと区別しておくことが大切になる。

パス名展開

先述の通り、IFSに基づくホワイトスペース処理が行われた後にはパス名の展開が実施される。パス名の展開は、パターンの一致する文字列に展開することで行われるのだが、次のルールがある。

  • パターン展開ではパターン部分はスラッシュを含む文字列には展開されない
  • パターンの最初の文字がピリオドになっていない限り、先頭がピリオドで始まるパス名には展開されない


パス名展開はディレクトリやファイル名ごとに発生し、スラッシュを超えてパターンが一致することはないし、明示的に指定しない限り隠しファイルや隠しディレクトリに展開されることもない。

展開順序

ここまで展開処理について理解すると、それぞれの展開がどのような優先順位で行われるかがわかるはずだ。

  1. チルダ展開、パラメータ展開、コマンド展開、算術展開
  2. 上記展開処理で発生した文字列に対してIFS変数を利用したフィールド分割
  3. パス名展開
  4. クォートを削除


シェルの操作はそれほど難しいものではない。ただし、実際には上記のように処理の順番が厳密に決まっており、シェルはそのルールに従って処理を行っていることを覚えておいてほしい。

シェルの仕組みとして説明すべきことは、これでほとんど取り上げたことになる。慣れれば考えることなくシェルを使うようになると思うが、時にはこうやってシェルがどのように処理を進めているのかに思いをはせてみるとよいだろう。特にシェルスクリプトやワンライナーの作成などで行き詰まったときは、こうして基礎に戻って見直すとすらっと問題がとけることもある。

参考資料