DevOps Bootcamp · レッスン

クリーンアップとシグナル処理のためのtrapハンドラー

EXIT、ERR、INTのtrapを登録し、一時ファイルを削除して、途中までの処理を確実にロールバックします。

レッスン 2/413 ステップ

「クリーンアップとシグナル処理のためのtrapハンドラー」はCoddyKit上の無料DevOps Bootcampレッスンです。 これはレッスン2/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはDevOps Bootcamp学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 DevOps Bootcampコースには全4レッスンが含まれています。

トラップハンドラーが重要な理由

Bashスクリプトは、正常に終了した場合でも、エラーによって終了した場合でも、ユーザーがCtrl+Cを押した場合でも、一時ファイルや書きかけのデータ、壊れた状態を残すことがあります。クリーンアップを行わないと、これらの不要なデータが蓄積し、原因の特定が難しい問題を引き起こします。

トラップハンドラーを使うと、特定のシグナルまたは疑似シグナルを受け取ったときにBashが自動的に実行する関数やコマンドを登録できます。

  • EXIT — スクリプトが理由を問わず終了するたびに実行されます
  • ERR — いずれかのコマンドがゼロ以外の終了ステータスを返した後に実行されます
  • INT — ユーザーがSIGINT(Ctrl+C)を送信したときに実行されます
  • TERM — SIGTERM(例:killから送信された場合)を受信したときに実行されます

適切に記述されたスクリプトでは、リスクのある処理を始める前に、最上部でこれらのトラップを登録します。

trapビルトインの構文

trapビルトインは、1つ以上のシグナルまたは疑似シグナルに対するハンドラーを登録します。一般的な構文は次のとおりです。

trap 'command_or_function' SIGNAL [SIGNAL...]

主なルール:

  • 最初の引数は、シグナルが発生したときに実行するシェルコードを囲んだ文字列です(関数名も指定できます)。
  • ハンドラーの後には、複数のシグナルを指定できます。
  • trap '' SIGNALは、そのシグナルを無視します(空のハンドラー)。
  • trap - SIGNALは、そのシグナルをデフォルトの動作にリセットします。

トラップは同じシェルで呼び出された関数には継承されますが、( )や&で起動したサブシェルには継承されません。

#!/usr/bin/env bash
# Syntax examples — not a full script

# Register a cleanup function on EXIT
trap cleanup EXIT

# Inline handler for INT and TERM
trap 'echo "Interrupted!"; exit 1' INT TERM

# Ignore SIGHUP
trap '' HUP

# Reset SIGPIPE to default
trap - PIPE

EXITトラップ — 安全網

EXIT疑似シグナルは、登録しておくべき最も重要なトラップです。シェルがどのような理由でも終了すると実行されます。正常終了、exit N、処理されないエラー、シグナルによる終了が該当します。そのため、一時ファイルの削除に適しています。

推奨される方法:

  • mktempで一時ファイルを作成し、名前が一意で予測できないようにします。
  • パスをただちに変数に保存します。
  • 作成直後にEXITトラップを登録し、必ずクリーンアップが実行されるようにします。

スクリプトの終了コードは保持されます。exitをハンドラー内で明示的に呼び出さない限り、トラップハンドラーの終了コードは無視されます。

#!/usr/bin/env bash
set -euo pipefail

TMPFILE=$(mktemp /tmp/myapp.XXXXXX)

cleanup() {
    echo "Removing temp file: $TMPFILE" >&2
    rm -f "$TMPFILE"
}
trap cleanup EXIT

# Do real work
echo "Processing..." > "$TMPFILE"
sort "$TMPFILE" -o "$TMPFILE"
cp "$TMPFILE" /tmp/myapp_result.txt

echo "Done. Result saved."

複数の一時ファイルとディレクトリの処理

実際のスクリプトでは、ファイル、ディレクトリ、FIFO、ロックファイルなど、複数の一時的なリソースを作成することがよくあります。それぞれを個別に追跡する代わりに、配列にまとめ、1つのクリーンアップ関数ですべてを削除します。

このパターンは簡単に拡張できます。新しい一時リソースを作成するたびに配列へ追加します。EXITトラップは、スクリプトがどこで終了しても常にリスト全体を処理します。

#!/usr/bin/env bash
set -euo pipefail

CLEANUP_TARGETS=()

add_cleanup() {
    CLEANUP_TARGETS+=("$1")
}

cleanup() {
    echo "Running cleanup..." >&2
    for target in "${CLEANUP_TARGETS[@]:-}"; do
        rm -rf "$target"
        echo "Removed: $target" >&2
    done
}
trap cleanup EXIT

# Create and track temp resources
TMPDIR_WORK=$(mktemp -d /tmp/work.XXXXXX)
add_cleanup "$TMPDIR_WORK"

TMPFILE_LOG=$(mktemp /tmp/run.XXXXXX.log)
add_cleanup "$TMPFILE_LOG"

echo "Working in $TMPDIR_WORK" | tee "$TMPFILE_LOG"
touch "$TMPDIR_WORK/output.txt"
echo "Step complete."

ERRトラップ — 失敗を捕捉する

ERR疑似シグナルは、単純なコマンドがゼロ以外の終了ステータスを返した後に実行されます。ただし、シェルオプションset -e(errexit)が有効であるか、コマンドがif、while、または||/&&の複合コマンド内にない場合に限られます。

ERRトラップは次の目的で使用します。

  • $LINENOと$BASH_COMMANDを使って、失敗した行をログに記録する。
  • EXITトラップが実行される前に、ロールバック処理を開始する。
  • 構造化されたエラーメッセージをstderrに出力する。

重要:関数が呼び出し元のシェルからERRトラップを継承するようにset -E(errtrace)を明示的に実行しない限り、ERRトラップは関数内では実行されません。

#!/usr/bin/env bash
set -eEuo pipefail

on_error() {
    local exit_code=$?
    local line=$1
    echo "ERROR: command failed with exit code $exit_code at line $line" >&2
    echo "  Command: $BASH_COMMAND" >&2
}
trap 'on_error $LINENO' ERR

echo "Starting task..."
cp /nonexistent/file /tmp/   # This will fail
echo "This line is never reached."

INTトラップ — Ctrl+Cを適切に処理する

Ctrl+Cを押すと、フォアグラウンドのプロセスグループにSIGINTが送信されます。トラップがない場合、Bashはすぐに終了し、処理途中の作業が残る可能性があります。

INTトラップを登録すると、次の処理が可能になります。

  • ユーザーにわかりやすいキャンセルメッセージを表示する。
  • EXITトラップに処理を引き継ぐ前に、途中までのロールバックを行う。
  • 意味のあるステータスコードで終了する(130はSIGINTの慣例的なコードで、128 + 2です)。

INTハンドラーの処理が終わったら、exit 130を呼び出して正しい終了コードを引き継ぎます。これにより、EXITトラップが自動的に実行されます。

#!/usr/bin/env bash
set -euo pipefail

TMPFILE=$(mktemp /tmp/demo.XXXXXX)

cleanup() {
    rm -f "$TMPFILE"
    echo "Temp file removed." >&2
}

on_interrupt() {
    echo "" >&2
    echo "Caught SIGINT — cancelling gracefully." >&2
    exit 130   # EXIT trap (cleanup) runs automatically after this
}

trap cleanup EXIT
trap on_interrupt INT

echo "Running long task. Press Ctrl+C to cancel."
for i in $(seq 1 10); do
    echo "Step $i/10..."
    sleep 1
done
echo "All steps complete."

EXIT、ERR、INTトラップの組み合わせ

本番環境のスクリプトでは、通常、あらゆる終了経路に対応できるように3つすべてのトラップをまとめて登録します。

  • EXIT — 常に実行され、ファイルのクリーンアップを処理します。
  • ERR — 失敗したコマンドと行番号をログに記録します。
  • INT — キャンセルメッセージを表示し、コード130で終了します。

EXITは必ず最後に実行されるため、クリーンアップ処理を記述する場所は1か所だけで済みます。ERRとINTのハンドラーはログ記録と状態設定に集中し、exitを呼び出して実際のクリーンアップをEXITに委ねることができます。

先頭でset -eEuo pipefailを使用すると、関数内のエラーでもERRが実行され、pipefailによってパイプライン内の失敗も検出できます。

#!/usr/bin/env bash
set -eEuo pipefail

TMPFILE=$(mktemp /tmp/combined.XXXXXX)
ROLLBACK_NEEDED=false

cleanup() {
    if [[ "$ROLLBACK_NEEDED" == true ]]; then
        echo "Rolling back partial changes..." >&2
    fi
    rm -f "$TMPFILE"
    echo "Cleanup done." >&2
}

on_error() {
    ROLLBACK_NEEDED=true
    echo "ERR at line $1: $BASH_COMMAND" >&2
}

on_interrupt() {
    echo "Interrupted by user." >&2
    exit 130
}

trap cleanup       EXIT
trap 'on_error $LINENO' ERR
trap on_interrupt  INT

echo "hello" > "$TMPFILE"
echo "Script finished successfully."

トラップの動的なリセットと無効化

スクリプトの途中でトラップを変更する必要が生じることがあります。たとえば、重要な処理が完了した後に1回だけクリーンアップを行い、その後の通常処理で不要なロールバックが発生しないようにする場合です。

  • trap - SIGNALは、シグナルを組み込みのデフォルト動作にリセットします。
  • trap '' SIGNALは、シグナルを完全に無視します(そのシグナルではプロセスを終了できません)。
  • 新しいハンドラーはいつでも再登録できます。最後に呼び出したtrapが有効になります。

よく使われるパターンは、リスクのある処理が成功した後にEXITトラップを更新し、ロールバック用ハンドラーから単純なクリーンアップ用ハンドラーへ切り替える方法です。

#!/usr/bin/env bash
set -euo pipefail

TMPFILE=$(mktemp /tmp/staged.XXXXXX)

rollback() {
    echo "ROLLBACK: removing $TMPFILE" >&2
    rm -f "$TMPFILE"
}

cleanup_only() {
    echo "CLEANUP: removing $TMPFILE" >&2
    rm -f "$TMPFILE"
}

# Start with rollback in case something fails during the risky phase
trap rollback EXIT

echo "Performing risky operation..."
echo "critical data" > "$TMPFILE"
# ... imagine more risky steps here ...

# Risky phase succeeded — switch to simple cleanup
trap cleanup_only EXIT

echo "Risky phase done. Now doing safe finalization."
cp "$TMPFILE" /tmp/staged_result.txt
echo "All done."

トラップを介した終了コードの引き継ぎ

見落としやすい問題があります。クリーンアップ関数に失敗する可能性のあるコマンドが含まれていると、スクリプトの最終終了コードが上書きされることがあります。元の終了コードを明示的に保持してください。

トラップハンドラー内では、$?にトラップを発生させた終了コードが格納されています。他のコマンドによって値が変わる前に、ハンドラーの最初の行で保存してください。

#!/usr/bin/env bash
set -euo pipefail

TMPFILE=$(mktemp /tmp/exitcode.XXXXXX)

cleanup() {
    local original_exit=$?   # capture BEFORE any other command
    echo "Cleaning up (original exit: $original_exit)" >&2
    rm -f "$TMPFILE"
    # Exit with the original code so callers see the real result
    exit "$original_exit"
}
trap cleanup EXIT

echo "Doing work..."
echo "data" > "$TMPFILE"

# Simulate failure
if [[ ! -f /tmp/required_marker ]]; then
    echo "Required marker missing!" >&2
    exit 2
fi

echo "Done."

関数とサブシェル内のトラップ

正しいスクリプトを書くには、トラップの継承を理解することが重要です。

  • 関数は同じシェルで実行され、トラップを継承します。ただしERRが関数に伝播するのは、set -E(errtrace)が有効な場合だけです。
  • サブシェル(( ... ))は親のトラップのコピーを使って開始されますが、サブシェル内での変更は親に影響しません。
  • バックグラウンドジョブ(cmd &)はサブシェルであり、親のINT/TERMトラップを継承しません(デフォルトのシグナル処理が適用されます)。

実践上のルールとして、トラップの登録はメインスクリプトの本体で行い、ERRを関数に伝播させる必要がある場合はset -Eを使用し、&をまたいだトラップの継承には決して依存しないでください。

#!/usr/bin/env bash
set -eEuo pipefail   # -E = errtrace: ERR propagates into functions

on_error() {
    echo "ERR in function or main at line $1" >&2
}
trap 'on_error $LINENO' ERR

risky_function() {
    echo "Inside risky_function"
    ls /nonexistent_path   # triggers ERR — works because of set -E
}

# Subshell: has its own copy; changes don't affect parent
(
    trap '' ERR   # disable ERR only inside subshell
    ls /nonexistent_path 2>/dev/null || true
    echo "Subshell completed without triggering parent ERR"
)

risky_function

実践パターン:ファイルのアトミック置換

防御的なBashでよく使われる典型的なパターンがファイルのアトミック置換です。一時ファイルに書き込んでから、所定の場所へ名前を変更します。名前の変更前に何かが失敗しても、元のファイルは変更されません。トラップを使えば、この処理を堅牢にできます。

手順:

  1. 保存先と同じファイルシステム上に一時ファイルを作成します(mvをアトミックにするためです)。
  2. 早期終了した場合に一時ファイルを削除できるよう、EXITトラップを登録します。
  3. 新しい内容を書き込み、検証します。
  4. アトミックに名前を変更します(mv)。この時点で初めて元のファイルが置き換えられます。
  5. EXITトラップは、一時ファイルがまだ存在する場合にのみ削除します(mvが成功すると、一時ファイルは存在しなくなります)。
#!/usr/bin/env bash
set -euo pipefail

DEST="/tmp/important_config.conf"
TMPFILE=$(mktemp "$(dirname "$DEST")/.tmp.XXXXXX")

cleanup() {
    rm -f "$TMPFILE"   # no-op if mv already moved it
}
trap cleanup EXIT

# Write new content to temp file
cat > "$TMPFILE" <<'EOF'
[settings]
version=2
mode=production
EOF

# Validate before replacing
if ! grep -q 'version=' "$TMPFILE"; then
    echo "Validation failed — aborting replacement" >&2
    exit 1
fi

# Atomic rename — DEST is replaced only here
mv "$TMPFILE" "$DEST"
echo "Config updated atomically: $DEST"

理解度チェック:ERRトラップの継承

Bashの関数でERRトラップがどのように動作するかについての理解度を確認します。

振り返り:クリーンアップとシグナルのためのトラップハンドラー

このレッスンでは、トラップハンドラーを使ってBashスクリプトの耐障害性を高める方法を学びました。

  • EXITは万能の安全網です。一時リソースを作成した直後に登録し、元の終了コードを保持するために必ず最初に$?を保存します。
  • ERRを使うと、$LINENOと$BASH_COMMANDで失敗をログに記録できます。関数に継承させるにはset -Eを使用します。
  • INTでCtrl+Cを適切に処理します。メッセージを表示してコード130で終了し、EXITにクリーンアップを実行させます。
  • 一時リソースを配列にまとめ、1つのクリーンアップ関数ですべて削除します。
  • trap - SIGNALでリセットし、trap '' SIGNALでシグナルを動的に無視します。
  • アトミックな名前変更パターン(mktemp → 書き込み → 検証 → mv)とEXITトラップを組み合わせると、書き込み途中のデータによって保存先ファイルが破損することを防げます。

set -eEuo pipefailと適切に配置したトラップを組み合わせることで、本番環境のBashスクリプトに確かな防御の基盤を作れます。

無料で開始

AI チューターと学ぶ DevOps Bootcamp — 無料

ブラウザでリアルコードを書いて実行し、24/7 の AI チューターから瞬時にサポートを受け、ウェブまたはアプリで続きから学習できます。

コース
142
レッスン
568

よくある質問

「クリーンアップとシグナル処理のためのtrapハンドラー」レッスンは無料ですか?

はい。「クリーンアップとシグナル処理のためのtrapハンドラー」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、DevOps Bootcampコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 DevOps Bootcampコースには全4レッスンが含まれています。

「クリーンアップとシグナル処理のためのtrapハンドラー」で何を学びますか?

EXIT、ERR、INTのtrapを登録し、一時ファイルを削除して、途中までの処理を確実にロールバックします。 ブラウザで直接実行するハンズオンコードでDevOps Bootcampを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。

DevOps Bootcampを始めるのに経験は必要ですか?

事前経験は必要ありません。CoddyKitのDevOps Bootcampは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン2/4です。

「クリーンアップとシグナル処理のためのtrapハンドラー」レッスンにはどのくらい時間がかかりますか?

ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。

このDevOps Bootcampレッスンでコードを書いて実行できますか?

はい。すべてのDevOps Bootcampレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. set -euo pipefailによる厳格モード
  2. クリーンアップとシグナル処理のためのtrapハンドラー
  3. 安全な一時ファイルとロックディレクトリ
  4. 冪等なスクリプトと指数バックオフによるリトライ
← DevOps Bootcampに戻る