シェルで1行ずつファイルから読み込む

スミッコ基盤チームのスミッコで、こっそりとシェルを書いている。最近、仕事で実装することはほとんどなかったため、なかなかにして楽しい。営業利益率は、いったん考えないことにする。
で、(今さら)シェルスクリプトを使って、ファイルから1行ずつテキストを読み込み処理したくなった。まぁよくある話だろう。久々のシェル、どうやるのか覚えていなかったため、ちょいと調べたら、次のサンプルが出てきた。

BUFIFS=$IFS
IFS=

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

IFS=$BUFIFS

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


これは、とてもお行儀のよい、正しいスクリプトだ。でも最初見たときは、「何コレ?そんなバカな。ファイルディスクリプタの3番?やめやめ、read コマンドをループで使えばよいのね。」と思ってしまった。時間がない(と勝手に思っている)時はこういう逃げの思考がすぐに働く。そして結局、時間をとられることになる。
で、書いてしまったダメなスクリプトが次*1

#### うまく動作しない!!

# 処理中にエラーが発生した項目を格納する変数
proc_fail_items=""

# _target_file から _rec_mark で始まる行を取得し、
# 定義されている項目に対して、処理を実行する。

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

if [ "x${proc_fail_items}" = "x" ]; then
    exit ${NORMAL_END}
else
    proc_fail_items=`echo ${proc_fail_items} | sed "s/^,//"`
    :  # ここで、proc_fail_items をログに出力する。
    exit ${ERROR_END}
fi

_some_proc (){
    _item=${1}

    :  # ここで、_item に対する処理を実行する。
    if [ ${?} -eq 0 ]; then
        return ${NORMAL_END}
    else
        proc_fail_items="${proc_fail_items},${_item}"
        return ${ERROR_END}
    fi
}


このスクリプトは、処理に失敗した項目を、変数 proc_fail_items に格納しておいて、ループ中に1回でも処理が失敗していたら(proc_fail_items が空でなかったら)エラーで終了しようと目論んだモノ。でもこの変数は、while文の内外で共有されない。つまり、18行目 proc_fail_items の値は、6行目のままである。これは、パイプラインの処理が別プロセスで実行される(そしてエクスポートされていない変数が引き継がれない)ことを考えていないダメな例なのだ。ようやくつまずいた。


もっと一般的には、パイプを使わなくっても、while 〜 done が別プロセスになることがある*2らしい。従って、先に引用したスクリプトが最も安全となる。今後、3回くらいのエントリに分けて、先の正しいスクリプトを説明してみようと思う。

*1:変数 NORMAL_END、ERROR_END には、別途値が入っているものとする。

*2:ちょっと試したところbashは大丈夫そう。