Bashで”while read”ループ内で変更した変数がループ外に反映されないときの対処

2023-03-27Bash,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 5 の実行結果を合計してみる

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

SUM という変数を用意し、1〜5の値を加算します。

最後に SUM 変数を出力すれば、 1〜5の合計値がわかります。

$ SUM=0

$ seq 1 5 | while read -r NUM; do
  ((SUM+=$NUM))
done

$ echo $SUM
0

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

これはなぜでしょう?

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

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

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

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

$ SUM=0

$ seq 1 5 | while read -r NUM; do
  ((SUM+=$NUM))
   echo $SUM
done

1
3
6
10
15

$ echo $SUM
0

このように、ループ内では変数への加算は行えているのですが、その変更は親プロセスからは見えません。

解決方法 1

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

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

15

正しく出力できました。

ただし、この方法では{...} ブロックを抜けた後に SUM 変数を利用することができません。

解決方法 2

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

$ SUM=0

$ while read -r NUM; do
  ((SUM+=$NUM))
done < <(seq 1 5)

$ echo $SUM
15

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

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

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

解決方法 3

Bash 4.2 から導入された lastpipe というオプションを使う方法があります。

shopt -s lastpipe というコマンドを実行しオプションを有効にできます。

有効にすると、パイプで連結されたコマンドのうち一番最後のものを現在のシェルプロセスで実行します。

ここでは while コマンドがカレントシェルで実行されることとなります。

以下のようなスクリプトを作り、実行すると 5 という結果が表示されます。

ただし注意が必要です。インタラクティブなシェル操作ではうまく SUM 変数値はカウントアップされません。シェルスクリプトを作成して実行する必要があります

# こんなシェルスクリプトが用意されていることを確認し
$ cat main.sh
#!/usr/bin/env bash

SUM=0

shopt -s lastpipe

seq 1 5 \
  | while read -r NUM; do
    ((SUM+=$NUM))
  done

echo $SUM

# 実行してみる
$ ./main.sh
15

シェルスクリプトを作らずに、インタラクティブシェル内で shopt -s lastpipe を有効にしたい場合には、 set +m オプションも実行しておきます。
( set +m を実行することでバックグラウンドジョブをフォアグラウンドに戻せなくなるなどの弊害があるのでおすすめはしません。 )

$ SUM=0

$ shopt -s lastpipe

$ set +m

$ seq 1 5 \
  | while read -r NUM; do
    ((SUM += NUM))
  done

$ echo $SUM
15

ひとこと

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

2023-03-27Bash,CentOS,Linux,Ubuntu,Zsh