Shellスクリプトのtrapコマンドでクリーンアップ処理を自動化する

はじめに

Shellスクリプトで「途中で止められても大丈夫」なプログラムを作るには、trapコマンドを使用する。

trapを使うことで、スクリプトが終了するときやシグナルを受け取ったときに、自動的にクリーンアップ処理を実行できる。

trapとは

trapは、シェルがシグナルを受け取ったときや特定の条件で実行されるコマンドを指定するShellの組み込みコマンドである。

基本構文

trap 'コマンド' シグナル

シグナル一覧

シグナル数値意味発生タイミング
EXIT-スクリプト終了時正常終了・異常終了時
INT2割り込みCtrl+C押下時
TERM15終了要求killコマンド実行時
HUP1ハングアップターミナル切断時
QUIT3終了(コアダンプ)Ctrl+\押下時
USR110ユーザー定義シグナル1プログラムで自由に使用
USR212ユーザー定義シグナル2プログラムで自由に使用

trapを使ってみる

まずは基本的なtrapの使用例から始めてみる。
以下のサンプルスクリプトで実際の動作を確認してみることとする。

サンプルスクリプト:基本的なtrap

#!/bin/bash
# ファイル名: basic_trap.sh

echo "=== trapの基本動作テスト ==="
echo "スクリプトのPID: $$"

# クリーンアップ関数の定義
cleanup() {
    # 二重実行を防ぐフラグ
    if [ "${CLEANUP_DONE:-}" = "true" ]; then
        return 0
    fi
    CLEANUP_DONE=true
    
    echo
    echo "クリーンアップ処理が実行されました"
    echo "一時ファイルを削除中..."
    rm -f "/tmp/test_$$"
    echo "クリーンアップ完了"
    exit 0
}

# trapの設定
trap cleanup EXIT INT TERM

# 一時ファイルの作成
echo "一時ファイルを作成: /tmp/test_$$"
echo "テストデータ" > "/tmp/test_$$"

echo "10秒間処理中... (Ctrl+Cで中断してみてください)"
echo "   または別のターミナルから: kill $$"

# メイン処理(10秒間のループ)
for i in {1..10}; do
    echo "処理中... $i/10"
    sleep 1
done

echo "正常終了"

実行結果

1. 正常終了した場合

$ bash basic_trap.sh
=== trapの基本動作テスト ===
スクリプトのPID: 12345
一時ファイルを作成: /tmp/test_12345
10秒間処理中... (Ctrl+Cで中断してみてください)
   または別のターミナルから: kill 12345
処理中... 1/10
処理中... 2/10
処理中... 3/10
処理中... 4/10
処理中... 5/10
処理中... 6/10
処理中... 7/10
処理中... 8/10
処理中... 9/10
処理中... 10/10
正常終了

クリーンアップ処理が実行されました
一時ファイルを削除中...
クリーンアップ完了

2. Ctrl+Cで中断した場合

$ bash basic_trap.sh
=== trapの基本動作テスト ===
スクリプトのPID: 12346
一時ファイルを作成: /tmp/test_12346
10秒間処理中... (Ctrl+Cで中断してみてください)
   または別のターミナルから: kill 12346
処理中... 1/10
処理中... 2/10
処理中... 3/10
^C
クリーンアップ処理が実行されました
一時ファイルを削除中...
クリーンアップ完了

3. killコマンドで終了した場合

# 端末1
$ bash basic_trap.sh
=== trapの基本動作テスト ===
スクリプトのPID: 12347
一時ファイルを作成: /tmp/test_12347
10秒間処理中... (Ctrl+Cで中断してみてください)
   または別のターミナルから: kill 12347
処理中... 1/10
処理中... 2/10
処理中... 3/10
Terminated

クリーンアップ処理が実行されました
一時ファイルを削除中...
クリーンアップ完了

# 端末2
$ kill 12347

trapの動作フローチャート

正常処理

シグナル受信

Ctrl+C

kill命令

その他

スクリプト開始

trap cleanup EXIT INT TERM

一時ファイル作成

メイン処理開始

処理の状態

正常終了

シグナル種類

INT シグナル

TERM シグナル

その他シグナル

EXIT シグナル

cleanup関数実行

一時ファイル削除

プロセス終了

重要なポイント

  1. EXITシグナル: 正常終了・異常終了に関わらず必ず実行される
  2. INTシグナル: Ctrl+C押下時に実行される
  3. TERMシグナル: killコマンド実行時に実行される
  4. $$変数: 現在のプロセスIDを取得(一時ファイル名のユニーク化に使用)

二重実行の問題と対策

trap cleanup EXIT INT TERM とすると、例えばCtrl+Cで中断した場合:

  1. INTシグナルでcleanup実行
  2. 直後にEXITシグナルでcleanup再実行

この二重実行を防ぐため、上記のサンプルでは CLEANUP_DONE フラグを使用している。

使用例

1. クリーンアップ処理

#!/bin/bash

# 一時ファイルの定義
TEMP_FILE="/tmp/script_temp_$$"

# クリーンアップ関数
cleanup() {
    # 二重実行を防ぐフラグ
    if [ "${CLEANUP_DONE:-}" = "true" ]; then
        return 0
    fi
    CLEANUP_DONE=true
    
    echo "クリーンアップ処理を実行中..."
    [ -f "$TEMP_FILE" ] && rm -f "$TEMP_FILE"
    echo "一時ファイルを削除しました"
    exit 0
}

# trapの設定
trap cleanup EXIT INT TERM

# メイン処理
echo "処理開始: 一時ファイル $TEMP_FILE を作成"
echo "作業中のデータ" > "$TEMP_FILE"

echo "10秒間処理中... (Ctrl+Cで中断してみてください)"
sleep 10

echo "処理完了"

2. プロセス監視とクリーンアップ

#!/bin/bash

# バックグラウンドプロセスのPIDを格納する変数
BG_PID=""

# クリーンアップ関数(詳細版)
cleanup() {
    # 二重実行を防ぐフラグ
    if [ "${CLEANUP_DONE:-}" = "true" ]; then
        return 0
    fi
    CLEANUP_DONE=true
    
    echo
    echo "=== クリーンアップ処理開始 ==="
    
    # バックグラウンドプロセスの終了
    if [ -n "$BG_PID" ] && kill -0 "$BG_PID" 2>/dev/null; then
        echo "バックグラウンドプロセス (PID: $BG_PID) を終了中..."
        kill "$BG_PID"
        wait "$BG_PID" 2>/dev/null
        echo "バックグラウンドプロセスを終了しました"
    fi
    
    # 一時ディレクトリの削除
    if [ -d "/tmp/work_$$" ]; then
        echo "一時ディレクトリを削除中..."
        rm -rf "/tmp/work_$$"
        echo "一時ディレクトリを削除しました"
    fi
    
    echo "=== クリーンアップ処理完了 ==="
    exit 0
}

# 複数のシグナルに対してtrapを設定
trap cleanup EXIT INT TERM HUP

# 作業ディレクトリの作成
mkdir -p "/tmp/work_$$"

# バックグラウンドで長時間処理を実行
(
    while true; do
        date >> "/tmp/work_$$/log.txt"
        sleep 1
    done
) &

# バックグラウンドプロセスのPIDを記録
BG_PID=$!
echo "バックグラウンドプロセス開始 (PID: $BG_PID)"

# メイン処理
echo "メイン処理実行中... (15秒間または Ctrl+C で中断)"
sleep 15

echo "処理完了"

3. ログファイルのローテーション例

#!/bin/bash

LOG_FILE="/tmp/app.log"
OLD_LOG_FILE="/tmp/app.log.old"

# ログローテーション関数
rotate_log() {
    echo "$(date): ログローテーション実行" >> "$LOG_FILE"
    
    if [ -f "$LOG_FILE" ]; then
        mv "$LOG_FILE" "$OLD_LOG_FILE"
        echo "$(date): 新しいログファイル開始" >> "$LOG_FILE"
        echo "ログファイルをローテーションしました"
    fi
}

# USR1シグナルでログローテーション
trap rotate_log USR1

# 通常のクリーンアップ
trap 'echo "アプリケーション終了"; exit 0' EXIT INT TERM

echo "アプリケーション開始 (PID: $$)"
echo "ログローテーションは: kill -USR1 $$"

# メインループ
counter=0
while true; do
    counter=$((counter + 1))
    echo "$(date): ログエントリ #$counter" >> "$LOG_FILE"
    echo "ログエントリ #$counter を記録"
    sleep 2
done

trapの無効化と再設定

trapについて一度設定したものを無効化したり再設定したりできる。
以下ので例で試せる。

#!/bin/bash

# 最初のtrap設定
trap 'echo "最初のクリーンアップ"' EXIT

echo "最初のtrap設定完了"

# trapの確認(bash固有機能)
trap -p EXIT

# trapの無効化
trap - EXIT
echo "trapを無効化しました"

# 新しいtrapの設定
trap 'echo "新しいクリーンアップ"; rm -f /tmp/new_temp' EXIT
echo "新しいtrap設定完了"

# 現在のtrap設定を確認
echo "現在のtrap設定:"
trap -p

シェル互換性について

重要: trap -p は bash 固有の機能で、POSIX準拠のシェル(shdash など)では使用できない。

# POSIX準拠シェルでのtrap確認方法
#!/bin/sh
trap 'echo "クリーンアップ"' EXIT
echo "trap設定完了"

# 引数なしのtrapで現在の設定を表示(POSIX互換)
trap

実行結果例

bashで実行した場合

$ bash trap_management.sh
最初のtrap設定完了
trap -- 'echo "最初のクリーンアップ"' EXIT
trapを無効化しました
新しいtrap設定完了
現在のtrap設定:
trap -- 'echo "新しいクリーンアップ"; rm -f /tmp/new_temp' EXIT
新しいクリーンアップ

shで実行した場合

$ sh trap_management.sh
最初のtrap設定完了
trap_management.sh: 9: trap: Illegal option -p
trapを無効化しました
新しいtrap設定完了
現在のtrap設定:
trap_management.sh: 18: trap: Illegal option -p
最初のクリーンアップ

ベストプラクティスについて

1. エラーハンドリングと組み合わせ

エラーハンドリングとして利用する。

#!/bin/bash

set -euo pipefail  # エラー時即座に終了

cleanup() {
    # 二重実行を防ぐフラグ
    if [ "${CLEANUP_DONE:-}" = "true" ]; then
        return 0
    fi
    CLEANUP_DONE=true
    
    local exit_code=$?
    echo "終了コード: $exit_code"
    
    if [ $exit_code -ne 0 ]; then
        echo "エラーが発生しました"
        # エラー情報をログに記録
        echo "$(date): エラー終了 (exit code: $exit_code)" >> /tmp/error.log
    fi
    
    # クリーンアップ処理
    [ -f "/tmp/work_$$" ] && rm -f "/tmp/work_$$"
}

trap cleanup EXIT

# 危険な処理例
echo "重要な処理を実行中..."
false  # エラーを発生させる例

まとめ

trapの重要なポイント

  1. 必ず設定する: 長時間実行するスクリプトには必須
  2. 複数シグナル対応: EXIT INT TERM は基本セット
  3. 二重実行対策: フラグを使って重複実行を防ぐ
  4. 階層的クリーンアップ: 処理の段階に応じたクリーンアップ
  5. テスト重要: Ctrl+C で中断テストを必ず実行

使い分けガイド

# 簡単なクリーンアップ
trap 'rm -f /tmp/temp_file' EXIT

# 中断可能な処理
trap 'echo "中断されました"; exit 1' INT

# エラーハンドリング  
trap 'cleanup_function' EXIT INT TERM HUP

trapを適切に使用することで、どんな状況でも安全に終了できる堅牢なShellスクリプトが作成できる。

参考

環境

bash 5.x以上(trap -p オプション使用時)
POSIX準拠シェル(sh、dash など)

注意: trap -p は bash 固有機能のため、POSIX準拠シェルでは引数なしの trap を使用する。

おわりに

最近シェルスクリプトを書くことが多いので、trapについて学んでみた。
簡単なスクリプトでもエラーハンドリングを考えると、trapを使わないといけない部分があるのでこのあたりは覚えておいたほうが良いだろう。
特に本番環境で動作するスクリプトや、重要なデータを扱うスクリプトでは必須の機能といえる。 まずは簡単なクリーンアップ処理から始めて、エラーハンドリングから複雑な処理も混ぜて学んでいこうと思う。

Hugo で構築されています。
テーマ StackJimmy によって設計されています。