Software-Defined どさにっき 〜2015年8月中旬〜

by やまや
<< = >>

2015年8月20日(木)

サブシェルはイヤだ

_ たとえば、ファイルの中身を逆順で出力する。要は tac。

cat file | while IFS= read -r line; do
    r="$line
$r"
done
echo -n "$r"

_ これ、うまく動きそうだけど、動かない。なぜなら、| によって while ループがサブシェルで実行されるため、その中で定義した変数 $r をループの外で参照できないから。

_ この場合、こうすれば回避できる。

while IFS= read -r line; do
    r="$line
$r"
done < file
echo -n "$r"

_ | が原因なんだから、それを使わずに入力リダイレクトしてやればよい。ただ、この程度ならいいけど、while ループの行数が長くなってしまうと、< file の部分が遠く離れてしまって可読性が落ちる。これを何とかしたいんじゃ。

_ いきなり本題から逸れるが、リダイレクトはコマンドラインの末尾に書かなければならないというルールはない。以下すべて文法的に正しく、同じ結果を返す。

grep hoge < filename
grep < filename hoge
< filename grep hoge

_ が、残念ながら、以下は不可。

< file while read f; do
    ...
done

_ { ... } でグルーピングしてやっても不可。

< file { while read f; do
    ...
done; }

_ 理不尽なことに、{ ... } ではなく、( ... ) なら可。しかし、() は {} と違ってサブシェルを生成するので | を使うのと変わらず今回の要件を満たさない。

< file ( while read f; do
    ...
done )

_ どうしたらいいのかと小一時間考えてひらめいた。

exec < file
while read f; do
   ...
done

_ exec で入力を切り替えてやればよい。ただし、スコープが while ループだけでなくスクリプト全体なので、ループを脱出した後の処理がおかしくなることがある。それが困るなら、標準入力を一時的に別の記述子に退避して、後で戻してやればよい。

exec 3<&0
exec < file
while read f; do
   ...
done
exec 0<&3

_ …うーん、これ、可読性はむしろ下がってるよね。ふつーに while read f; do ...; done < file の方がずっとわかりやすい。ボツ。

_ ただ、標準入力を手でつなぎかえるというアイデアは悪くない。cat file | ... なら ... < file で置き替えられるけど、| の前が cat じゃないコマンドであれば(あるいは cat でも引数が複数ファイルだったりすると)簡単に置き替えることはできない。そういう場合に while ループをサブシェルにしないためにはこうすればよい。

p=fifo.$$
mkfifo $p           # 名前つきパイプを生成
なんかコマンド > $p &       # 名前つきパイプに書いて、
while read f; do
    ...
done < $p           # 名前つきパイプから読む
rm $p
wait                # バックグラウンドプロセスの終了待ち(いらないかも)

_ なんかコマンド と while ループを | で直接つなぐのではなく、名前付きパイプを使って手でつないでやる。exec と mkfifo という違いはあるけど、標準入力を切り替えてつなぎかえるというアイデア自体は同じ。可読性が悪いというのも同じだけど。

_ なお、厳密にいうと、while ループがサブシェルになるのではなく、| のうしろがサブシェルになるので、以下のように while ループとその後続の処理をまとめて {} で括ってしまえば、ループの中で設定した変数を while の外、{} の内側にかぎり持ち出すことはできる。{ ... } の外に出ると参照できなくなる。最悪、スクリプトの最後まで {} で括ってしまえばなんとかならんこともない。

なんかコマンド | {
    while read f; do
       ...
    done
    ...
}

_ もうひとつ手っ取り早い解決法として、ksh を使うというのもある。ksh は | のうしろがサブシェルではなくカレントシェルで実行されるので、こんなことに頭を悩ます必要がない。

date +'%Y %m %d' | read y m d

_ ksh ならこれで $y $m $d に年月日を一度に代入できる。ksh 以外のシェルでは不可。なお、OpenBSD や NetBSD の /bin/ksh は 本物の Korn shell ではなく、pdksh というある意味劣化クローンなので、このようなことはできない。


<< = >>
やまや