コマンドパイプラインの終了コード - 拡張 POSIX シェルスクリプト Advent Calendar 2013 - ダメ出し Blog

2013-12-18(Wed) [sh][shell] [更新履歴]

拡張 POSIX シェルスクリプト Advent Calendar 2013、18日目の記事です。 今日も書く暇がなかったので軽く済ませます。すみません。

今日は Twitter で @koie さん からシェルネタを振られたので、 勝手に採用して、もう少し詳しく紹介したいと思います。

コマンドの終了コード

実行したコマンドの終了コードはシェル変数 $? で取得できます。

$ true
$ echo $?
0
$ false
$ echo $?
1
$ grep
使用法: grep [OPTION]... PATTERN [FILE]...
Try 'grep --help' for more information.
$ echo $?
2

コマンドパイプラインの場合はどうでしょうか。 次の例のように、パイプラインの最後のコマンドの終了コードが採用されます。

$ true | true
$ echo $?
0
$ true | false
$ echo $?
1
$ false | true
$ echo $?
0
$ false | false
$ echo $?
1
$ sh -c 'exit 11' | sh -c 'exit 22' | sh -c 'exit 33'
$ echo $?
33

パイプラインの全コマンドの終了コード

bash, zsh 依存となりますが、コマンドパイプラインのすべてのコマンドの終了コードを得るためのシェル変数が用意されています。

bash では配列型のシェル変数 $PIPESTATUS に各コマンドの終了コードが入ります。

$ sh -c 'exit 11' | sh -c 'exit 22' | sh -c 'exit 33'
$ echo "${PIPESTATUS[@]}"
11 22 33

zsh では配列型のシェル変数 $pipestatus に各コマンドの終了コードが入ります。

% sh -c 'exit 11' | sh -c 'exit 22' | sh -c 'exit 33'
% echo "${pipestatus[@]}"
11 22 33

ksh は…、そのようなシェル編集や手段は用意されていません。残念。 うまい方法を思い付かなかったのですが、 こんな風にすれば「いずれかのコマンドが失敗したら死ぬ」程度なら実現できます。 いまひとつですね、はい…。

#!/bin/ksh

function pipe_run {
  "$@"
  typeset status="$?"
  [[ $status -ne 0 ]] && kill "$$"
  return 0
}

pipe_run cmd1 | pipe_run cmd2 | pipe_run cmd3

すべての終了コードの検査

$PIPESTATUS (bash), $pipestatus (zsh) は、パイプラインでない単発のコマンド実行でも更新されます。 これが厄介の元で、パイプラインの複数のコマンド終了コードを順次検査するには少し工夫が必要になります。

次の bash の例のように、コマンドパイプライン後の echo "${PIPESTATUS[0]}" の実行で $PIPESTATUS の内容は echo コマンドの終了コード 0 だけが含まれる状態になってしまいます。

$ sh -c 'exit 11' | sh -c 'exit 22' | sh -c 'exit 33'
$ echo "${PIPESTATUS[0]}"
11
$ echo "${PIPESTATUS[1]}"

$ echo "${PIPESTATUS[2]}"

$

終了コード値の上書きを避けるため、 コマンドパイプライン直後に別の配列変数にコピーすれば問題ありません。

$ sh -c 'exit 11' | sh -c 'exit 22' | sh -c 'exit 33'
$ status=("${PIPESTATUS[@]}")
$ echo "${status[0]}"
11
$ echo "${status[1]}"
22
$ echo "${status[2]}"
33

パイプライン実行の度にコピーして検査するコードを書くのはあまり効率的ではありませんね。 そこでパイプライン後の全コマンドの終了コードを検査するシェル関数を考えてみました。

次の例のように検査処理をシェル関数で実装し、最初に $PIPESTATUS (bash), $pipestatus (zsh) をコピーしてから順次検査するとよさそうです。

#!/bin/bash
# or
#!/bin/zsh

pipestatus() {
  local _status="${PIPESTATUS[*]-}${pipestatus[*]-}"
  [[ ${_status//0 /} == 0 ]]
  return $?
}

foo-command |bar-command |xxx-command
if pipestatus; then
  echo OK
else
  echo NG
fi

この例中の pipestatus() 関数は、パイプラインの全コマンドの終了コードが 0 であれば 0 (真)を、そうでなければ 1 (偽) を返すようになっています。

zsh と bash の pipefail シェルオプション

zsh 5.0 以降と bash 3.0 以降には pipefail というシェルオプションが実装されていて、 これを有効にすると、 パイプライン中のコマンドのうち 0 を返さなかった最右辺のコマンドの終了コードが $? に設定されるようになります。

$ set -o pipefail
$ sh -c 'exit 11' | sh -c 'exit 22' | sh -c 'exit 33'
$ echo $?
33
$ sh -c 'exit 11' | sh -c 'exit 22' | true
$ echo $?
22
$ true | sh -c 'exit 22' | true
$ echo $?
22
$ sh -c 'exit 11' | true | true
$ echo $?
11

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

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


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

バツイチでアラフォーでほとんど諦めていますが、新たなパートナーも募集中です。 ちゃんと愛情表現してくれて精神的に自立している女性がいいです。お友達から始めましょう。 → https://twitter.com/satoh_fumiyasu


よろしければ、過去の Advent Calendar もどうぞ。