bash strict mode 入門 - set -euo pipefail で安全なスクリプトを書く

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 拡張。POSIX sh(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 -ecd 失敗で即終了する

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 || true

set -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 を後付けする

次に読む