「シェルでテキストファイルから1行ずつ読み込む」シリーズの最終回。これまで、次のような内容で書いてきた。
- 「シェルで1行ずつファイルから読み込む - あしのあしあと」 ⇒ テキストファイルから1行ずつ読み込むというありふれた処理ではまったこと。具体的には“パイプ+while read”
- 「絵で見てわかるファイルディスクリプタ・パイプ・リダイレクト - あしのあしあと」 ⇒ ファイルディスクリプタ、パイプ、リダイレクトのおさらい。せっかくなので、絵をつかって説明
- 「while・read・exec 再入門 - あしのあしあと」 ⇒ テキストファイルから1行ずつ読み込む際に必要となる while 文、read コマンド、exec コマンドのおさらい。“パイプ+read”の罠w
で、ようやくスタートの、このスクリプトに戻る。
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の世界](https://images-fe.ssl-images-amazon.com/images/I/51gWeaXVBFL._SL160_.jpg)
入門UNIXシェルプログラミング―シェルの基礎から学ぶUNIXの世界
- 作者: ブルース・ブリン,Bruce Blinn,山下哲典
- 出版社/メーカー: ソフトバンククリエイティブ
- 発売日: 2003/02
- メディア: 単行本
- 購入: 18人 クリック: 331回
- この商品を含むブログ (64件) を見る
最後に、(今回は 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番を新たにオープンすることで問題が解決するのかもわからない。
*3:初めて仕事でシェルスクリプトを書いた時には、辞書的な書籍を用いて、コピー&ペーストの連続だった。この時は、何本書いてもシェルが書けるようになった気がしなかった。そんな時、たまたま出会ったのがこの本。この本のおかげでベースができ、本数を重ねるとともに、シェルが書けるようになっていく気がした。これは、本当にわかりやすくてよい本だ。
*4:この処理では、_target_item 内に空白があった場合、それが除去されない。