「どこで止まっているのか、原因が全くわからない」
シェルスクリプトのデバッグは、初心者だけでなく中級者でも手が止まりやすいポイントです。Linuxサーバーの運用自動化を進めるほど、スクリプトが複雑になり、バグの原因追跡が難しくなります。
この記事では、Bashの組み込みオプション
set -x(トレースモード)を中心に、set -e(エラー即終了)・set -u(未定義変数をエラーに)・trap ERR(エラー時のクリーンアップ)との組み合わせによるデバッグ・安全化の手法を解説します。RHEL 9.4 / Ubuntu 24.04 LTS で動作確認済みの実践的な内容です。この記事のポイント
・set -x を使うと実行コマンドがトレース表示されデバッグが容易になる
・set -e でスクリプトをエラー発生時に即終了させ被害を最小化できる
・set -u で未定義変数を早期検知し変数名ミスのバグを防げる
・trap ERR/EXIT を加えるとエラー時の後片付けを確実に実行できる
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
なぜシェルスクリプトのデバッグは難しいのか
Bashスクリプトはデフォルトでは「エラーが起きても処理を続行する」という仕様になっています。つまり、途中でコマンドが失敗しても何事もなかったように次の行に進んでしまうのです。例えば次のスクリプトを考えてみてください。
#!/bin/bash # バグを含むスクリプトの例 cp /etc/hosts /tmp/hosts_backup rm /tmp/hossts_backup # hostsをhosstsとスペルミス echo "バックアップ完了"
このような「失敗しているのに成功したように見える」状況は、本番環境では深刻な事故につながります。Postfix の設定やDNS 設定の自動化スクリプトでこの問題が発生すると、サービス停止につながりかねません。
デバッグオプションで「何が起きているか見える化する」と同時に、エラーが起きた時点で即座に止める仕組みを組み合わせるのが、現場で通用するアプローチです。
set -x の基本的な使い方
1. set -x とは何か
set -x はBashのシェルオプションの一つで、「トレースモード(xtrace)」とも呼ばれます。このオプションを有効にすると、スクリプト内で実行される全てのコマンドが実行前に標準エラー出力(stderr)へ表示されます。表示形式は「+ コマンド」の形式です。+ マークが、実行された(またはこれから実行される)コマンドを示しています。
2. スクリプト内で有効にする方法
最もよく使う方法は、スクリプトの先頭でset -x を宣言することです。#!/bin/bash set -x BACKUP_DIR="/tmp/backup" mkdir -p "$BACKUP_DIR" cp /etc/hosts "$BACKUP_DIR/hosts" echo "バックアップ完了"
$ bash debug_test.sh + BACKUP_DIR=/tmp/backup + mkdir -p /tmp/backup + cp /etc/hosts /tmp/backup/hosts + echo 'バックアップ完了' バックアップ完了
+ マークが「set -x で表示されたトレース行」を示しています。変数への代入も含め、全ての操作が順番に記録されるため、どこで何が起きているかが一目でわかります。3. コマンドラインオプションで有効にする方法
スクリプトを修正せずにデバッグしたい場合は、実行時に-x オプションを渡す方法が便利です。# スクリプトを直接実行する場合 bash -x スクリプト名.sh # shebangがある場合も同様 bash -x /usr/local/bin/backup.sh
4. 特定の箇所だけトレースを有効にする
スクリプト全体ではなく、問題が疑われる部分だけトレースしたい場合は、set -x と set +x(無効化)を組み合わせます。#!/bin/bash echo "ここはトレースしない" set -x # トレース開始 DB_HOST="db.example.com" mysql -h "$DB_HOST" -u root -p"${DB_PASS}" mydb -e "SHOW TABLES;" set +x # トレース終了 echo "ここもトレースしない"
set +x で一時的にトレースを止めるのが現場の鉄則です。set -e でエラー時に即終了させる
5. set -e とは何か
set -e(errexit)は、スクリプト内のコマンドが0以外の終了コードを返したとき、スクリプト全体を即座に終了させるオプションです。「失敗しても続行する」というBashのデフォルト動作を変え、「失敗したら止まる」という安全な動作に切り替えます。
#!/bin/bash set -e mkdir /tmp/mydir cp /etc/hosts /tmp/mydir/ # ここで cp が失敗すると、以降の処理は実行されない echo "ファイルコピー完了"
6. set -e の注意点と落とし穴
set -e が有効な状態でも「終了コードを無視したい」場合があります。その場合は || true または || : を使います。#!/bin/bash set -e # grep が見つからなくても続行させたい場合 grep "WARN" /var/log/messages || true # コマンドが失敗しても続けたい場合 rm -f /tmp/old_lock || true echo "処理完了"
if文の条件部分では、失敗した場合に else へ分岐するため set -e の影響を受けません。#!/bin/bash set -e # if の条件部はエラーでもスクリプトが終了しない if grep -q "ERROR" /var/log/app.log; then echo "エラーを検知しました" fi
set -e が適用されないケースは他にもあります。while・until・for などのループ条件部や、||・&& で接続されたコマンドの左辺も、失敗してもスクリプトが終了しません。このため set -e 単体ですべてのエラーを拾えるわけではなく、後述の set -u・set -o pipefail との組み合わせが重要です。set -u で未定義変数をエラーにする
7. set -u とは何か
set -u(nounset)は、未定義の変数を参照したときにエラーとして扱うオプションです。Bashのデフォルト動作では、未定義の変数は空文字列として展開されます。これが原因で発生する典型的な事故を見てみましょう。
#!/bin/bash # 危険なスクリプトの例(set -u なし) TARGET_DIR="$TARGETT_DIR" # スペルミス:TARGETT_DIR は未定義 # TARGET_DIR が空文字のまま渡されると / 配下を全削除する壊滅的事故につながる rm -rf "${TARGET_DIR:?'ディレクトリ未設定'}/old"
set -u があれば、未定義変数を参照した時点でスクリプトが停止します。$ bash test.sh test.sh: line 2: TARGETT_DIR: unbound variable
8. デフォルト値の指定と set -u の共存
set -u を有効にした状態で変数にデフォルト値を設定したい場合は、${変数名:-デフォルト値} という書き方を使います。#!/bin/bash set -u # LOG_LEVEL が未設定の場合は "INFO" を使う LOG_LEVEL="${LOG_LEVEL:-INFO}" # BACKUP_DIR が未設定の場合は /tmp/backup を使う BACKUP_DIR="${BACKUP_DIR:-/tmp/backup}" echo "ログレベル: $LOG_LEVEL" echo "バックアップ先: $BACKUP_DIR"
3つの組み合わせ:set -eux が現場の定番
9. set -eux を一行で書く
実務のシェルスクリプトでは、set -e・set -u・set -x の3つを組み合わせた set -eux(または set -euxo pipefail)がスクリプト冒頭の定番になっています。#!/bin/bash set -eux # e: エラーで即終了 # u: 未定義変数をエラー # x: コマンドをトレース表示 DEPLOY_DIR="/var/www/html" BACKUP_DIR="/var/www/backup" mkdir -p "$BACKUP_DIR" cp -r "$DEPLOY_DIR" "$BACKUP_DIR/$(date +%Y%m%d)" echo "デプロイバックアップ完了"
10. pipefail との組み合わせ
パイプを使うコマンドではset -e だけでは不十分な場合があります。パイプの途中でエラーが起きても、最後のコマンドが成功すれば全体の終了コードが0になってしまうためです。set -o pipefail を追加することで、パイプの途中のコマンドが失敗した場合にも終了コードにエラーが伝播します。#!/bin/bash set -euxo pipefail # pipefail なし:cat が失敗しても gzip が成功すれば全体は成功扱いになる # pipefail あり:cat が失敗した時点でスクリプトが終了する cat /nonexistent_file | gzip > output.gz # パイプ途中の失敗も検知できる grep "ERROR" /var/log/app.log | wc -l
set -euxo pipefail を1行目に書くのが最も安全なスタートラインです。trap ERR / EXIT でエラー時に後片付けをする
11. trap コマンドの基本
trap コマンドはシグナルや特殊なイベントが発生したときに実行する処理を登録できます。シェルスクリプトのエラーハンドリングでよく使うのは ERR(コマンド失敗時)と EXIT(スクリプト終了時)の2つです。set -e・set -u・set -o pipefail でエラーを検知して即終了させることができますが、終了した後の「後片付け」(一時ファイルの削除・ロックファイルの解放など)は別途手当が必要です。そこで trap EXIT を使います。#!/bin/bash set -euo pipefail # エラー発生時の処理を登録 trap 'echo "エラー: 行 $LINENO で失敗しました"' ERR # スクリプト終了時(正常・異常問わず)の後片付けを登録 cleanup() { echo "後片付けを実行します" rm -f /tmp/work_$$ # プロセスID付き一時ファイルを削除 } trap 'cleanup' EXIT echo "処理開始" cp /nonexistent_file /tmp/ # ここで失敗 echo "ここには到達しない"
処理開始 cp: cannot stat '/nonexistent_file': No such file or directory エラー: 行 13 で失敗しました 後片付けを実行します
$LINENO はエラーが発生した行番号を保持する特殊変数です。ログに残すことで、後から原因を特定しやすくなります。12. trap ERR と trap EXIT の使い分け
・trap ERR:コマンド失敗時のみ実行されます。エラーメッセージの表示やログ記録に使います。・trap EXIT:スクリプト終了時に必ず実行されます(正常終了でも異常終了でも)。一時ファイルの削除やロックファイルの解放に使います。
・trap INT:Ctrl+Cによる中断(シグナル2)時に実行されます。対話型スクリプトでの割り込み処理に使います。
「後片付けは成功・失敗を問わず必ず実行したい」場合は
trap EXIT を使い、「エラー発生時だけ通知したい」場合は trap ERR を使うのが基本的な使い分けです。複数の trap を設定する場合、シグナルごとに独立して動作します(後から設定したものが上書きするわけではありません)。13. $BASH_COMMAND・$LINENO でエラー情報をログに残す
$BASH_COMMAND は現在実行中のコマンド文字列を保持する特殊変数です。trap ERR と組み合わせることで、どのコマンドが失敗したかを自動的にログに残せます。#!/bin/bash set -euo pipefail error_handler() { local exit_code=$? local line_no=$1 echo "[ERROR] $(date '+%Y-%m-%d %H:%M:%S') スクリプト: $0" >&2 echo "[ERROR] 行番号: ${line_no}, 終了コード: ${exit_code}" >&2 echo "[ERROR] 実行コマンド: ${BASH_COMMAND}" >&2 } trap 'error_handler $LINENO' ERR cp /nonexistent_file /tmp/
# 実行結果 [ERROR] 2026-06-18 10:30:00 スクリプト: ./test.sh [ERROR] 行番号: 14, 終了コード: 1 [ERROR] 実行コマンド: cp /nonexistent_file /tmp/
/var/log/myapp.log などに書き出しておくと、後からトラブルシュートが格段に楽になります。14. set -euxo pipefail と trap を組み合わせた実務スクリプト例
これまでの内容を組み合わせた、実務で使えるバックアップスクリプトの例を示します。#!/bin/bash # backup.sh — データベースバックアップスクリプト(エラーハンドリング実装例) set -euo pipefail # --- 設定 --- DB_NAME=${DB_NAME:-myapp} BACKUP_DIR=${BACKUP_DIR:-/backup/mysql} RETAIN_DAYS=${RETAIN_DAYS:-7} TIMESTAMP=$(date '+%Y%m%d_%H%M%S') BACKUP_FILE="${BACKUP_DIR}/${DB_NAME}_${TIMESTAMP}.sql.gz" LOCK_FILE="/tmp/backup_${DB_NAME}.lock" # --- エラーハンドラー --- error_handler() { local exit_code=$? local line_no=$1 echo "[ERROR] $(date '+%Y-%m-%d %H:%M:%S') 行:${line_no} コマンド:${BASH_COMMAND}" >&2 } # --- 後片付け --- cleanup() { rm -f "${LOCK_FILE}" [[ -f "${BACKUP_FILE}" && ! -s "${BACKUP_FILE}" ]] && rm -f "${BACKUP_FILE}" } trap 'error_handler $LINENO' ERR trap 'cleanup' EXIT # --- 多重起動防止 --- if [[ -f "${LOCK_FILE}" ]]; then echo "[WARN] 既に実行中です(ロックファイル: ${LOCK_FILE})" >&2 exit 1 fi touch "${LOCK_FILE}" # --- メイン処理 --- mkdir -p "${BACKUP_DIR}" echo "[INFO] バックアップ開始: ${DB_NAME}" mysqldump --single-transaction --no-tablespaces "${DB_NAME}" | gzip > "${BACKUP_FILE}" if [[ ! -s "${BACKUP_FILE}" ]]; then echo "[ERROR] バックアップファイルが空です" >&2 exit 1 fi echo "[INFO] バックアップ完了: ${BACKUP_FILE}" find "${BACKUP_DIR}" -name "${DB_NAME}_*.sql.gz" -mtime +${RETAIN_DAYS} -delete echo "[INFO] ${RETAIN_DAYS}日以前のバックアップを削除しました"
mysqldump 失敗も拾えます。・ロックファイル:多重起動を防ぐ仕組みです。
trap EXIT でスクリプト終了時に必ず削除されます。・空ファイルチェック:
-s(サイズが0より大きい)でバックアップの正常完了を確認します。より詳しい trap コマンドの使い方はtrapコマンドでbashスクリプトのシグナルを捕捉・処理する方法も参照してください。
デバッグ出力の活用と実務Tips
15. BASH_XTRACEFD でトレースをファイルへ書き出す
set -x のトレース出力はデフォルトで標準エラー出力(stderr)へ出力されます。本番環境でトレースを有効にしたままログファイルに保存したい場合は BASH_XTRACEFD を使います。#!/bin/bash # トレース出力をファイルディスクリプタ3に割り当て exec 3>/tmp/debug_trace.log BASH_XTRACEFD=3 set -x BACKUP_DIR="/tmp/backup" mkdir -p "$BACKUP_DIR" cp /etc/hosts "$BACKUP_DIR/" set +x exec 3>&- # ファイルディスクリプタを閉じる
/tmp/debug_trace.log に書き出されます。cron で実行する自動化スクリプトのデバッグに特に有効です。16. PS4 でトレースの見やすさを改善する
デフォルトのトレースプレフィックスは+ ですが、PS4 環境変数を変更することで行番号やタイムスタンプを追加できます。#!/bin/bash # 行番号付きのトレース表示 export PS4='+(${BASH_SOURCE}:${LINENO}): ' set -x echo "処理開始" sleep 1 echo "処理終了"
# 実行結果 +(debug_test.sh:5): echo '処理開始' 処理開始 +(debug_test.sh:6): sleep 1 +(debug_test.sh:7): echo '処理終了' 処理終了
トラブルシュート|よくあるエラーと対処法
「unbound variable」エラーへの対処
set -u を有効にすると unbound variable エラーが出やすくなります。多くの場合、スペルミスや初期化忘れが原因です。# エラー例 ./script.sh: line 5: MY_VARR: unbound variable # 対処:変数名を確認し、デフォルト値を設定する MY_VAR="${MY_VAR:-default_value}"
set -e で意図せずスクリプトが終了する場合
grep が一致なしで終了コード1を返す場合や、test コマンドが偽で終了する場合など、意図した「失敗」でスクリプトが止まることがあります。#!/bin/bash set -e # grep が0件のとき終了コード1を返し、set -e でスクリプト終了してしまう # 対処1:|| true を付けて終了コードを無視する WARN_COUNT=$(grep -c "WARN" /var/log/app.log || true) echo "警告件数: $WARN_COUNT" # 対処2:if 文で囲む(if内では set -e が無効) if grep -q "WARN" /var/log/app.log; then echo "警告あり" fi # 対処3:特定箇所だけ set -e を一時無効化 set +e some_command_that_might_fail exit_code=$? set -e
サブシェルでのデバッグ
$() や |(パイプ)はサブシェルで実行されます。set -x はサブシェルにも引き継がれますが、set -e の動作が予想外になる場合があります。Linux 基本コマンドの解説も合わせて確認すると理解が深まります。#!/bin/bash set -euxo pipefail # サブシェルで実行されるコマンドも set -e の対象 FILES=$(ls /nonexistent 2>/dev/null || echo "") if [ -z "$FILES" ]; then echo "ファイルなし" fi
trap が期待通りに動かない場合
trap ERR はサブシェル($() やパイプの右側)内では引き継がれないケースがあります。また、set -e なしで trap ERR だけ設定しても、コマンド失敗時にスクリプトが自動停止しないため、必ず組み合わせて使うことが前提です。#!/bin/bash set -euo pipefail # set -e は trap ERR の前提として必須 cleanup() { echo "クリーンアップ実行" rm -f /tmp/work_$$ } trap 'cleanup' EXIT # trap EXIT はサブシェル終了後も親シェルで実行される result=$(some_command || true) echo "$result"
bash のバージョン確認
set -e の挙動は bash のバージョンによって細かく異なります。CI/CD 環境と本番環境でバージョンが異なる場合は特に注意が必要です。bash --version # → GNU bash, version 5.1.8(1)-release (x86_64-redhat-linux-gnu) # RHEL 9 系 # → GNU bash, version 5.2.21(1)-release (x86_64-pc-linux-gnu) # Ubuntu 24.04 系
set -o pipefail の挙動に差が出ることがあります。本記事のまとめ
| やりたいこと | コマンド |
|---|---|
| 実行コマンドを全てトレース表示する | set -x |
| トレースを無効化する | set +x |
| エラー発生時に即終了させる | set -e |
| 未定義変数をエラーにする | set -u |
| パイプ途中のエラーを検知する | set -o pipefail |
| 4つをまとめて設定する(現場の定番) | set -euxo pipefail |
| スクリプト実行時にトレースを有効にする | bash -x スクリプト名.sh |
| 行番号付きトレースを表示する | export PS4='+(${BASH_SOURCE}:${LINENO}): ' |
| トレースをファイルへ書き出す | BASH_XTRACEFD=3 |
| エラー発生時のクリーンアップを登録する | trap 'cleanup' ERR |
| スクリプト終了時に必ず処理を実行する | trap 'cleanup' EXIT |
set -euxo pipefail を冒頭に書き、trap ERR と trap EXIT でエラー処理と後片付けを登録する習慣を持つだけで、スクリプトの品質は大幅に上がります。特に本番環境の自動化スクリプトでは、エラーが握り潰される状態は最も危険です。デバッグオプションを使いこなして、安全で信頼性の高いスクリプトを書いてください。
スクリプトのバグ原因を特定できず、本番環境での作業に不安を感じていませんか?
シェルスクリプトのデバッグ技術は、サーバー運用自動化の質を左右します。set -x の使い方を知っているだけでなく、現場で通用する安全なLinuxサーバー構築の「型」を体系的に身につけたい方へ、『Linuxサーバー構築入門マニュアル(図解60P)』を完全無料でプレゼントしています。
「独学の時間がもったいない」「プロから直接、現場の技術を最短で学びたい」という本気の方には、2日で実務レベルのスキルが身につく【初心者向けハンズオンセミナー】も開催しています。
3,100名以上が実践した「型」を無料で公開中
プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。
登録10秒/合わなければ解除3秒 / 詳細はこちら
- 次のページへ:localectlコマンドでLinuxのロケールとキーボードを設定する方法|文字化け対策と環境統一も
- 前のページへ:mdadmコマンドでLinuxソフトウェアRAIDを構築・監視する方法|RAID1作成からディスク障害復旧まで
- この記事の属するカテゴリ:Linuxtips・シェルスクリプトへ戻る

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