ここに書くTipsは、そもそもパイプとwhileにまつわる問題を経験したことないと何を言ってるか分からないと思うのでまず経緯から書く。
よくやる失敗例
シェルスクリプトを書いていると以下の様にwhile
とread
を組み合わせたコードを書くことがよくあります。
find -type f | while read -r f; do
echo "$f" #ここのコードは色々
done
これはコマンドの標準出力を1行ずつ f という変数に読み込んで何らかの処理を行うってやつです。
whileの中でやることがファイル操作などの一般的なことならこれで全然問題ないんですが、実行中のシェル環境に対する処理(具体的には変数の設定など)を行おうとすると期待通りにいってくれなくなります。例えばこんな感じ↓
# findで見つけたファイル名をfilesという配列変数に詰め込みたい
files=()
find -type f | while read -r f; do
files+=("$f")
done
echo "${files[@]}" # 確かにfilesに値を入れた筈なのに空が出力される??
これはパイプで繋げると後ろのwhileがサブシェルで実行されてしまうために起こる現象です。噛み砕いて説明すると…
- 現在findを実行中のbashとは別に、パイプの後ろでもう一つのbashが起動されてその子プロセスのbash上でfiles変数をセットしていることになります。
- すると当然、子プロセスが終了(whileの全てが終了)した時点で子bashプロセス上に設定されたfiles変数の中身も消え去ります。
- 親プロセスのfilesは最初に初期化したままなので空が出力されることになります。
解決編
これ実は上手い解決策に結構悩みます、パイプしたいのにパイプすると駄目なんだから…あーもー。となって、んで一時ファイルを作ってから <tmpfile とかで標準入力に流しこでとりあえず解決しちゃったりするわけです。ですがそうすると一時ファイルの後処理をしなくちゃいけないし見た目も汚いしでやっぱ気持ち悪いです。
そこで登場するのが Process Substitution という機能です。よく2つのコマンドの標準出力のdiffを取るとかの例で紹介されてるやつです。この機能が今回のwhile問題にも活用出来ます。
まずは上述の失敗例を解決したコードを書いてから説明します。
# findで見つけたファイル名をfilesという配列変数に詰め込みたい
files=()
while read -r f; do
files+=("$f")
done < <(find -type f)
echo "${files[@]}" # 期待通りファイル名の一覧が出力される!
まぁ見たまんまですが、結構これが思いつかないんですよね(^^;
ただし1つだけ注意点があって、標準入力に突っ込む< <(...)
のところですが<
と<(...)
の間にスペースを空けてるのが重要です。この空白が無いと<<
がヒアドキュメントの開始と解釈されてエラーになってしまいます。それさえ気をつければOKです。
まぁ、シェルスクリプト書きまくってる人でもなければあまり使うケースは無いかもしれませんが、覚えておくと便利です。