パイプラインとサブシェルの問題はシェル依存 - 拡張 POSIX シェルスクリプト Advent Calendar 2013 - ダメ出し Blog

2013-12-10(Tue) [sh][shell] [更新履歴]

拡張 POSIX シェルスクリプト Advent Calendar 2013、10日目の記事です。 そろそろネタが尽きそうです。 スクリプトの添削依頼や疑問・質問、やって欲しいネタをお待ちしております。 記事に反映できるかどうかは内容や私の実力次第ですが…。

今日はコマンドパイプラインを実行したときの変数のスコープの問題について紹介します。 「あー、パイプラインとサブシェルの問題だね。知っているよ!」という方も、 最後までお付き合いいただけると新しい発見があるかもしれません。

まずシェルスクリプトのお題として 「メンバー情報が空でないグループの数とグループ名の一覧をカンマ区切りで表示するスクリプトの作成」 を考えてみます。 実装のポイントは次の通りです。

私の手元の環境でメンバーが空でない(メンバーがいる)グループを確認したところ、 18グループが該当しました。以下、スクリプトの実行例はこの状態を前提とします。

$ getent group |grep -v ':$'
dialout:x:20:fumiyas
cdrom:x:24:fumiyas
…省略…
$ getent group |grep -v ':$' |wc -l
18

bash によるよくある間違った実装例

シェルスクリプトに不慣れな人が実装するとこんな感じの実装になるでしょうか。 典型的な間違った実装です。

nomember-buggy.bash

このスクリプトを実行してみます。

$ bash ./nomember-buggy.bash
0
NOT FOUND

期待通りに動いていないようです。何故でしょうか。 それは、POSIX やそれ以前の sh、そして bash では、 コマンドパイプライン中のすべてのコマンドがサブシェル内で実行されるためです。 サブシェル内ではメインシェルのすべての変数を継承しますが、 サブシェル内で設定・変更・削除した変数はメインシェルに反映されません。 よって、パイプライン以降のメインシェルで処理結果を参照することはできません。

キーワード「パイプ サブシェル」でググるとこの現象(と回避方法)が見つかるように、 シェルスクリプトの問題の中でも比較的よく語られている問題です。

bash による正しい実装例 (その1)

複数のコマンドの組み合せによる処理と結果をパイプラインのように、 かつパイプラインを避けて実装するには、 パイプラインの前段の処理をプロセス置換 <(コマンド) に置き換え、 パイプラインの最後段の処理をメインシェルで実行し、 プロセス置換の結果をリダイレクトで受け取る方法が一般的です。

nomember-good1.bash

このスクリプトは期待通りに動作します。

$ bash ./nomember-good1.bash
18
dialout,cdrom,floppy,audio,dip,video,plugdev,…省略…

正しく動くのはいいですが、パイプラインより美しさに欠けるように見えます。 どうでしょうか。

ksh, zsh による正しい実装例

ksh と zsh はどうでしょうか。正しい実装例を示します。

nomember-good.ksh

間違い探しをしてもらうまでもなく、最初の bash 向けの間違った実装例との違いは shebang が #!/bin/bash から #!/bin/ksh に変わっただけです。

ksh と zsh であれば、このスクリプトは期待通りに動作します。

$ ksh ./nomember-good.ksh
18
dialout,cdrom,floppy,audio,dip,video,plugdev,…省略…
$ zsh ./nomember-good.ksh
18
dialout,cdrom,floppy,audio,dip,video,plugdev,…省略…

最初の間違った例のスクリプトも期待通りに動作します。 (ksh, zsh で直接実行しているので当たり前)

$ ksh ./nomember-buggy.bash
18
dialout,cdrom,floppy,audio,dip,video,plugdev,…省略…
$ zsh ./nomember-buggy.bash
18
dialout,cdrom,floppy,audio,dip,video,plugdev,…省略…

これは何故でしょうか。 ksh と zsh では、コマンドパイプラインの最後段がメインシェルで実行されるためです。 パイプラインで複数コマンドの組み合せ処理が自然に記述できて、 大変素晴しいですね。私が bash より ksh, zsh が好きな理由の一つです。

bash による正しい実装例 (その2)

bash ファンのみなさん、お待たせいたしました! 実は bash でも ksh, zsh と同じ挙動にすることができます!! ただし bash 4.2 以降限定です。どうぞ!!!!

nomember-good2.bash

最初に新しい bash オプション lastpipe を有効にします。これだけ。 これで bash でも美しいパイプラインが書けますね。


ところで、12月25日はクリスマスな上に、 OSS 界隈で地味に活躍されているふみやすさんの誕生日ですね!
http://www.amazon.co.jp/registry/wishlist/27M7TV8CEEF6G?sort=priority

逆に、あなたの書いた OSS や Blog や Advent Calendar が気に入ったら何か送りたく なってしまうかもしれないので、プロフィールや Web サイトに あなたの Amazon 欲しいものリストの URL を貼っておいてくださいね!


私が勤める OSSTech っていう某弊社で社員募集しているようです。 人材紹介会社を介さなければ、入社後に 20万円のボーナス! 「ふみやすっていう人に紹介された」と言ってもらえると私にもボーナス!! → https://www.osstech.co.jp/recruit/


よろしければ、これまで参加した/参加予定のほかの Advent Calendar もどうぞ。