やってみる

アウトプットすべく己を導くためのブログ。その試行錯誤すらたれ流す。

bashのwhile内にある変数を外から参照する方法

 え、そんなこともできないの? bash糞仕様。

問題

 ファイルの行数を数えたい。しかしwhile内の変数countをループ外で参照したらとカウントされていなかった。

#!/bin/bash
Run() {
    local count=0
    seq 10 20 > num.txt
    cat num.txt | while read line; do
        echo "$count: $line"
        count=$((count + 1))
        [ 5 -lt $count  ] && { break; }
    done
    echo "count=${count}"
}
Run
項目
期待値 count=6
実際値 count=0

 whileの外側で変数定義してもダメ。上記コードには書いてないが関数外でグローバル変数宣言してもダメだった。なぜ?

原因

 スコープが違う。パイプ(|)を使うとその部分はサブシェル(子シェル)になってしまい変数のスコープ(?)が別になる。スコープというより別プロセスだから変数のメモリ領域も別になる。そのせいでwhileの内と外で変数を共有できない。

 つまり以下。

# メインシェルのメモリ空間 確保
count=0

# サブシェルのメモリ空間 確保
cat num.txt | while read line; do
    count=$((count + 1)) # 1..6
done
# サブシェルのメモリ空間 解放

echo "count=${count}" # 0
# メインシェルのメモリ空間 解放

解法

解法 コード メリット デメリット
1 同一プロセス内に収める (while do done; 変数参照;) パイプが使える done以降が見にくい
2 一時ファイルにしてリダイレクトする while do done < 一時ファイル done < 一時ファイル以降はいつも通り 一時ファイルが必要
3 ヒアドキュメント while do done << EOS 一時ファイル不要 データとコードが分離できない

 まるで三すくみのような関係。一長一短。

1. 同一プロセス内に収める

#!/bin/bash
Run() {
    local count=0
    seq 10 20 > num.txt
    cat num.txt | ( while read line; do
        echo "$count: $line"
        count=$((count + 1))
        [ 5 -lt $count  ] && { break; }
    done; echo "count=${count}" )
}
Run

 メリットはパイプが使えること。デメリットはdone以降の記述が面倒かつ読みにくいこと。

 done以降の処理が多いときは避けたい。関数化することで簡略化する案もある。(while ... done; Func ${count};) みたいに。

2. 一時ファイルにしてリダイレクトする

#!/bin/bash
Run() {
    local count=0
    seq 10 20 > num.txt
    while read line; do
        echo "$count: $line"
        count=$((count + 1))
        [ 5 -lt $count  ] && { break; }
    done < num.txt
    echo "count=${count}"
}
Run

 メリットはdone以降の記述がいつもどおりなこと。デメリットはリダイレクトする一時ファイルが必要なこと。

 一時ファイルを作成していいならこれ。最も可読性の良い書き方。

3. ヒアドキュメント

#!/bin/bash
Run() {
    local count=0
    while read line; do
        echo "$count: $line"
        count=$((count + 1))
        [ 5 -lt $count  ] && { break; }
    done << EOS
10
11
12
13
14
15
16
17
18
19
20
EOS
    echo "count=${count}"
}
Run

 メリットは一時ファイル不要。デメリットはデータとコードが分離できないこと。

 可読性も保守性も低い。実用的ではなさそう。データが固定かつ少数のときはアリ。

番外編: while関数化

 他の方法としてwhile文を関数化してしまう案もある。が、以下の点で好ましくない。

  • 結果をechoによる標準出力で行う仕様上、echoの使用が制限される
  • whileの処理が複雑だときれいに分離するのが難しい
#!/bin/bash
MyWhile() {
    local count=0
    seq 10 20 > num.txt
    cat num.txt | while read line; do
        count=$((count + 1))
        [ 5 -lt $count  ] && { echo "$count"; break; }
    done
}
Run() {
    local count=$(MyWhile)
    echo $count
}
Run

対象環境

  • Raspbierry pi 3 Model B+
  • Raspbian stretch 9.0 2018-11-13
  • bash 4.4.12
$ uname -a
Linux raspberrypi 4.14.71-v7+ #1145 SMP Fri Sep 21 15:38:35 BST 2018 armv7l GNU/Linux

参考

所感

 リダイレクトが最もマシか。

 それにしても、これはひどい。無駄に複雑になり読みにくくなる。pythonなど他言語では考えられない糞仕様。