シェルスクリプトが途中でコケたのに、最後まで走り切って正常終了する。
自動化でいちばんこわいのが、この「失敗に気づかないまま進む」パターンです。
bash には、それを防ぐためのスイッチが3つ用意されています。
スクリプトの先頭でよく見かける set -euo pipefail という1行が、まさにそれ。
この3つがそれぞれ何を止めているのかを、実際の挙動と一緒に整理するのが本記事の狙いです。
set -e はエラーが出た時点で止める
何も設定しない bash は、コマンドが失敗しても次の行をそのまま実行します。
#!/bin/bash
cd /nonexistent-dir
echo "ここに来てしまう"
実行すると、cd が失敗しているのに echo が動きました。
$ bash a.sh
a.sh: line 2: cd: /nonexistent-dir: No such file or directory
ここに来てしまう
exit=0
しかも終了コードは 0 なので、呼び出し元から見れば成功したようにしか見えません。
静かに壊れて、静かに成功を返すのがいちばん厄介なパターン。
先頭に set -e を1行足すと、失敗した時点でスクリプトが打ち切られます。
#!/bin/bash
set -e
cd /nonexistent-dir
echo "ここには来ない"
$ bash b.sh
b.sh: line 3: cd: /nonexistent-dir: No such file or directory
exit=1
失敗を失敗のまま呼び出し元へ返せるようになりました。
set -u は未定義の変数を許さない
bash は、存在しない変数を参照しても文句を言わず、空文字として扱います。
これが牙をむくのは、変数名をタイプミスしたときです。
#!/bin/bash
set -e
TARGET=/tmp/demo-dir
rm -rf "$TARGE"/
echo "done"
TARGET を TARGE と書き間違えたせいで、rm に渡ったのはスラッシュ1文字だけになりました。
$ bash c.sh
rm: it is dangerous to operate recursively on '/'
rm: use --no-preserve-root to override this failsafe
exit=1
今回は GNU coreutils 側の安全装置が止めてくれましたが、パスの組み立て方によっては本当に消える場面です。
set -u を足しておけば、未定義の変数に触れた瞬間に落ちます。
$ bash d.sh
d.sh: line 4: TARGE: unbound variable
exit=1
タイプミスした変数名がそのまま名指しされるので、原因を探す時間もほとんどかかりません。
set -o pipefail はパイプの途中の失敗を拾う
パイプでつないだとき、bash が終了コードとして採用するのは一番最後のコマンドだけです。
#!/bin/bash
set -eu
cat /nonexistent-file | wc -l
echo "パイプは失敗したのに続く exit=$?"
$ bash e.sh
cat: /nonexistent-file: No such file or directory
0
パイプは失敗したのに続く exit=0
cat は失敗しているのに、wc -l が成功したせいで全体は成功扱い。
set -o pipefail を足すと、パイプの中で1つでも失敗があれば全体が失敗になります。
$ bash f.sh
0
cat: /nonexistent-file: No such file or directory
exit=1
ログを集計するワンライナーほど、この設定の有無で結果が変わってくるはず。
3つまとめると set -euo pipefail になる
-e と -u は1文字オプションなので、-eu とつなげて書けます。
pipefail のほうは長い名前のオプションなので、-o を付けて指定する決まり。
#!/bin/bash
set -euo pipefail
いま有効になっているかどうかは、set -o の出力で確かめられます。
$ bash -c "set -euo pipefail; set -o | grep -E 'pipefail|errexit|nounset'"
errexit on
nounset on
pipefail on
入れた直後にハマる3つのポイント
grep はマッチ0件でも失敗扱い
grep はマッチが1件も無いとき、終了コード 1 を返す仕様です。
set -e 下では、これがそのままスクリプトの停止につながります。
#!/bin/bash
set -euo pipefail
COUNT=$(grep -c "存在しない語" /etc/hostname)
echo "COUNT=$COUNT"
$ bash g.sh
exit=1
echo まで届かないので、COUNT の中身すら表示されません。
0件も想定内なら、末尾に || true を付けて失敗を握りつぶします。
COUNT=$(grep -c "存在しない語" /etc/hostname || true)
$ bash h.sh
COUNT=0
exit=0
if の条件部では set -e が働かない
条件として評価される場所は、失敗しても停止の対象外です。
#!/bin/bash
set -euo pipefail
if grep -q "存在しない語" /etc/hostname; then
echo "見つかった"
else
echo "見つからないが、ここは通る"
fi
grep がマッチしなくても else 側へ落ちるだけで、スクリプトは走り続けます。
止まってほしくない判定は if で包む、と覚えておくと扱いやすいはず。
sh には pipefail が無い
Ubuntu では /bin/sh の実体が dash なので、sh で実行すると弾かれます。
$ dash -c 'set -o pipefail; echo ok'
dash: 1: set: Illegal option -o pipefail
シバンに #!/bin/bash と書いて bash で実行するのが前提条件です。
set -u と共存させる書き方
環境変数が未設定のときはデフォルト値を使いたい、という場面もあります。
その場合は、変数名のうしろにコロンとハイフンを挟んでデフォルト値を添えるだけ。
#!/bin/bash
set -euo pipefail
echo "OUT=${OUTDIR:-/tmp/default}"
$ bash i.sh
OUT=/tmp/default
未設定でも落ちず、設定されていればその値が優先されます。
最後に
set -e は失敗で止める、set -u はタイプミスを止める、set -o pipefail はパイプ途中の失敗を拾う。
この1行を先頭に置いておくだけで、黙って壊れるスクリプトは目に見えて減ります。
本記事の実行例は、Ubuntu 22.04 上の GNU bash 5.1.16(1)-release で実際に動かして確認しました。
各オプションの正式な定義は Bash Reference Manual(The Set Builtin) にまとまっています。
cron から呼ぶスクリプトは失敗がとくに見えにくいので、こちらもあわせてどうぞ。
エラーメッセージをログに残す方法は、標準出力と標準エラー出力の記事で扱っています。
以上です。










コメントを残す