シェル(ただし Bourne Shell 系に限る)はお友達です。
一見すると役に立たないように思えるコマンドでも、
組み合わせ次第で複雑な処理をこなすための道具になります。
例えば行毎に ID が記載されているファイル ids
があったとしましょう。
各 ID を SHA1 ハッシュ値に置き換えたものが必要な場合、
以下のコマンドで生成することができます
(SHA1 ハッシュ値の算出には shasum を使っています):
cat ids | while read id
do
echo -n "$id" | shasum
done | cut -d ' ' -f 1 >ids.sha1
このように、行単位で何か処理を行う場合には read
を使います。
ちょっとしたことなら sed
や awk
で済ませられるのですが、
上記のように「行毎にコマンドを実行して云々」をやろうとすると無理が出てくるので、read
を使う方が自然です
(若者ならば何であれ perl
で済ませるところですが、
今回は read
を使いたいので考えないことにします)。
ところで、いきなり上記のようなコマンド列がすらすら書ける人は稀です。
ふつうの人は単純な処理(例えば指定した ID から SHA1 ハッシュ値を生成するだけ)を実現してから、
徐々に目的の処理を実装していくことになります。
筆者の場合、 read
は滅多に使ったことがないため、
まずおさらいがてら read
の使い方の確認から始めました。read
は標準入力から1行読み込み、
読み込んだ文字列を引数で指定された名前の変数に格納します
(話を単純にするため「単語」分割云々は考えないことにします)。
ということは、以下のコマンドを実行すると i
には 2
が格納されるので、1
2
の順で出力が行われるはずです。試してみましょう:
$ i='1'; echo "$i"; echo '2' | read i; echo "$i"
1
1
えっ……
3回目の echo
では 2
が出力されると予想したのですが、
何故か 1
が出力されました。一体 read
が格納した文字列はどこへ消えたのでしょうか。
さらに不可思議なことに、以下のように while
で括ると今度は期待通りの出力がなされます:
$ i='1'; echo "$i"; echo '2' | while read i; do echo "$i"; done
1
2
まさかと思って while
の後に i
の内容を出力してみると、今度は以下の結果になります:
$ i='1'; echo "$i"; echo '2' | while read i; do echo "$i"; done; echo "$i"
1
2
1
read
の挙動を考えると、while
の有無(と前後)で i
内容がころころ変動するのは不可思議です。
これはどういうことなのでしょうか。
答えはパイプ(|
)にあります。
Bash Reference Manual – 3.2.2 Pipelines
によると、パイプの挙動に関して以下の説明があります:
Each command in a pipeline is executed in its own subshell (see Command Execution Environment).
つまり、
i='1'; echo "$i"; echo '2' | read i; echo "$i"
は、敢えて書き換えるならば
i='1'; echo "$i"; echo '2' | ( read i ); echo "$i"
と同等ということです。より誇張するならば
i='1'; echo "$i"; echo '2' | ( read xxx ); echo "$i"
ということです。こう書き換えれば 2
が出力されない理由ははっきりします。read
は確かに標準入力から読み込んだ内容を i
に格納しているものの、
その i
はサブシェルの i
であり、元々のシェルのプロセスの i
とは別物です。
ですから、 read
が格納した内容はどこにも使われることがないまま捨てられていたということです。
次の問題は while
の有無(と前後)で i
の内容が変動しているように見えた点ですが、
これもパイプの定義を参照すれば理由が分かります。
Bash Reference Manual – 3.2.2 Pipelines
によるとパイプの定義は以下の通りです:
[time [-p]] [!] command1 [ [| or |&] command2 …]
ここで command1 や command2 はls
のような単純なコマンドや{ foo; bar; baz; }
のようなグループ化されたコマンドやfor
のような複合コマンドを指します。while
は複合コマンドなので、 while
全体がひとつの command として扱われています。
つまり、
i='1'; echo "$i"; echo '2' | while read i; do echo "$i"; done; echo "$i"
は、敢えて書き換えるならば
i='1'; echo "$i"; echo '2' | ( while read i; do echo "$i"; done ); echo "$i"
と同等ということです。read
が格納した値はサブシェル内、
この場合は while
の中でしか参照できないということです。
予想外のところで躓いてしまいましたが、
これでまたひとつシェルと仲良くなれました。
やりましたね。