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
参考
- 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
所感
リダイレクトが最もマシか。