シェルスクリプトのtrap DEBUGでコマンド実行ログを自動記録する設計|BASH_COMMANDとタイムスタンプでスクリプトの動作を可視化する方法

運営:リナックスマスター.JP(代表 宮崎智広)
HOME > Linux技術 リナックスマスター.JP(Linuxマスター.JP) > シェルスクリプト > シェルスクリプトのtrap DEBUGでコマンド実行ログを自動記録する設計|BASH_COMMANDとタイムスタンプでスクリプトの動作を可視化する方法
「スクリプトが途中で止まったのに、どのコマンドで失敗したのか全く分からない」
ログには最後のエラーメッセージだけが残り、何十ものコマンドのうちどこで問題が起きたか追跡できない。複雑な自動化スクリプトほど、こうした状況に陥りやすい。

この記事では、bashのtrap DEBUGとBASH_COMMAND変数を組み合わせたコマンド実行ログの自動記録設計を解説します。RHEL 9系/Ubuntu 24.04 LTSを想定環境として説明します。
set -xとは異なるアプローチで、スクリプトの動作をタイムスタンプ付きのログとしてファイルに残す実装パターンを紹介します。

この記事のポイント

・trap DEBUGはコマンド実行直前に毎回呼ばれるbashの疑似シグナルである
・BASH_COMMAND変数には「これから実行されるコマンド文字列」が格納される
・タイムスタンプと組み合わせてファイルに書くと長時間バッチの実行追跡に使える
・set -xと違い、ログの書き先・フォーマット・フィルタリングを自由に設計できる


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

trap DEBUGとBASH_COMMANDの仕組み

bashにはEXIT・ERR・SIGTERMといったシグナル以外に、DEBUGという疑似シグナルがある。trap 関数名 DEBUGをセットすると、スクリプト内でコマンドが実行される直前に毎回、登録した関数が呼ばれる。

このときbashが自動的にセットする組み込み変数BASH_COMMANDには、「これから実行されるコマンド文字列」がそのまま格納されている。例えば次のシンプルなスクリプトを実行してみる。

#!/bin/bash trap 'echo "CMD: $BASH_COMMAND"' DEBUG echo "hello" ls /tmp date

出力例(環境により異なります)

CMD: echo "hello" hello CMD: ls /tmp (/tmpの内容) CMD: date 2026年10月31日 09:00:00 JST

echo・ls・dateが実行される直前にtrap DEBUGハンドラが呼ばれ、BASH_COMMANDの内容が出力されていることが分かる。

trap DEBUGが呼ばれるタイミングは「コマンド実行直前」であって「実行後」ではない。そのため「コマンドが成功したかどうか」の判定は、trap DEBUG単体ではできない。終了ステータスを取りたい場合はtrap ERRやPIPESTATUSを別途組み合わせる。

基本的な使い方

1. DEBUGトラップを設定する

最小の構成は関数を1つ定義してtrapに登録するだけだ。

#!/bin/bash _debug_log() { echo "[DEBUG] $BASH_COMMAND" } trap '_debug_log' DEBUG

シングルクォートで囲んでtrapに登録することが重要だ。ダブルクォートだとtrap登録時点でBASH_COMMANDが展開されてしまい、その後のすべての呼び出しで同じ文字列が出力される誤りに陥る。trap '...' DEBUGのシングルクォートにより、BASH_COMMANDはコマンド実行のたびに評価される。

2. タイムスタンプ付きコマンドログを実装する

実務では実行時刻を付与することで「どの時点でどの処理にいたか」が後から把握できるようになる。

#!/bin/bash _debug_log() { printf '%(%Y-%m-%dT%H:%M:%S)T [CMD] %s\n' -1 "$BASH_COMMAND" } trap '_debug_log' DEBUG

printf '%(%Y-%m-%dT%H:%M:%S)T' -1はbash 4.2以降で使えるタイムスタンプ取得の組み込み機能だ。外部コマンドのdateを毎回フォークしないため、$(date +...)より高速で動く。ハンドラ内でdateを呼ぶと「コマンド数の2倍のプロセスフォーク」が発生してスクリプト全体の速度が下がるため、できるかぎりbash組み込みで完結させる。
出力例(環境により異なります)

2026-10-31T09:00:01 [CMD] ls /var/log 2026-10-31T09:00:01 [CMD] gzip /var/log/messages.1 2026-10-31T09:00:02 [CMD] find /var/log -mtime +30 -delete

3. ログをファイルに書き出す設計

標準エラーではなく専用のログファイルに書き出すにはリダイレクトを使う。

#!/bin/bash LOG_FILE="/var/log/myscript-debug.log" _debug_log() { printf '%(%Y-%m-%dT%H:%M:%S)T [CMD] %s\n' -1 "$BASH_COMMAND" >> "$LOG_FILE" } trap '_debug_log' DEBUG # スクリプト開始時にログを初期化する : > "$LOG_FILE"

: > "$LOG_FILE"は実行のたびにログを空にする書き方だ。古いログが積み重なって肥大化しないよう、スクリプト開始時に必ず初期化する設計を入れておく。本番では日付ローテーションと組み合わせて運用することが多い。

set -xとの違いと使い分け

set -xとtrap DEBUGはどちらも「実行されるコマンドを追跡する」機能だが、設計上の特性が大きく異なる。
比較項目 set -x trap DEBUG
出力先 標準エラー(固定) 自由(ファイル・変数・フィルタ可)
フォーマット PS4変数で制限あり 完全に自由(printfで整形可)
フィルタリング 不可(全コマンドが対象) 可(条件付きで特定区間のみ記録)
パフォーマンス影響 低い 中程度(関数呼び出しが発生)
主な用途 開発中・対話的なデバッグ確認 本番ログ・監査ログ・実行追跡
開発中に手元ですぐ確認したいならset -xが手軽だ。一方、本番スクリプトの実行を構造化したログファイルに記録しておきたい場合はtrap DEBUGが適している。シェルスクリプトの設計全般を体系的に習得したい方はシェルスクリプト実践講座も参照してほしい。

実務での活用パターン

1. 長時間バッチスクリプトの実行追跡

夜間に動くバックアップや集計スクリプトが「想定の2倍かかっている」という状況が起きたとき、どのコマンドに時間がかかったかを特定するのは難しい。trap DEBUGでタイムスタンプ付きのログを残しておけば、後から各コマンドの実行時刻を確認して詰まり箇所を特定できる。

#!/bin/bash LOG="/var/log/batch-trace.log" _trace() { printf '%(%Y-%m-%dT%H:%M:%S)T [TRACE] %s\n' -1 "$BASH_COMMAND" >> "$LOG" } trap '_trace' DEBUG : > "$LOG" # 実際の処理 rsync -av /data/source/ /data/backup/ tar -czf /data/backup/snapshot.tar.gz /data/source/ find /data/backup/ -mtime +30 -delete

rsync・tar・findのそれぞれが$LOGに記録される。rsyncに想定外の時間がかかっていれば、ログの時刻差からすぐに特定できる。

2. 特定の処理区間だけをトレースする

スクリプト全体をトレースするとログが膨大になる。危険な操作(大量削除・DB更新・権限変更)だけを記録したい場合は、フラグで区間を管理する設計が有効だ。

#!/bin/bash LOG="/var/log/sensitive-ops.log" _TRACE_ON=0 _trace() { [[ $_TRACE_ON -eq 1 ]] || return 0 printf '%(%Y-%m-%dT%H:%M:%S)T [SENSITIVE] %s\n' -1 "$BASH_COMMAND" >> "$LOG" } trap '_trace' DEBUG : > "$LOG" echo "前処理(ログ対象外)" some_safe_preparation # ここからトレース開始 _TRACE_ON=1 find /var/www -name "*.tmp" -delete chown -R www-data:www-data /var/www/html # ここでトレース終了 _TRACE_ON=0 echo "後処理(ログ対象外)"

_TRACE_ON=1の区間のコマンドだけがログに記録される。ハンドラ内のreturn 0もDEBUGトラップを呼ぶが、フラグ判定を単純な整数比較にとどめることで余分なネストを防げる。

3. サブシェルへのDEBUGトラップ継承設計

bashのtrapはデフォルトではサブシェルに継承されない。( ... )やコマンド置換$( ... )の中ではtrap DEBUGが無効になる。関数内やサブシェルでもトラップを有効にしたい場合はset -T(functrace)と組み合わせる。

#!/bin/bash # set -T でERRトラップとDEBUGトラップをサブシェル・関数に継承する set -T LOG="/var/log/full-trace.log" _trace() { printf '%(%Y-%m-%dT%H:%M:%S)T [TRACE] %s\n' -1 "$BASH_COMMAND" >> "$LOG" } trap '_trace' DEBUG : > "$LOG" do_work() { # set -T によりこの関数内でもtrap DEBUGが有効になる cp /src/app.conf /etc/myapp/ chmod 640 /etc/myapp/app.conf } do_work

set -Tを使うと関数・サブシェル・コマンド置換でもDEBUGトラップが有効になる。全コマンドが対象になりログ量が増えるため、継承が本当に必要な場合にのみ有効にすることを推奨する。

注意点とトラブルシュート

パフォーマンスへの影響
trap DEBUGはコマンドごとにハンドラ関数を呼び出すため、コマンド数が多いループではオーバーヘッドが積み重なる。ハンドラ内では外部コマンドを呼ばず、printf '%(%Y-%m-%dT%H:%M:%S)T'などのbash組み込みだけで完結させるのが鉄則だ。

ハンドラ内の再帰呼び出し問題
_trace関数の中でも再びDEBUGトラップが発火するため、意図せず大量のログが生成されることがある。これを防ぐには、ハンドラで使うコマンドをbash組み込み(printf・local・[[ ]])だけに絞るのが最も現実的な設計だ。どうしても外部コマンドが必要な場合は、ハンドラ先頭でtrap '' DEBUG(空トラップで一時無効化)してから処理し、末尾で再登録する方法もあるが実装が複雑になる。

BASH_COMMANDの内容が期待と異なるケース
for・whileのヘッダー行・条件式[[ ... ]]・算術式(( ... ))は、式全体として1コマンド分のDEBUGトラップを発火させる。ループ本体の各コマンドも個別に記録されるため、ログを解析する際は「ループヘッダー+本体コマンド」が交互に並ぶことを念頭に置く。

パイプライン中のBASH_COMMANDの挙動
cmd1 | cmd2のようなパイプラインでは、cmd1とcmd2がそれぞれ個別にDEBUGトラップを発火させる。パイプライン全体の成否はPIPESTATUSで別途確認する必要がある。

本記事のまとめ

やりたいこと 実装のポイント
コマンド実行を追跡したい trap '_func' DEBUG + $BASH_COMMAND
タイムスタンプ付きでログに残したい printf '%(%Y-%m-%dT%H:%M:%S)T' -1 でbash組み込み時刻を取得
特定の処理区間だけを記録したい フラグ変数でDEBUGハンドラの出力を制御する
関数内でもトラップを有効にしたい set -T(functrace)をスクリプト先頭に設定する
set -xと使い分けたい 開発中はset -x、本番ログ記録はtrap DEBUGが適切
パフォーマンスを下げたくない ハンドラをbash組み込みコマンドのみで実装する
trap DEBUGはset -xとは異なり、出力フォーマット・書き先・フィルタリングをすべて設計者が制御できるのが最大の強みだ。長時間バッチの実行追跡や、危険な操作だけを監査ログに残す設計など、運用現場の要件に合わせた柔軟なロギングを実現できる。
現場で通用する安全なLinuxサーバー構築の「型」を体系的に身につけたい方へ、シェルスクリプトの設計力を実務レベルで高める講座を用意しています。trap DEBUGのような設計パターンも含め、現場で壊れないスクリプトの書き方を体系的に学べるシェルスクリプト実践講座はこちら>>

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

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

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

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

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

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

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

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

運営

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

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