で、今度こそシェルでテキストファイルから1行ずつ読み込む

「シェルでテキストファイルから1行ずつ読み込む」シリーズの最終回。これまで、次のような内容で書いてきた。


で、ようやくスタートの、このスクリプトに戻る。

BUFIFS=$IFS
IFS=

exec 3< 入力ファイル名
while read FL 0<&3
do
        処理
done
exec 3<&-

IFS=$BUFIFS

【 ファイルからの読み込み 】 | 日経 xTECH(クロステック)より》


このスクリプトは、ボーンシェル(bourne shell)など、パイプを使わずとも while read が別プロセスで(現在のシェルの変数に影響せずに)実行されてしまう場合を意識しているらしい*1。とてもお行儀のよいスクリプトなのだ。なお、bash においては新たなFD(ファイルディスクリプタ)を用意する必要はない*2。なので、最初にうまくいかなかったシェルは、このエントリの最後に示すように書き直せば、とりあえず動く。


では早速、スクリプトの中身に移ろう。
まず初めに、下準備をしている。ファイルからテキストを“1行ずつ”読み込みたいのだから、環境変数 IFS に空白が入っていてはまずい(空白で切られてしまう)。そこでまず、IFS を BUFIFS に退避し、空にする。で、最後に BUFIFS から元に戻す。ま、これはよいだろう(read コマンドを用いる場合には、必ず留意しなければならない)。
その後だ。
“exec 3< 入力ファイル名”を実行している。2回目の話3回目の話を聞いたことがないと、すっとばしたくなる行だが、もう大丈夫だろう。次の図のように、現在のシェルで、FDの3番からファイルの読み込みができるようになったわけだ。絵のかきっぷりについては、2回目を参照のこと。

この状態で“read FL 0<&3”が実行される。FDの0番(標準入力)からの読み込み“0<”を、FDの3番と同じファイル“3&”からにしているため、このコマンドが実行される時は、ファイルの内容が標準入力から入力される。read コマンドは、標準入力からの入力を読み込むコマンドであるため、“0<3&”しているわけだ(あたりまえだが、直接FDの3番からは読み込んでくれない)。

ん?だったら、最初から“exec 0< 入力ファイル名”でよいのでは?
と思うのだが、これをやると標準入力が完全に奪われてしまう。

“exec 0< 入力ファイル名”を標準入力を奪われずにやるのであれば、“exec 3<&0 < 入力ファイル名”とすればよい。“3<”をFDの0番と同じファイル(画面)から“&0”にして、その後“0<”を入力ファイルからにするのだ。つまり、ひとまず画面からの入力をFDの3番経由にしておいて、while read のために0番を占有するということだ。


なお、これらの方法は、ブルース・ブリンの「入門UNIXシェルプログラミング*3」にもバッチリ記載がある。

入門UNIXシェルプログラミング―シェルの基礎から学ぶUNIXの世界

入門UNIXシェルプログラミング―シェルの基礎から学ぶUNIXの世界



最後に、(今回は bash を用いているので)最初にうまくいかなかったシェルは、while 文の前でパイプを用いず、リダイレクト(またはヒアドキュメント)を用いて書き直せばよいことがわかる。これは、3回目の read コマンドの実験で明らかになった。
例えば、次のようにすればよいだろう(このシェルは、ファイル _target_file から _rec_mark から始まる行のみを読み込み、_rec_mark 以降の文字列(空白を除く)を抽出する単純なものだ)。

while read _target_item
do
    _some_proc ${_target_item}
done << __EOC__
`grep  "^${_rec_mark}"  ${_target_file}   | \
 sed "s/^${_rec_mark}//; s/[[:blank:]]//g"`
__EOC__

あるいは、全ての行を読み込み、while 文の中で処理を実行してもよい(次の処理は、上記の処理とは若干の差異がある*4のだが)。

_buf_ifs=${IFS}
IFS=
while read _line
do
    # _rec_mark から始まる場合には、その後の文字列を抽出する。
    # ( ) 内にマッチした文字列を、\1、\2 …で参照できる。
    _target_item=`sed "s/^${_rec_mark}\(.*\)[[:blank:]]*$/\1/"`
    if [ "x${_target_item}" = "x" ]; then
        :
    else
        _some_proc ${_target_item}
    fi
done < ${_target_file}
IFS=${_buf_ifs}

いずれにしても、(bash では)パイプさえ使わなければ、現在のシェルの変数に値が反映されるということで。
長くなったが、以上。

*1:今は手元に環境がないので、ボーンシェルで実行した場合に、どのようなプロセスの構造になっているのか試せない。また、この時、どうしてFDの3番を新たにオープンすることで問題が解決するのかもわからない。

*2:少なくとも、RHELでは。

*3:初めて仕事でシェルスクリプトを書いた時には、辞書的な書籍を用いて、コピー&ペーストの連続だった。この時は、何本書いてもシェルが書けるようになった気がしなかった。そんな時、たまたま出会ったのがこの本。この本のおかげでベースができ、本数を重ねるとともに、シェルが書けるようになっていく気がした。これは、本当にわかりやすくてよい本だ。

*4:この処理では、_target_item 内に空白があった場合、それが除去されない。