シェルスクリプトのWhile文で代入した変数が空になっている時の対処

先日作成を進めていたシェルスクリプトのWhile文で、変数に値が代入されていない事があったので、その備忘。 これは、Whileと他のコマンドを組み合わせた時に、その組み合わせ方に問題があるとサブシェルとして扱われてしまう事からくる問題だ。

たとえば、以下のように引数で読み込んだファイルの頭に行番号を追記するシェルスクリプトを記述したとする。

#!/bin/sh
i=0
cat $1 | while read line;
do
    i=`expr $i + 1`
    TEXT=$TEXT`echo $i`":$line\n";
done
echo $TEXT

で、実際にこのスクリプトを実行すると、以下のようになる。

/work/test$ cat test.txt
testtest
testtest1
testtest2
testtest3

/work/test$ sh test.sh test.txt

/work/test$

デバックモードで実行した結果がこちら。

/work/test$ sh -x test.sh test.txt
+ i=0
+ read line
+ cat test.txt
+ expr 0 + 1
+ i=1
+ echo 1
+ TEXT=1:testtest\n
+ read line
+ expr 1 + 1
+ i=2
+ echo 2
+ TEXT=1:testtest\n2:testtest1\n
+ read line
+ expr 2 + 1
+ i=3
+ echo 3
+ TEXT=1:testtest\n2:testtest1\n3:testtest2\n
+ read line
+ expr 3 + 1
+ i=4
+ echo 4
+ TEXT=1:testtest\n2:testtest1\n3:testtest2\n4:testtest3\n
+ read line
+ expr 4 + 1
+ i=5
+ echo 5
+ TEXT=1:testtest\n2:testtest1\n3:testtest2\n4:testtest3\n5:\n
+ read line
+ echo

/work/test$

最後のechoの所を見るとわかるが、変数「$TEXT」の中身が空になっている事がわかる。 さて、これはなぜなのだろうか?

じつは、シェルスクリプトでは「|(パイプ)」を介した処理は別の処理として扱われてしまい、変数の共有が出来ないという仕様がある。 今回の場合、catでファイルの中身を開き、それをパイプでwhileに渡しているためにこのような表示になってしまうのだ。

こういう場合は、以下のように、while文の最後(doneの右)にリダイレクトを記述し、対象のファイルを参照するように記述してあげればいい。

#!/bin/sh
i=0
while read line;
do
    i=`expr $i + 1`
    TEXT=$TEXT`echo $i`":$line\n";
done<$1
echo $TEXT

これで、意図した動きをしてくれるようになるだろう。

$ sh test.sh test.txt
1:testtest
2:testtest1
3:testtest2
4:testtest3
5:

デバックモードで実行した結果がこちら。

$ sh -x test.sh test.txt
+ i=0
+ read line
+ expr 0 + 1
+ i=1
+ echo 1
+ TEXT=1:testtest\n
+ read line
+ expr 1 + 1
+ i=2
+ echo 2
+ TEXT=1:testtest\n2:testtest1\n
+ read line
+ expr 2 + 1
+ i=3
+ echo 3
+ TEXT=1:testtest\n2:testtest1\n3:testtest2\n
+ read line
+ expr 3 + 1
+ i=4
+ echo 4
+ TEXT=1:testtest\n2:testtest1\n3:testtest2\n4:testtest3\n
+ read line
+ expr 4 + 1
+ i=5
+ echo 5
+ TEXT=1:testtest\n2:testtest1\n3:testtest2\n4:testtest3\n5:\n
+ read line
+ echo 1:testtest\n2:testtest1\n3:testtest2\n4:testtest3\n5:\n
1:testtest
2:testtest1
3:testtest2
4:testtest3
5: