2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

シェルスクリプトで SIGPIPE を無視する方法

Last updated at Posted at 2024-08-17

シェルスクリプトで SIGPIPE を無視する方法です。特に set -o pipefail を使用しているときに便利でしょう。ちなみに pipefail は POSIX.1-2024 で標準化されたので、もはや bash の拡張機能ではありません。以下のコードは POSIX 準拠の範囲で書いており、どの環境でも動作します。

SIGPIPE を無視するためのシェル関数
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 の典型的な使い方
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 配列を使って調べる方法もありますが、PIPESTATUSpipestatus は 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 )) を実行する
2
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?