bash strict mode 入門 - set -euo pipefail で安全なスクリプトを書く
bash strict mode とは?
結論: スクリプト先頭に
set -euo pipefailを置き、エラー・未定義変数・パイプ失敗を即座に検出する書き方。隠れたバグの早期発見が目的。
bash の既定動作は寛容すぎる。コマンドが失敗しても、変数が未定義でも、パイプの途中でエラーが出ても、スクリプトはそのまま走り続ける。結果として「途中で失敗したのに最後まで実行され、壊れたデータを残す」事故が起きる。
bash strict mode は、スクリプト冒頭で 3 つのオプションを有効化し、bash を「失敗したら止まる」挙動に切り替える定番イディオムである。
#!/usr/bin/env bash set -euo pipefail IFS=$'\n\t'
この記事で扱う 4 要素
set -e(errexit): コマンド失敗で即終了set -u(nounset): 未定義変数の参照をエラーにset -o pipefail: パイプライン途中の失敗を検出IFS=$'\n\t': 単語分割の事故を防ぐ(任意だが推奨)
前提(対象環境)
- シェル: bash(
#!/usr/bin/env bash) pipefailは bash 拡張。POSIXsh(dash 等)では動かない- 対話シェルではなくスクリプトでの利用を想定
なぜ既定の bash は危険なのか?
結論: 既定の bash はコマンドが失敗しても続行し、タイプミスした変数は空文字として扱う。失敗が無視されたまま処理が進む点が事故の温床。
次のスクリプトを見てほしい。バックアップ用ディレクトリへ移動してから古いファイルを消す、よくある処理である。
#!/usr/bin/env bash cd "$BACKUP_DIR" rm -rf ./*
BACKUP_DIR の設定を忘れると、既定の bash では cd ""(何もしない、カレントディレクトリのまま)となり、rm -rf ./* が今いるディレクトリで実行される。cd の失敗が無視されるため、意図しない場所を消してしまう。
strict mode を有効にすると、この事故は 2 重に防がれる。
set -uが未定義の$BACKUP_DIR参照でスクリプトを止める- 仮に空文字でも
set -eがcd失敗で即終了する
既定の bash では「失敗しても次の行へ進む」のが標準動作。strict mode はこの暗黙の続行を断ち切るための保険である。
set -e(errexit)の効果と落とし穴
結論:
set -eはコマンドが非ゼロ終了したら即スクリプトを終了する。ただしif条件・&&・関数内など効かない文脈が多く、過信は禁物。
set -e は、コマンドが 0 以外の終了ステータスを返した時点でスクリプトを終了させる。
set -e cp important.conf /etc/myapp/ # 失敗したらここで停止 systemctl restart myapp # 上が成功した時だけ実行される
errexit が効かない主なケース
set -e には「効かない文脈」が仕様として存在する。これを知らないと「止まるはずが止まらない」事故になる。
if cmd; then ...の条件部(失敗が判定に使われるため)cmd && .../cmd || ...の左辺(最後のコマンド以外)!で否定したコマンド- パイプラインの最後以外のコマンド(
pipefailで補う)
set -e
# NG: grep が失敗しても止まらない(条件部だから)
if grep -q pattern file.txt; then
echo "found"
fi
# 意図的に失敗を許容したい場合は || true を明示
risky_command || trueset -e だけに頼らず、重要な処理は終了ステータスを明示的にチェックするのが堅実。set -e は「うっかり見落とした失敗」を拾う保険と捉える。
set -u と pipefail の役割
結論:
set -uは未定義変数の参照をエラーにしてタイプミスを検出。set -o pipefailはパイプ途中の失敗を拾い、grep | sortのような連結の成否を正しく判定する。
set -u(nounset)
未定義の変数を参照するとエラーになり、スクリプトが停止する。変数名のタイプミスを即座に発見できる。
set -u name="penguin" echo "$nmae" # タイポ → "unbound variable" でエラー終了
意図的に「未定義なら既定値」を使いたい場合は、parameter expansion で明示する。
# 未定義でもエラーにせず既定値を使う
echo "${OPTIONAL_VAR:-default}"
# 位置パラメータも同様($1 が無い場合の保険)
target="${1:-/tmp}"set -o pipefail
既定では、パイプラインの終了ステータスは最後のコマンドのものになる。途中のコマンドが失敗しても、最後が成功すればパイプ全体は成功扱いになってしまう。
# pipefail なし: curl が失敗しても grep が成功すれば $? は 0 curl -s https://example.com/data | grep "key" set -o pipefail # pipefail あり: curl の失敗が pipe 全体の失敗として伝わる curl -s https://example.com/data | grep "key"
pipefail はパイプラインのうち最も右側で失敗したコマンドの終了ステータスを返す。すべて成功なら 0。データ取得とフィルタを連結する処理で特に効く。
IFS の設定はなぜ推奨されるのか?
結論:
IFS=$'\n\t'は単語分割の区切りを改行とタブだけに限定する設定。ファイル名やパスに含まれる空白での意図しない分割事故を防ぐ。
IFS(Internal Field Separator)は、bash が文字列を単語に分割する際の区切り文字。既定値はスペース・タブ・改行の 3 つ。このうちスペースが、空白入りファイル名で事故を起こす。
# 既定 IFS: "my file.txt" がスペースで 2 語に割れる
for f in $(ls); do
echo "$f"
done
# IFS=$'\n\t': 改行・タブのみを区切りにする
IFS=$'\n\t'Aaron Maxwell が提唱した「unofficial bash strict mode」では、set -euo pipefail に加えてこの IFS 設定をセットで推奨している。スペースを区切りから外すことで、forループや変数展開での分割が直感的になる。
IFS 変更は副作用がある。スペース区切りの単語分割に依存する処理(read -a での配列読み込み等)がある場合は、その箇所だけ局所的に IFS を戻すか、設定を見送る。
strict mode の実践テンプレート
結論: shebang・strict mode・エラートラップをまとめた定型を雛形として持っておくと、毎回安全なスクリプトを最短で書き始められる。
実務でそのまま使えるテンプレート。trap でエラー発生行を表示すると、デバッグが一気に楽になる。
#!/usr/bin/env bash
#
# 安全なスクリプトの雛形
#
set -euo pipefail
IFS=$'\n\t'
# エラー発生時に行番号を表示
trap 'echo "Error on line $LINENO" >&2' ERR
main() {
local target="${1:-/tmp}"
echo "処理対象: $target"
# ここに本処理を書く
}
main "$@"コピペ用: 最小構成
#!/usr/bin/env bash set -euo pipefail IFS=$'\n\t'
なぜ `#!/usr/bin/env bash` を使うのか
#!/bin/bash と直接書くと、bash が /bin 以外(例: /usr/local/bin)にインストールされた環境で動かない。env を経由すると PATH 上の bash を探すため、可搬性が高まる。pipefail は bash 固有のため、#!/bin/sh ではなく bash を明示することも重要。
strict mode の注意点まとめ
結論: strict mode は万能ではない。
set -eの効かない文脈・IFSの副作用・既存スクリプトへの後付けリスクを理解した上で使う。
| 項目 | 注意点 |
|---|---|
set -e |
if 条件・&& 左辺・関数内など効かない文脈がある |
set -u |
既定値が要る変数は ${VAR:-default} で明示する |
pipefail |
bash 専用。POSIX sh では使えない |
IFS=$'\n\t' |
スペース区切り依存の処理がある場合は副作用に注意 |
| 後付け | 既存スクリプトに足すと、隠れていた失敗が一斉に顕在化する |
やってはいけないこと
pipefailを#!/bin/shスクリプトで使うset -u環境で$1を未チェックのまま参照する- 巨大な既存スクリプトへ無検証で strict mode を後付けする