2011/11/26
shell での while ループにおける問題
シェルスクリプトでコマンドの出力をループ処理する場合は
パイプ | を使用するのが一般的な手法だと思うが、
パイプ | を使うとループが別なプロセスとして実行されるので
ループ内部で設定したシェル変数がループ外部で参照できないという問題があり、
(存在するとすれば) shell script 業界では割と FAQ 的な問題だ。
何が問題かと言って posix で定義されていないから
シェルの実装に依存しているのが一番の問題だったりするのだろう。
この問題に対しては、シェルビルトインの exec (1) を利用して
ディスクリプタを複製して while ループの入力を標準入力にしてしまうのが
一番汎用的で柔軟性のある対応だと思うのだが、
シェルを愛する面々は
皆同じ様な苦労をしているのだ
と改めて認識した。
当初は一時ファイルを作成する方法を考えついたのだが、
自分的にはなるべく一時ファイルを使いたくなかったので
ファイルから入力する部分をバッククォートによるコマンド実行に置き換え等、
色々と試行錯誤した結果ヒアドキュメントを使う方法に落ち着いた。
# これでは正常に動作しなかった exec 3<&0 0<`${command}` while read line :
勿論一時ファイルを作成しても良いのだが、 ヒアドキュメントを利用する事で一時ファイルが不要になる分だけ 処理的に美しいかなと思う (完全に好みの問題)。
1#!/bin/sh 2# コマンドの出力をバッククォートしてヒアドキュメントとして使用する 3 4command="/path/to/command" 5count=0 6 7# exec 3<&0: 標準入力 (FD0) を FD3 に複製 8# exec 0<<EOF: ヒアドキュメントを標準入力として使用 9exec 3<&0 0<<EOF 10`${command}` 11EOF 12 13while read line 14do 15 : 16 count=`expr ${count} + 1` 17done 18 19# exec 0<&3: FD3 に複製した標準入力 (FD0) を復帰 20# exec 3<&-: FD3 をクローズ</i> 21exec 0<&3 3<&- 22 23echo ${count}
もう一点は子プロセスで親プロセスの変数値を変更する方法。
こちらはプロセスが別になってしまうとファイルを使うしかないので、
同じプロセス内で関数呼び出しとして対応する方法が汎用的ではないだろうか。
1#!/bin/sh 2# eval を利用して呼び元の変数を設定する 3 4default="デフォルト値" 5 6sub() 7{ 8 9 local _val 10 11 : 12 _val="設定したい値" 13 14 eval "${1}='${_val:-${default}}'" 15 16} 17 18value="元の値" 19 20sub value 21 22echo ${value}
ちなみに上のリンク元にある parent.sh の中の処理で
${tmp_file} の内容を for ループで eval しているが、
これはシェルビルトインの . もしくは source を利用して
. ${tmp_file} としても同一の結果が得られるので楽だと思う。
1#!/bin/sh 2 3g1="aa" 4g2="bb" 5 6tmp_file=`mktemp /tmp/ps.XXXXXX` 7export tmp_file 8 9./child.sh # child.sh で、上で設定した変数 g1, g2 の値を変更する 10 11# for v in `cat $tmp_file`; do 12# eval $v 13# done 14# ${tmp_file} の内容は name='value' 形式が保証されているので 15# ファイル自体を `.' コマンドで読み込むだけで 16# 行毎に eval を実行しなくてもシェル変数に代入された値が利用できる 17 18if [ -f ${tmp_file} ] 19then 20 . ${fmp_file} 21 rm -f ${tmp_file} 22fi 23 24echo $g1 #=> cc に変更された 25echo $g2 #=> dd に変更されたブログに直接コメントしたかったのだが、 はてなのアカウントが無いので申し訳ないがここで意見させて頂く。