シェルスクリプトのset -euo pipefailとtrap ERRによるエラーハンドリング設計|どこで失敗したかを確実に捕捉する実装パターン

運営:リナックスマスター.JP(代表 宮崎智広)
HOME > Linux技術 リナックスマスター.JP(Linuxマスター.JP) > シェルスクリプト > シェルスクリプトのset -euo pipefailとtrap ERRによるエラーハンドリング設計|どこで失敗したかを確実に捕捉する実装パターン
「set -e を入れているのに、どこでエラーが出たのか分からない」
こういう経験、一度はあるはずです。

シェルスクリプトに 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 のクリーンアップ関数と組み合わせるとリソース解放が漏れない


「このままじゃマズい」と感じていませんか?
参考書を開く気力もない、同年代に取り残される不安——
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら

set -e だけでは足りない理由

set -e(errexit)はコマンドが0以外の終了コードを返すとスクリプトを即座に終了させるオプションです。
「これを先頭に書けばエラーは止まる」と考えると、落とし穴にはまります。

1. パイプラインの中間エラーは見逃される

# set -e だけの場合、以下のコマンドはエラーにならない set -e cat /nonexistent_file | sort echo "ここまで到達する(終了コード=0)"

cat が失敗しても、パイプラインの終了コードは最後のコマンド(sort)のものになります。
sort に何も渡されなくても sort 自体は0で終了するので、set -e はスクリプトを止めません。

2. 未定義変数の参照もスルーされる

set -e echo "ディレクトリ: $UNDEFINED_VAR" # 空文字列として展開される rm -rf "${UNDEFINED_VAR}/work" # rm -rf "/work" と同義になる危険

変数の未定義チェックは set -e では対応できません。

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 が未定義なら即エラーで終了

デフォルト値を持たせたい変数は「${VAR:-デフォルト値}」の形で書くと set -u との組み合わせでも問題ありません。

3. set -o pipefail:パイプラインのエラーを検知する

パイプラインでいずれかのコマンドが失敗したとき、パイプライン全体の終了コードを0以外にします。
set -e と組み合わせると、パイプの途中エラーでもスクリプトが止まるようになります。

set -eo pipefail cat /nonexistent_file | sort # cat が失敗するので pipefail により全体が失敗 echo "ここには到達しない"

この3つを1行で書く慣用形が「set -euo pipefail」です。
スクリプトの先頭(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

この状態では、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 ERR に到達するようになります。
スクリプトが複数の関数に分かれている場合は、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 を加えると失敗が隠れる

変数への代入式全体が失敗する場合は set -e が働きますが、「|| 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]
set -euo pipefail と trap ERR・trap EXIT を組み合わせた設計は、シェルスクリプトの信頼性を大きく向上させます。
「スクリプトが静かに失敗して気づかなかった」という問題の多くは、この設計を採用することで防げます。

「エラー処理は入れているつもりだった」から「どこで何が失敗したかが必ず分かる設計」へ。この骨格を覚えたうえで、次のスクリプトから適用してみてください。
現場で通用する安全なLinuxサーバー構築の「型」を体系的に身につけたい方へ、
シェルスクリプトの設計パターンをはじめ、実務で使える手順を体系的に学べるセミナーを開催しています。
詳しくは下記からご確認ください。
>> Linux Master Pro Seminar の詳細はこちら

無料メルマガで学習を続ける

Linuxの実践スキルをメールで毎週お届け。
登録は30秒、解除もいつでも可。

登録無料・いつでも解除できます

暗記不要・1時間後にはサーバーが動く

3,100名以上が実践した「型」を無料で公開中

プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。

姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら

Linux無料マニュアル(図解60P) 名前とメールで30秒登録

運営

リナックスマスター.JP(代表 宮崎智広)

株式会社イーネットマーキュリーが運営するLinux教育サイトです。代表の宮崎智広は現役のLinuxサーバー管理者として20年以上の実務経験を持ち、累計3,100名以上のエンジニアを指導してきました。実践型のLinuxセミナーや無料マニュアルの配布を通じてLinux人材の育成に取り組んでいます。