え、そんなこともできないの? 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
参考
- https://www.atmarkit.co.jp/bbs/phpBB/viewtopic.php?topic=15886&forum=10
- https://qiita.com/exy81/items/723184c0fcd7953d0f2c
- http://iwsttty.hatenablog.com/entry/2015/01/31/183525
所感
リダイレクトが最もマシか。