コマンドの実行結果を”while read”で1行ずつ読み込んでいく時の注意

Bash,CentOS,Linux,Ubuntu,Zsh

はじめに

よく知られていることですが、
あるコマンドの実行結果をパイプで渡し、
while read 命令で1行ずつ読み込んで処理する場合に
注意しないといけないことがあります。

多くのエンジニアが一度はハマったことがあるはずです。

while read コマンドを使った場合の 「注意事項」「解決方法」 を紹介します。

検証環境

$ uname -moi
x86_64 x86_64 GNU/Linux

$ head -n 2 /etc/os-release
NAME="Ubuntu"
VERSION="21.04 (Hirsute Hippo)"

$ bash -version | head -n 1
GNU bash, バージョン 5.1.4(1)-release (x86_64-pc-linux-gnu)

seq 1 15 の実行結果から3の倍数を数を求める

1〜15の数の中から、 3の倍数 を求めてみます。

COUNT という変数を用意し、3の倍数が見つかるたびに COUNT 変数をインクリメントします。

最後に COUNT 変数を出力すれば、 3の倍数の数がわかるというわけです。

$ COUNT=0

$ seq 1 15 | while read -r NUM; do
  ((NUM % 3 == 0)) && ((COUNT++))
done

$ echo $COUNT
0

実行結果は 0 となってしまいました。

これはなぜでしょう?

パイプの後方はサブプロセスとなる

これはパイプの後方がサブプロセスとなります。

親プロセスで行った変数の変更は、サブプロセスから参照できます が、
サブプロセスで行った変数の変更は、親プロセスから参照できません

先程のループ処理で COUNT 変数の内容を出力してみましょう。

$ COUNT=0

$ seq 1 15 | while read -r NUM; do
  ((NUM % 3 == 0)) && ((COUNT++))
   echo $COUNT
done

0
0
1
1
1
2
2
2
3
3
3
4
4
4
5

$ echo $COUNT
0

このように、ループ内では変数のカウントアップは行えているのですが、その変更は親プロセスからは見えません。

解決方法1

1つ目の解決方法として、変数をパイプの後ろで「定義」「インクリメント」「出力」するという方法があります。

$ seq 1 15 | {
  # 定義
  COUNT=0
  # インクリメント
  while read -r NUM; do
    ((NUM % 3 == 0)) && ((COUNT++))
  done
  # 出力
  echo $COUNT
}

5

正しく出力できました。

ただし、この方法ではこの後に COUNT 変数を利用することができません。

解決方法2

パイプを使わなければよい ので、別の方法で while read コマンドに標準入力を渡してやります。

$ COUNT=0

$ while read -r NUM; do
  ((NUM % 3 == 0)) && ((COUNT++))
done < <(seq 1 15)

$ echo $COUNT
5

この方法であれば、 COUNT 変数を後続で利用することができます。

記述方法には注意が必要です。

< <(seq 1 15) のように < の間にスペースが必要です。 <<(seq 1 15) のようにスペースなしで詰めてはいけません 。

ひとこと

はじめに書いたとおり、この問題は多くのエンジニアが一度はハマる問題でよく知られていますが、取り上げてみました。