コマンドパイプラインの終了コード - 拡張 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万円のボーナス! 「ふみやすっていう人に紹介された」と言ってもらえると私にもボーナス!! → https://www.osstech.co.jp/recruit/


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