できない.dev

bash で set -e を書いてもエラーでスクリプトが止まらない

set -e には仕様上の適用外がある。
条件文脈で実行されたコマンド、パイプラインの途中、local や export と同時に書いた代入では終了ステータスが無視される。
止めたい箇所を明示的に判定する。

公開:

要約

set -eerrexit)は「あらゆる失敗で止まる」設定ではない。終了ステータスが何かの判定に使われる位置では、失敗しても止まらないと決められている。
次のスクリプトは A から D まで全部表示される。

set -e
f() { false; echo "in-f-after-false"; }
if f; then :; fi
echo "A"
false || echo "B"
! false
echo "C"
false | true
echo "D"

条件文脈で呼ばれた f は、中の false でも止まらず次の echo まで進む。
関数の内側も丸ごと適用外になる点が、特に見落としやすい。

よくある原因

  1. 条件文脈: ifwhileuntil の条件部分、&&|| の左側は、失敗を「偽」として使う場所なので set -e の対象外である。
  2. ! による否定: ! cmd は失敗を期待する書き方なので、そこも対象外になる。
  3. 関数の巻き込み: 上の例のように、条件文脈で呼ばれた関数は本体の全コマンドが適用外になる。
    関数の中に set -e を書き足しても変わらない。
  4. パイプライン: 既定ではパイプライン全体の終了ステータスは最後のコマンドのものだけである。curl ... | tar xcurl が失敗しても、tar が 0 を返せば通ってしまう。
  5. local / export との同時代入: local x=$(false)local コマンドの実行であり、その終了ステータスは 0 になる。
    同じ行で書いた $(...) の失敗は消える。
    単独の x=$(false) なら止まる。

解決策

1. pipefail を足す

set -eo pipefail
curl -fsSL https://example.com/archive.tar.gz | tar xz

pipefail を付けると、パイプラインのどれか 1 つでも失敗すれば全体が失敗になる。curl には -f も付けて、HTTP エラーを終了ステータスに反映させる。

2. 宣言と代入を分ける

local out
out=$(some_command)

こう書けば some_command の終了ステータスが代入文のものとして評価され、set -e が効く。
関数内で local を使うときの定石である。

3. 止めたい場所を明示する

some_command || { echo "some_command に失敗した" >&2; exit 1; }

適用外の条件を暗記して回避するより、重要な箇所だけ自分で判定するほうが読み手にも意図が伝わる。

4. ERR トラップで位置を出す

set -eo pipefail
trap 'echo "failed: line $LINENO (exit $?)" >&2' ERR

ERR トラップは set -e が止める条件と同じ場面で発火する。
どこで落ちたのか分からないスクリプトの調査に使える。

5. 挙動を先に確かめる

bash -c 'set -e; if false; then :; fi; echo reached'

reached が出れば、その書き方は適用外だと確認できる。
仕様の詳細は Bash Reference Manual(新しいタブで開く) の The Set Builtin にある。

この記事は役立ちましたか?