シェルスクリプトで SIGPIPE を無視する方法です。特に set -o pipefail
を使用しているときに便利でしょう。ちなみに pipefail は POSIX.1-2024 で標準化されたので、もはや bash の拡張機能ではありません。以下のコードは POSIX 準拠の範囲で書いており、どの環境でも動作します。
igpipe() {
# set -e (errexit) が有効でも無効でも正しく動作するようにするため
case $- in
*e*) set +e; (set -e; "$@"); set -e -- $? ;;
*) ("$@"); set -- $? ;;
esac
# 終了ステータスが 128 以上でない場合はシグナルではないので調べる必要はない
[ "$1" -ge 128 ] || return "$1"
# 「kill -l 終了ステータス」で対応するシグナル名を取得可能
# 補足: POSIX では SIGPIPE が 141 (128 + 13) である保証はなく
# 実際に ksh、yash では 141 ではない
[ "$(kill -l "$1")" = PIPE ] || return "$1"
}
set -o pipefail
# SIGPIPE が発生する
seq 1000000 | head -n 10
echo "$? : ${PIPESTATUS[@]}" # => 141 : 141 0
# SIGPIPE を無視する
igpipe seq 1000000 | head -n 10
echo "$? : ${PIPESTATUS[@]}" # => 0 : 0 0
補足 SIGPIPE はパイプラインを使ったコードで、データを出力しているコマンドよりも、出力先のコマンドが早く終了することで、データの書き込み先がなくなったことを知らせるシグナルです。
バッドプラクティス(ダメな方法)
SIGPIPE を無視しようとして、以下のようなコードを書くのはよくありません。
SIGPIPE(?) を無視する良くない方法
seq 1000000 | head -n 10 || true
{ seq 1000000 || true; } | head -n 10
たしかにこの方法で SIGPIPE を無視することができますが、その他のエラーまで無視してしまいます。その他の方法として一時的に pipefail を無効にする方法も考えられますが、当然 pipefail 本来の機能(途中のコマンドのエラーを検出する)まで機能しなくなってしまいます。
補足 pipefail とは?
pipefail はパイプラインのエラーを検出するときに便利な機能です。デフォルト(pipefail が無効)だと、パイプライン全体の終了ステータスは、パイプラインの最後のコマンドのものとなってしまい、途中のエラーを検出することができません。
set -o pipefail
# pipefail が無効だと cmd1 や cmd2 でエラーになっても、
# cmd3 が正常終了してしまうとパイプライン全体は正常終了扱いになる
if cmd1 | cmd2 | cmd3; then
echo すべてのコマンドが正常終了しました
else
echo いずれかのコマンドでエラーが発生しました
fi
上記のコードは POSIX 準拠 (POSIX.1-2024) のコードです。bash と mksh の PIPESTATUS
配列や zsh の pipestatus
配列を使って調べる方法もありますが、PIPESTATUS
や pipestatus
は POSIX 準拠ではないため非推奨とします。
PIEPSTATUS を使って調べる方法の例(POSIX 準拠ではないため非推奨)
cmd1 | cmd2 | cmd3
if (( (${PIPESTATUS[@]/#/+}) == 0 )); then
echo すべてのコマンドが正常終了しました
else
echo いずれかのコマンドでエラーが発生しました
fi
なぜこのコード (( (${PIPESTATUS[@]/#/+}) == 0 ))
で動くのかというと、${var[@]/#pattern/str}
は配列の各要素の先頭から、パターンにマッチする文字列を置き換える変数展開で、ここではパターンを省略しているため各要素の先頭に + を追加するという意味になるからです。つまりこのコードは PIPESTATUS
配列のすべての要素を足しています。
(exit 1) | (exit 2) | (exit 3)
echo $(( ${PIPESTATUS[@]/#/+} )) # => 6
# ↑ echo $(( +1 +2 +3 )) を実行する