while・read・exec 再入門

シェルで1行ずつファイルから読み込む - あしのあしあと」、「絵で見てわかるファイルディスクリプタ・パイプ・リダイレクト - あしのあしあと」に引き続き、「シェルで1行ずつファイルから読み込む」シリーズの3回目。今回は、シェルで1行ずつファイルから読み込む際に用いる、1) while 文、2) read コマンド、3) exec コマンドについてメモしておく(while や read はよく使うにもかかわらず、きちんと理解していなかったなぁと反省しながら)。

1) while 文

while 文は、「条件が true の間、処理を繰り返す」ループ処理に用いる。で、シェルの場合には、条件が true とはつまり数値の“0”のことなので、「コマンドの戻り値が 0 である間、処理を繰り返す」とも言える。さらに言うと、複数のコマンドが並んだ時は、最後の戻り値が全体の戻り値であるので、「最後のコマンドの戻り値が 0 である間、処理を繰り返す」とも言える。

あと知っておくべきは、while 〜 done までの全体を、あたかも1つのコマンドのように扱えるという点だ。次のように、パイプ、リダイレクトやヒアドキュメントと組み合わせて用いることができる。

なるほど。これは便利。

2) read コマンド

read コマンドは、標準入力からの1行のテキストを、変数 IFS で区切って、指定された変数に格納するコマンドである。これは、けっこう使うコマンドではないか。IFS には、スペース、タブ、改行が指定されているが、カンマやスラッシュなど、他の文字を指定することも可能だ。


例えば、次のように、変数 val1 〜 val4 までを用意する(もちろん最初は空)。

$ val1=; val2=; val3=; val4=
$ echo "val1=${val1}, val2=${val2}, val3=${val3}, val4=${val4}"

val1=, val2=, val3=, val4=

さらに、次のテキストファイル a2z.txt を用意する。

abcdef ghi jklmn
opq rst uvw xyz

このファイルを標準入力からリダイレクトさせると、1行目が読み込まれ*1、変数 val1 〜 val4 に値が格納される。これが最も典型的な使用例だと思う。

$ read val1 val2 val3 val4 < a2z.txt
$ echo "val1=${val1}, val2=${val2}, val3=${val3}, val4=${val4}"

val1=abcdef, val2=ghi, val3=jklmn, val4=

ちなみに、ヒアドキュメントを使えば、上記の処理は次のようにも書ける。

read val1 val2 val3 val4 << __EOC__
abcdef ghi jklmn
opq rst uvw xyz
__EOC__

次のように書いても、全く同様。

read val1 val2 val3 val4 << __EOC__
`cat a2z.txt`
__EOC__


さて。ここで、次のように「cat や echo の出力をパイプラインで処理すればよいのでは?」と思ってしまうところだ。
だって、read コマンドは、標準入力の値を変数に格納するコマンドだから(そして、パイプを用いて cat コマンドの標準出力を標準入力につなげているから)。

# これでもよいと思うのだが、ダメな使い方。
cat a2z.txt | read val1 val2 val3 val4

でも、このやり方は使えない。けっこう多くの人が、一度は(軽く)つまずいたのではないだろうか?そして、私も同じようにつまずいた。
なぜか?これは、パイプの先のプロセスで、val1 〜 val4 に値が設定されているからだ。元のシェルのプロセスでは使えない。これでは意味がない。でも確かに値は設定されるのだ。例えば、次のようにすれば、val1 に値が設定されているのがわかる。

# val1 には、abcdef が設定される。
cat a2z.txt | (read val1 val2 val3 val4; echo ${val1})

これで、「シェルで1行ずつファイルから読み込む - あしのあしあと」で紹介したダメなスクリプトが、なぜダメなのか、ようやくわかった。while 〜 done 全体をパイプでつなぐのはよいが、read コマンドによって設定された値や、while 文中での操作は「別世界」で実行されているのだ。

3) exec コマンド

exec コマンドは、次のようなコマンドだ。正直なところ、実は今まで使ったことがない。“exec ls”とか実行すると、シェルが終わってしまうからね。シェルにおいては、限られた用途でしか使わない。

その限られた用途の1つが、(0〜2番以外の)新たなファイルディスクリプタ(以下、“FD”と略す)を使いたい時。例えばFDの4番を使いたい時は、“exec 4> ファイル”によってオープンされ、“exec 4>&-”によってクローズされる(最後の“-”がクローズの意味)。
ちなみに、オープンしたFDは、サブシェルにも引き継がれる。まぁ fork するからね。
そんな様子も含めて、1つFDの4番を用いる例を書いてみる。ちなみに、FDの4番が使える状態になっていないと(オープンされていないと)、不正なFDだと怒られることになる。

echo "ABCDEF" >&4      # 不正なFDと怒られる
exec 4> file.txt       # FD4番をオープンする(file.txt に向ける)
echo "ABCDEF" >&4      # 標準出力の内容が(FD4番の先である)file.txt に出力される
./subshell.sh          # シェル内でFD4番をクローズする
echo "GHIJKL" >&4      # 親シェルのFD4番はクローズされていないため、成功する
exec 4>&-              # 親シェルのFD4番をクローズする

ここで、subshell.sh は次のような内容である。

#!/bin/sh
echo "abcdef" >&4      # FD4番のオープンを引き継いでいるため、成功する
exec 4>&-              # FD4番をクローズする
echo "ghijkl" >&4      # 不正なFDと怒られる

fork でプロセスが作成され、サブシェル(子プロセス)でもFDの4番が使える。クローズはもちろん、自分のプロセスのFD4番だよ、と。


これで、「シェルで1行ずつファイルから読み込む」ことができるようになったはず。

*1:1行目しか読み込まれない。なので、“while read val1 val2 val3 val4 < a2z.txt do 処理 done”なんてやってしまうと、毎回1行目が読み込まれるハメになる。