bashのwhileループ内の変数をループ外で使う
化石みたいな問題。
よく知られていることだが、下記のような各行を足し算するシェルスクリプトを書くと、最後の結果が0になってびっくりする。
$ cat get_nothing.sh __calculate_total() { local total=0 fpath=$1 cat $fpath | while read line; do total=$(( $total + ${line:-0} )) done echo "Total reaches $total" } __calculate_total $@
$ cat numbers.txt 101 -40 4566 32 $ bash ./get_nothing.sh numbers.txt Total reaches 0
これはwhile等の制御文だったかパイプの先だったか忘れたが、bashが別プロセス起こしちゃうんで、戻ってくるときにせっかく変更した変数の内容が破棄されるかららしい。
解決方法としては、UNIX FAQとかにexecでファイルディスクリプタを一時的に変更して云々な話が書かれていたりするが、現在の超高度に発達したSIerの開発現場において、そんなマイナーなテク使われても誰も読めなくて迷惑するのでやめてほしい。あと、一時ファイルにwhileループごとに値を書き出すのもやめて。コードレビューする人の 疲労感を無駄に積み増すんじゃない。
一番よい解決方法は、bashやめてPerlなりPythonなりまともな言語で書く。
でも、現在の超高度に発達したSIerの開発現場では、言語一つ変更するのに、周囲とのネゴとか試験仕様書の更新とか、誰も幸せにならないエネルギーが必要なので、シェルスクリプトの範囲内で何とかしてみる方法を考えてみた。
たぶんこう書くと16倍(当社比)くらいすっきり感が出ると思う。
$ cat goes_well.sh __sum_up() { local line total=0 while read line; do total=$(( $total + ${line:-0} )) done echo $total return 0 } main() { local total=0 fpath=$1 total=`cat $fpath | __sum_up` echo "Total reaches $total" return 0 }
つまり、while部分を別関数に切り出す。切り出しただけなので、行数はそれほど変わらない。
ファイル読み込み中にエラーが出た場合の処理を書きたい場合も容易に拡張できる。
$ cat goes_well2.sh __sum_up() { local line total=0 while read line; do if [[ "$line" =~ ^-?[0-9]+$ ]]; then total=$(( $total + ${line} )) else return 1 fi done echo $total return 0 } main() { local rc total=0 fpath=$1 # Todo: check the fpath is valid. total=`cat $fpath 2>/dev/null | __sum_up` rc=$? if [ $rc -ne 0 ]; then return $rc fi echo "Total reaches $total" return 0 } main $@
$ bash ./goes_well2.sh numbers.txt ;echo "rc=$?" Total reaches 4659 rc=0 $ cat numbers2.txt 101 -40 NOT NUMBER 4566 32 $ bash ./calc_total.sh numbers2.txt ;echo "rc=$?" rc=1
欠点は、"total="のパイプより前のコマンドでエラーが出ても$?が0になってしまう点で、そこのところはあまりいい書き方思いつかないので、まじめに直前にファイル存在チェック書いたほうが読みやすくてよいと思う。
まあでも、やはりbashを使わないのが正解。
ちなみにfor 文で
for nb in `cat numbers.txt`; do ..(足し算).. done echo $total
などとしても動くが、numbers.txt のファイルの中身が文字列としてメモリに全展開されるので、ファイルサイズが小さいことが保証されていない限りお勧めしない。