trap 入門 - シグナルを捕捉してクリーンアップ処理を書く

trap 入門 - シグナルを捕捉してクリーンアップ処理を書く

この記事で解決できること

  • trapシグナルを捕捉 し、確実に後始末を走らせる型が分かる
  • 「Ctrl-C で中断したら一時ファイルが残った」という 典型的な事故を防ぐ 方法が身につく
  • EXIT 疑似シグナル・cleanup 関数・set -e を組み合わせた 実務テンプレ が手に入る

結論(実務の型)

  • 後始末は trap cleanup EXIT 一本に寄せる(どんな終わり方でも走る)
  • 一時ファイルは mktemp で作り、生成直後に trap を仕掛ける
  • SIGKILL(9)と SIGSTOP捕捉できない。それ以外で対処する

前提(対象環境)

  • シェル:bash(POSIX sh でも EXIT / INT / TERM は共通で動作)
  • 対象:シェルスクリプトを書く中級者

trap とは何か?

結論: trap はシグナルや特殊イベントを受け取ったときに実行するコマンドを登録する bash 組み込みコマンド。後始末の予約に使う。

trap は「特定のシグナルを受け取ったら、このコマンドを実行する」という 割り込みハンドラを登録する 組み込みコマンド。基本構文は次の形。

trap 'コマンド' シグナル名...

たとえば Ctrl-C(SIGINT)を捕まえてメッセージを出す例。

trap 'echo "中断されました"' INT

シグナル名は SIGINT でも INT でも数値 2 でも指定できる。SIG プレフィックスは省略可能で、移植性の観点からは名前(INT / TERM / EXIT)での指定が推奨される。

第 1 引数はシェルが評価する 文字列。シングルクォートで囲むと「シグナルを受け取った時点」で展開される。ダブルクォートにすると trap を仕掛けた時点で変数が展開されるため、後始末用の変数を埋め込むなら挙動の違いに注意する。

なぜ EXIT 疑似シグナルを使うのか?

結論: EXIT はスクリプトが終了する瞬間に必ず走る疑似シグナル。正常終了・エラー終了・Ctrl-C のどれでも実行されるため、後始末はここに集約するのが最も堅い。

実際のシグナル(INT / TERM)を一つずつ捕まえると、捕まえ漏れたシグナルで後始末がスキップされる。EXITシグナルではなく「スクリプトが終わる」というイベント に反応するため、終わり方を問わず一度だけ実行される。

#!/usr/bin/env bash
tmpfile=$(mktemp)
trap 'rm -f "$tmpfile"' EXIT

# ここで何が起きても(正常終了 / set -e のエラー終了 / Ctrl-C)
# スクリプト終了時に tmpfile は必ず削除される
echo "作業中..." > "$tmpfile"
cat "$tmpfile"

EXIT トラップは SIGKILL(kill -9)では走らないSIGKILL はプロセスを即座に強制終了するためハンドラを実行する余地がない。これは trap の限界として理解しておく。

どうやってクリーンアップ関数を書くのか?

結論: 後始末を cleanup 関数にまとめ、trap cleanup EXIT で登録する。複数の一時リソースがあっても 1 箇所で管理できる。

インラインのコマンド文字列は短い処理なら良いが、削除対象が増えると可読性が落ちる。関数に切り出す のが実務の定番。

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

workdir=$(mktemp -d)
lockfile="/tmp/myjob.lock"

cleanup() {
  local rc=$?          # 直前の終了コードを保存
  rm -rf "$workdir"
  rm -f "$lockfile"
  echo "クリーンアップ完了 (exit=$rc)"
  exit "$rc"           # 元の終了コードで終わる
}
trap cleanup EXIT

# --- 本処理 ---
touch "$lockfile"
echo "data" > "$workdir/output.txt"

ポイントは関数冒頭の local rc=$?cleanup 内で別のコマンドを実行すると $? が上書きされるため、最初に終了コードを退避 し、最後に exit "$rc" で元のコードを返す。これで「後始末はするが、エラーはエラーとして呼び出し元に伝える」挙動になる。

mktemp -d で作業ディレクトリを作り、rm -rf "$workdir" でまとめて消す型は、個別の一時ファイルを追跡するより安全。workdir が空文字だと rm -rf が暴発するため、set -u で未定義変数をエラーにしておくと事故を防げる。

複数のシグナルを使い分けるには?

結論: 後始末は EXIT に集約し、中断時の挙動を変えたい場合だけ INT / TERM を個別に捕捉する。捕捉できないシグナルもある。

主要なシグナルと用途を整理する。

シグナル 番号 送られる契機 捕捉
INT 2 Ctrl-C
TERM 15 kill の既定
HUP 1 端末切断
QUIT 3 Ctrl-\
EXIT 0 スクリプト終了(疑似)
KILL 9 kill -9 不可
STOP 19 プロセス一時停止 不可

INT を受けたときだけ独自メッセージを出し、後始末自体は EXIT に任せる例。

#!/usr/bin/env bash
tmpfile=$(mktemp)
trap 'rm -f "$tmpfile"' EXIT
trap 'echo "ユーザーにより中断されました"; exit 130' INT

echo "処理中... Ctrl-C で中断できます"
sleep 30

INT ハンドラ内の exit 130 でスクリプトを終わらせると、続けて EXIT トラップが走って tmpfile が消える。慣例として INT での終了コードは 128 + 2 = 130 を使う。

SIGKILL(9)と SIGSTOP(19)は OS 仕様で捕捉・無視・再定義のいずれも不可能。「kill -9 でも掃除したい」という要求は trap では満たせない。別プロセスの監視や systemd の ExecStopPost= 等、外側の仕組みで対処する。

set -e と組み合わせて安全にするには?

結論: set -e(エラー即終了)と trap は相性が良い。途中でコマンドが失敗してもトラップが後始末を実行するため、中途半端な状態を残さない。

set -e を付けると、コマンドが失敗した時点でスクリプトが終了する。このとき EXIT トラップは走るので、後始末は保証される。さらに ERR 疑似シグナル(bash 拡張)を使うと、どのコマンドで失敗したか を記録できる。

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

workdir=$(mktemp -d)
trap 'rm -rf "$workdir"' EXIT
trap 'echo "エラー: 行 $LINENO で失敗 (exit=$?)" >&2' ERR

cp /etc/hostname "$workdir/"
grep "存在しない文字列" "$workdir/hostname"   # ここで失敗 → ERR と EXIT が走る
echo "ここには到達しない"

ERR トラップで失敗を通知し、EXIT トラップで後始末する二段構えが、防御的スクリプトの基本形。

set -o pipefail を併用すると、パイプライン途中の失敗も検出できる。set -euo pipefail + trap cleanup EXIT はシェルスクリプトの堅牢化テンプレートとして覚えておくと良い。

trap の状態を確認・解除するには?

結論: trap -p で設定中のトラップを一覧表示し、trap - シグナル名 で解除する。デバッグ時に役立つ。

現在仕掛けられているトラップは trap -p で確認できる。

$ trap 'rm -f /tmp/x' EXIT
$ trap -p
trap -- 'rm -f /tmp/x' EXIT

特定のシグナルのトラップを 解除 するには、コマンド部分を - にする。

trap - EXIT       # EXIT トラップを解除(既定動作に戻す)

シグナルを 無視 したい(届いても何もしない)場合は、コマンド部分を空文字にする。

trap '' INT       # Ctrl-C を無効化する

trap '' INT で無視に設定すると、その状態は子プロセスにも継承される。スクリプトの一部だけ Ctrl-C を無効化したいなら、クリティカルセクションを抜けた後に trap - INT で元に戻すこと。

まとめ:trap 安全テンプレート

結論: mktemp で一時リソースを作り、直後に trap cleanup EXIT を仕掛け、cleanup 関数で終了コードを保存してから後始末する。これが事故らない型。

コピペ用:堅牢なスクリプトの骨格

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

workdir=$(mktemp -d)

cleanup() {
  local rc=$?
  rm -rf "$workdir"
  exit "$rc"
}
trap cleanup EXIT
trap 'echo "中断" >&2; exit 130' INT

# --- ここに本処理を書く ---
echo "scratch" > "$workdir/tmp.txt"

押さえるべき要点は 3 つ。

  • 後始末は EXIT に集約(正常・異常・中断のすべてで走る)
  • 終了コードは cleanup 冒頭で local rc=$? 退避し、最後に exit "$rc"
  • SIGKILL / SIGSTOP は捕捉不能。それを前提に設計する

次のステップとして、trap を使った実用スクリプト(ロックファイル管理・定期ジョブ)を getopts 入門cron の基本 と組み合わせて書いてみると理解が深まる。

次に読む