シェルスクリプトのパイプとループとサブシェルの罠

パイプの先はサブシェル

シェルスクリプトでは、パイプの先はサブシェルになるので変数のスコープが別になります。

例えば下記のようなwhile文を書くと、
#!/bin/bash

NAME="のびた"
echo $NAME

echo 1 | while read line
do
    NAME="しずか"
    echo $NAME
done

echo $NAME
出力
のびた
しずか
のびた
となってしまいます。
whileスコープ内で変数代入しても、ループを抜けるとその効果が消えてしまうのです。

何が困るかというと、while内でカウントアップしたいときに困ります。
#!/bin/bash

COUNT=0

echo -e "a\nb\nc" | while read line
do
    echo $line
    COUNT=$(expr $COUNT + 1)
done

echo $COUNT # 出力:0
ループを抜けた瞬間にカウンターはゼロになってしまいます。これは困る。

メカニズム

なぜこうなってしまうのでしょうか?
わかりやすい解説を見つけたので引用します。
それは while のせいではなくパイプ (|) のせいです。
「|」で多段に接続された各々のコマンドは別プロセスとして動きますので、そのいずれのプロセスも親である shell に変数を渡すことは出来ません。
パイプの場合、前段の実行が完了しなくても後段の実行が始まります。
 これは個々のプロセスが親である shell とは独立して動いていることを意味しますので、その while は sub shell と呼ばれる子プロセスによって実行されている訳です。
 shell 変数は一つのプロセス内でしか共有出来ませんから、その内容を他のプロセスから見ることは不可能です。ファイルに吐き出すか、もしくはパイプ以外の手段を講じるかですね。

パイプを使わなければおk

ではどう対処すればいいかというと、パイプを使わずにうまくやればいけます。
パイプを使わない例
COUNT=0

for line in a b c
do
    echo $line
    COUNT=$(expr $COUNT + 1)
done

echo $COUNT # 出力:3
一時ファイルを使う例
処理対象を一時ファイルに書きだしてから
while read line
do
...
done < file.txt
などとやるのも有効です。

参考
カテゴリ:

人気記事