こういう経験、一度はあるはずです。
シェルスクリプトに set -e を入れただけでは、パイプラインの途中やサブシェルの中のエラーを見逃すことがあります。
スクリプトが静かに終了していて、ログを追っても失敗箇所が特定できない。これは設計の問題です。
この記事では、RHEL 9系/Ubuntu 24.04 LTS を想定環境として、set -euo pipefail と trap ERR の組み合わせによるエラーハンドリング設計を解説します。
「どのファイルの何行目で何が失敗したか」を確実に記録できるスクリプトの骨格を、コード例とともに示します。
この記事のポイント
・ set -e/-u/-o pipefail の3つを組み合わせるとエラーの見逃しが大幅に減る
・ trap ERR + BASH_LINENO + FUNCNAME で失敗箇所をログに記録できる
・ set -E(errtrace)を加えると関数内のエラーも trap ERR に到達する
・ trap EXIT のクリーンアップ関数と組み合わせるとリソース解放が漏れない
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
set -e だけでは足りない理由
set -e(errexit)はコマンドが0以外の終了コードを返すとスクリプトを即座に終了させるオプションです。「これを先頭に書けばエラーは止まる」と考えると、落とし穴にはまります。
1. パイプラインの中間エラーは見逃される
# set -e だけの場合、以下のコマンドはエラーにならない set -e cat /nonexistent_file | sort echo "ここまで到達する(終了コード=0)"
sort に何も渡されなくても sort 自体は0で終了するので、set -e はスクリプトを止めません。
2. 未定義変数の参照もスルーされる
set -e echo "ディレクトリ: $UNDEFINED_VAR" # 空文字列として展開される rm -rf "${UNDEFINED_VAR}/work" # rm -rf "/work" と同義になる危険
3. 関数内のエラーが trap に届かない
set -e を使っていても、関数内で発生したエラーを trap ERR で捕捉するには、後述する set -E(errtrace)が必要です。これを知らずにいると、「trap を設定しているのに発火しない」という状況が起きます。
set -euo pipefail の3つのオプションを理解する
1. set -e(errexit):コマンド失敗で即終了
コマンドが0以外の終了コードを返したとき、スクリプトを終了します。ただし if 文の条件節・while の条件節・&& や || の左辺では無効になる点を覚えておいてください。
# set -e が有効でも終了しない例 set -e if grep -q "pattern" /etc/hosts; then echo "見つかった" fi # grep が1(見つからない)を返しても set -e は発動しない
2. set -u(nounset):未定義変数をエラーにする
未定義の変数を参照したとき、エラーを発生させます。スペルミスや設定漏れを実行前に検知できるため、本番スクリプトでは必須です。
set -u echo "${BACKUP_DIR}" # BACKUP_DIR が未定義なら即エラーで終了
3. set -o pipefail:パイプラインのエラーを検知する
パイプラインでいずれかのコマンドが失敗したとき、パイプライン全体の終了コードを0以外にします。set -e と組み合わせると、パイプの途中エラーでもスクリプトが止まるようになります。
set -eo pipefail cat /nonexistent_file | sort # cat が失敗するので pipefail により全体が失敗 echo "ここには到達しない"
スクリプトの先頭(shebang の直後)に置く書き方が一般的です。
trap ERR でエラー発生箇所を記録する
set -euo pipefail でエラー時にスクリプトが終了するようになっても、「どこで失敗したか」が分からなければ調査が進みません。trap ERR はコマンドが0以外の終了コードを返したとき(set -e が働く場面)に指定した処理を実行します。
1. BASH_LINENO・FUNCNAME・BASH_COMMAND の3変数
bash には、エラーが起きた文脈を知るための組み込み変数が用意されています。・BASH_LINENO[0]:エラーが発生した行番号
・FUNCNAME[1]:呼び出し元の関数名(トップレベルなら "main")
・BASH_COMMAND:失敗したコマンドの文字列
2. エラーハンドラ関数の実装例
#!/bin/bash set -euo pipefail # エラーハンドラ err_handler() { local rc=$? local line="${BASH_LINENO[0]}" local cmd="${BASH_COMMAND}" local func="${FUNCNAME[1]:-main}" echo "[ERROR] ${func}() 行 ${line}: コマンド「${cmd}」が終了コード ${rc} で失敗" >&2 } trap 'err_handler' ERR
[ERROR] main() 行 15: コマンド「cp /src/config.txt /dst/」が終了コード 1 で失敗
ログファイルに追記したい場合は「>>&2」を「>> /var/log/myscript.log」に変えるだけです。
シェルスクリプトの実践的な設計パターンをまとめた専用ページも参考にしてください。
set -E(errtrace)で関数内のエラーを trap ERR に渡す
bash のデフォルト動作では、trap ERR はトップレベルにしか適用されません。関数内で発生したエラーは trap ERR の対象外になるため、関数の中で失敗してもハンドラが呼ばれません。
1. set -E なしの問題
#!/bin/bash set -euo pipefail err_handler() { echo "[ERROR] 行 ${BASH_LINENO[0]}: ${BASH_COMMAND}" >&2; } trap 'err_handler' ERR # set -E がないと関数内では発動しない do_copy() { cp /nonexistent_source /dest # 失敗してもハンドラが呼ばれない } do_copy
2. set -E を加えた実装例
#!/bin/bash set -euo pipefail -E # -E(errtrace)を追加 err_handler() { local rc=$? echo "[ERROR] 行 ${BASH_LINENO[0]} 関数 ${FUNCNAME[1]:-main}: ${BASH_COMMAND} (rc=${rc})" >&2 } trap 'err_handler' ERR do_copy() { cp /nonexistent_source /dest # これでハンドラが呼ばれる } do_copy
スクリプトが複数の関数に分かれている場合は、set -E の追加が実質的に必須です。
trap EXIT とクリーンアップ関数を組み合わせる
trap ERR はエラー時の記録を担い、実際のリソース解放(一時ファイル削除・ロック解除)は trap EXIT に担当させます。trap EXIT はスクリプトが正常終了・異常終了のどちらで終わっても必ず実行されるため、クリーンアップの漏れを防げます。
1. クリーンアップ関数の設計
#!/bin/bash set -euo pipefail -E TMPDIR_WORK="" cleanup() { local rc=$? if [[ -n "${TMPDIR_WORK}" && -d "${TMPDIR_WORK}" ]]; then rm -rf "${TMPDIR_WORK}" echo "[INFO] 一時ディレクトリを削除: ${TMPDIR_WORK}" >&2 fi exit "${rc}" # 元の終了コードを維持する } err_handler() { local rc=$? echo "[ERROR] 行 ${BASH_LINENO[0]} 関数 ${FUNCNAME[1]:-main}: ${BASH_COMMAND} (rc=${rc})" >&2 } trap 'cleanup' EXIT trap 'err_handler' ERR # メイン処理 TMPDIR_WORK=$(mktemp -d /tmp/myscript.XXXXXX) cp /etc/hosts "${TMPDIR_WORK}/hosts.bak" # 以降の処理でエラーが出ても cleanup が呼ばれて一時ディレクトリが消える
2. trap ERR と trap EXIT の実行順序
エラーが発生したとき、実行順序は次のとおりです。・ERR トラップが先に実行される(エラー記録)
・set -e によってスクリプトが終了する
・EXIT トラップが最後に実行される(クリーンアップ)
cleanup 関数の最後で exit "${rc}" を呼ぶことで、元の失敗終了コードがスクリプトの終了コードとして呼び出し元(cron や systemd)に伝わります。
実務でよくある落とし穴
1. set -e が無効になる場所
set -e は以下の場所では効かない仕様です。・if 文の条件節(if some_command; then・・・)
・while/until の条件節
・&& または || の左辺
・! で否定したコマンド
これらの中で失敗しても set -e はスクリプトを止めないため、自分で終了コードを確認する必要があります。
2. コマンド置換内のエラーは捕捉されない
# コマンド置換内の失敗は set -e の対象外 set -euo pipefail RESULT=$(nonexistent_command) # これはエラーになる(変数代入が失敗) # ただし以下は見逃しやすい RESULT=$(some_command) || true # || true を加えると失敗が隠れる
3. 関数の戻り値と set -e の関係
関数が return 1 で終了した場合も set -e の対象になります。関数を if 文の条件節で呼ぶ場合は、その中で set -e が無効になる点に注意してください。
check_service() { systemctl is-active --quiet nginx return $? } # この書き方では check_service 内の失敗も set -e の対象外 if check_service; then echo "nginx は起動中" fi
本記事のまとめ
| やりたいこと | 設定・仕組み |
|---|---|
| コマンド失敗でスクリプトを止める | set -e |
| 未定義変数をエラーにする | set -u |
| パイプラインの途中エラーを検知する | set -o pipefail |
| エラー発生箇所をログに記録する | trap 'err_handler' ERR |
| 関数内のエラーも trap ERR に届ける | set -E(errtrace) |
| 正常終了・異常終了でも必ずクリーンアップする | trap 'cleanup' EXIT |
| エラー時の行番号・コマンドを取得する | BASH_LINENO[0]・BASH_COMMAND・FUNCNAME[1] |
「スクリプトが静かに失敗して気づかなかった」という問題の多くは、この設計を採用することで防げます。
「エラー処理は入れているつもりだった」から「どこで何が失敗したかが必ず分かる設計」へ。この骨格を覚えたうえで、次のスクリプトから適用してみてください。
シェルスクリプトの設計パターンをはじめ、実務で使える手順を体系的に学べるセミナーを開催しています。
詳しくは下記からご確認ください。
>> Linux Master Pro Seminar の詳細はこちら
3,100名以上が実践した「型」を無料で公開中
プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。
姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら
- 前のページへ:シェルスクリプトでGitフックを設計する方法|pre-commit・pre-pushへのチェック組み込みとチームへの配布パターン
- この記事の属するカテゴリ:シェルスクリプトへ戻る

無料メルマガで学習を続ける
Linuxの実践スキルをメールで毎週お届け。
登録は30秒、解除もいつでも可。
登録無料・いつでも解除できます