そういった経験をしたサーバー管理者は多いはずです。
bashスクリプトで本番運用をしていると、途中終了・割り込み・エラー時のクリーンアップは避けて通れません。一時ファイルやロックファイルを作るスクリプトが不意に中断されると、次回実行時に「古いロックファイルが残っている」「tmpディレクトリが膨らんでいる」という問題が起きます。
この記事では、bashの組み込みコマンド
trap を使ったシグナルの捕捉と後処理の方法を解説します。一時ファイルの自動削除、エラー時の強制終了処理、複数シグナルへの対応まで、RHEL 9.4 / Ubuntu 24.04 LTS で動作確認した手順でお伝えします。
この記事のポイント
・trap はbash組み込みコマンドでシグナル受信時に任意の処理を実行できる
・trap "rm -f /tmp/$$.lock" EXIT で終了時に一時ファイルを自動削除できる
・INT/TERM/ERRの3シグナルを押さえれば実務の9割のケースに対応できる
・trap '' SIGINT でシグナルを無視し、重要処理を割り込みから守れる
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
trapコマンドとは何か
trap は bash の組み込みコマンドです。シグナルや特殊イベント(EXIT・ERR等)を受信したときに、あらかじめ登録しておいたコマンドを自動実行する仕組みです。Linuxでは、Ctrl+Cを押すと SIGINT(シグナル番号2)、
kill コマンドの既定は SIGTERM(シグナル番号15) が送られます。何もしなければスクリプトはその場で終了しますが、trap を使えば「終了前に必ずこの処理を実行する」というフックを仕掛けられます。| シグナル名 | 番号 | 発生タイミング |
|---|---|---|
| SIGINT | 2 | Ctrl+C(キーボード割り込み) |
| SIGTERM | 15 | kill コマンドの既定 |
| SIGHUP | 1 | ターミナル切断・nohup無効化 |
| SIGKILL | 9 | 強制終了(trap で捕捉不可) |
| EXIT | — | スクリプト終了時(正常・異常ともに) |
| ERR | — | コマンドが0以外の終了コードで終了した時 |
trapの基本構文
# 基本構文 trap '実行するコマンド' シグナル名またはイベント名 # 複数コマンドは ; で区切るか、関数を呼び出す trap 'echo "中断しました"; exit 1' INT TERM # trapの登録解除(空文字でリセット) trap '' INT # シグナルを無視する(空文字) trap - INT # デフォルト動作に戻す(ハイフン)
最も実用的なパターン: EXIT トラップで一時ファイルを削除する
1. 一時ファイルの安全な削除パターン
本番スクリプトで一時ファイルを扱うときの鉄板パターンです。#!/bin/bash # 一時ファイルを変数で管理 TMPFILE=$(mktemp /tmp/myapp.XXXXXX) # EXIT トラップで確実に削除 cleanup() { echo "クリーンアップ中..." >&2 rm -f "$TMPFILE" } trap cleanup EXIT # 以降の処理(途中でCtrl+Cしても cleanup が呼ばれる) echo "データを処理中..." some_command > "$TMPFILE" cat "$TMPFILE"
・mktemp を使う: /tmp/myapp.$$ でも構いませんが、mktemp は競合しないユニークなファイル名を生成します
・EXIT を使う: INT や TERM を個別に列挙するより、EXIT に登録すれば正常終了・異常終了・シグナル終了のすべてで呼ばれます
・変数は関数の外で定義する: 関数内でローカル宣言すると、trap 登録前に変数が消えているケースがあります
実際にRHEL 9.4の環境で動作確認した出力です。
# スクリプトを実行し、途中でCtrl+Cした場合 $ bash cleanup_demo.sh データを処理中... ^C クリーンアップ中... $ ls /tmp/myapp.* ls: cannot access '/tmp/myapp.*': No such file or directory # ファイルが確実に削除されている
2. ロックファイルを使うスクリプトへの応用
cronで定期実行するスクリプトで二重起動を防ぐロックファイル方式にtrap を組み合わせます。#!/bin/bash LOCKFILE="/var/run/myapp.lock" # ロックファイルの存在チェック if [ -f "$LOCKFILE" ]; then echo "エラー: 別のプロセスが実行中です($LOCKFILE)" >&2 exit 1 fi # ロックファイルを作成し、EXIT で削除する touch "$LOCKFILE" trap 'rm -f "$LOCKFILE"; echo "ロック解除完了"' EXIT echo "処理開始: PID=$$" # ... 本処理 ... sleep 10 echo "処理完了"
ERRトラップでエラーをその場で検出する
3. set -e との組み合わせでエラー処理を強化する
ERR イベントは、コマンドが0以外の終了コードを返したときに発火します。set -e(エラーで即座に終了)と組み合わせると、エラーが発生した行番号を記録できます。#!/bin/bash set -e # エラーで即終了 # ERR トラップでエラー行番号とコマンドを記録 error_handler() { local exit_code=$? local line_no=$1 echo "[ERROR] 行 ${line_no} でエラーが発生しました(終了コード: ${exit_code})" >&2 echo "[ERROR] コマンド: ${BASH_COMMAND}" >&2 } trap 'error_handler $LINENO' ERR # テスト: 存在しないディレクトリへ移動 cd /nonexistent_dir echo "ここには到達しない"
# 実行結果 $ bash error_demo.sh bash: cd: /nonexistent_dir: No such file or directory [ERROR] 行 12 でエラーが発生しました(終了コード: 1) [ERROR] コマンド: cd /nonexistent_dir
$LINENO は現在の行番号を保持するbash変数です。$BASH_COMMAND はtrapが呼ばれた直前に実行されたコマンド文字列を保持します。これらを組み合わせると、ログから問題箇所を素早く特定できます。INT・TERM・EXITを組み合わせた堅牢なスクリプト例
4. 実務で使える完全なテンプレート
以下は、一時ファイル管理・ロックファイル・エラーログを組み合わせた実務レベルのテンプレートです。#!/bin/bash set -eu # -e: エラーで終了 / -u: 未定義変数でエラー SCRIPT_NAME=$(basename "$0") LOCKFILE="/var/run/${SCRIPT_NAME%.sh}.lock" LOGFILE="/var/log/${SCRIPT_NAME%.sh}.log" TMPFILE=$(mktemp) log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOGFILE"; } cleanup() { local exit_code=$? rm -f "$TMPFILE" "$LOCKFILE" if [ "$exit_code" -ne 0 ]; then log "異常終了 (exit_code=${exit_code})" else log "正常終了" fi } trap cleanup EXIT error_handler() { log "ERR: 行 $1 / コマンド: ${BASH_COMMAND}" } trap 'error_handler $LINENO' ERR # 二重起動チェック if [ -f "$LOCKFILE" ]; then log "別プロセスが実行中のためスキップ" exit 0 fi touch "$LOCKFILE" log "処理開始" # --- ここに本処理を書く --- log "処理完了"
シグナルを無視する方法
5. 重要処理を割り込みから保護する
データベースへの書き込みやファイルのコピー中に割り込まれると、データが壊れる場合があります。そういった「途中で止まってはいけない処理」では、シグナルを一時的に無視する方法が有効です。#!/bin/bash echo "保護区間の前: Ctrl+C で中断できます" sleep 3 # シグナルを無視(空文字でtrap) trap '' INT TERM echo "保護区間開始: ここではCtrl+Cが無効です" sleep 5 # 重要な処理(DBバックアップ等) echo "保護区間終了" # デフォルト動作に戻す trap - INT TERM echo "ここからはCtrl+Cで中断できます" sleep 3
trapのトラブルシュート
「trapが呼ばれない」場合のチェックポイント
SIGKILL(kill -9)を使っていないか:kill -9 はtrapを完全にバイパスします。送り側のスクリプトや手動操作で kill -9 が使われていないか確認してください。
サブシェルで実行していないか:
( trap ... ) のようにサブシェル内でtrapを登録しても、親シェルには影響しません。# NG: サブシェル内のtrapは親シェルに影響しない ( trap 'echo "サブシェル"' EXIT ) # OK: 親シェルで直接trap trap 'echo "親シェル"' EXIT
grep や diff は一致なし・差異なしの場合に終了コード1を返します。set -e 環境では意図せず終了することがあります。# NG: grep が0件マッチで終了コード1 → set -e でスクリプト終了 grep "pattern" file.txt # OK: || true で終了コードを0に揃える grep "pattern" file.txt || true
# 現在のtrap設定を一覧表示 trap -p # 特定のシグナルだけ確認 trap -p EXIT INT TERM
trapコマンド早見表
| やりたいこと | コマンド |
|---|---|
| 終了時に一時ファイルを削除する | trap 'rm -f "$TMPFILE"' EXIT |
| Ctrl+C 時にメッセージを表示して終了 | trap 'echo "中断しました"; exit 1' INT |
| エラー発生行を記録する | trap 'echo "行 $LINENO: $BASH_COMMAND" >&2' ERR |
| SIGINTを無視して割り込みを防ぐ | trap '' INT |
| デフォルト動作に戻す | trap - INT TERM |
| 現在のtrap設定を確認する | trap -p |
| 複数シグナルに同じ処理を設定する | trap 'cleanup' EXIT INT TERM |
trap は欠かせないコマンドです。まずは「EXIT トラップで一時ファイルを削除する」パターンから導入してみてください。既存のスクリプトに数行追加するだけで、運用上の問題の大半を防げます。bashスクリプトの引数処理については getoptsコマンドでbashスクリプトの引数を処理する方法 も参考にしてください。
cronで定期実行するスクリプトを運用する場合は、chkconfigとsystemctlの使い方 と合わせてサービス管理も確認しておきましょう。
プロセスへのシグナル送信については Linux ポート確認の全コマンド の関連でも触れている Linux 基本コマンドの解説 を参照してください。
「trap を使いこなして、安心して動かせるスクリプトを書きたい」なら
trap を使えば、スクリプトの信頼性は確実に上がります。
ネットの古い情報をコピペするだけでなく、現場で通用する安全なLinuxサーバー構築の「型」を体系的に身につけたい方へ、『Linuxサーバー構築入門マニュアル(図解60P)』を完全無料でプレゼントしています。
「独学の時間がもったいない」「プロから直接、現場の技術を最短で学びたい」という本気の方には、2日で実務レベルのスキルが身につく【初心者向けハンズオンセミナー】も開催しています。
3,100名以上が実践した「型」を無料で公開中
プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。
登録10秒/合わなければ解除3秒 / 詳細はこちら
- 次のページへ:timeoutコマンドで制限時間付きでコマンドを実行する方法|ハングアップ対策とシェルスクリプト活用も
- 前のページへ:getoptsコマンドでbashスクリプトの引数を処理する方法|オプション解析と実践例も
- この記事の属するカテゴリ:Linuxtips・シェルスクリプトへ戻る

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