こうした状況を防ぐには、スクリプト設計の段階からログ機能を組み込む必要があります。
この記事では、RHEL 9系/Ubuntu 24.04 LTSを想定環境として、シェルスクリプトのログ機能を設計・実装する方法を解説します。ログレベル関数の作り方から、タイムスタンプ付与、teeによるファイルとコンソールへの同時出力、trap EXITとの組み合わせまでを取り上げます。
この記事のポイント
・DEBUG/INFO/WARN/ERROR の4段階ログレベル関数を自作できる
・タイムスタンプとスクリプト名を付与して追跡しやすくする
・tee でコンソールとログファイルへ同時に書き出す設計を実装できる
・trap EXIT でスクリプト終了時の自動ログ記録を組み込める
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
スクリプトにログ機能を組み込む理由
echo でメッセージを出力しているだけでは、cronで自動実行したときに何が起きたか後から追いにくいです。ログ機能と単なる echo の違いは、以下の3点に集約されます。
・タイムスタンプ:いつ処理が走ったかを記録する・ログレベル:情報・警告・エラーを視覚的に区別する
・ファイル出力:実行後に確認できるよう永続化する
これらが揃わないと、スクリプトが失敗したときに「何時に」「どの処理で」「どのエラーが出たか」を特定するのに時間がかかります。保守性の高いスクリプトを書くには、最初からログ設計を組み込む習慣が重要です。
ログレベル関数の設計(DEBUG/INFO/WARN/ERROR)
ログレベルとは、メッセージの重要度を示す区分です。bash スクリプトではライブラリがないため、自前で関数を定義します。
1. ログレベル関数の定義
以下のようにファイルの先頭(またはライブラリファイル)に定義します。
#!/bin/bash set -euo pipefail # ログレベル定数(0=DEBUG 1=INFO 2=WARN 3=ERROR) LOG_LEVEL=${LOG_LEVEL:-1} LOG_FILE=${LOG_FILE:-/var/log/myscript.log} _log() { local level="$1" local level_num="$2" local msg="$3" local ts ts=$(date '+%Y-%m-%d %H:%M:%S') local line="${ts} [${level}] ${BASH_SOURCE[1]##*/}:${BASH_LINENO[0]} ${msg}" if [ "${level_num}" -ge "${LOG_LEVEL}" ]; then echo "${line}" | tee -a "${LOG_FILE}" fi } log_debug() { _log "DEBUG" 0 "$*"; } log_info() { _log "INFO" 1 "$*"; } log_warn() { _log "WARN" 2 "$*"; } log_error() { _log "ERROR" 3 "$*"; }
BASH_SOURCE[1] と BASH_LINENO[0] を使うことで、どのファイルの何行目で呼ばれたかを記録できます。
2. 呼び出し例
log_info "バックアップ処理を開始します" log_debug "対象ディレクトリ: /var/data" log_warn "ディスク使用率が80%を超えています" log_error "接続に失敗しました"
出力例(環境により異なります):
2026-10-19 10:00:01 [INFO] backup.sh:45 バックアップ処理を開始します 2026-10-19 10:00:01 [DEBUG] backup.sh:46 対象ディレクトリ: /var/data 2026-10-19 10:00:05 [WARN] backup.sh:52 ディスク使用率が80%を超えています 2026-10-19 10:00:08 [ERROR] backup.sh:60 接続に失敗しました
LOG_LEVEL=0 に設定するとDEBUGも含めて全レベルを出力し、LOG_LEVEL=3 にするとERRORだけを記録します。本番稼働時は INFO(1)または WARN(2)、問題調査時は DEBUG(0)に切り替える運用が一般的です。
タイムスタンプとスクリプト名を付与する
先の _log 関数では date '+%Y-%m-%d %H:%M:%S' でタイムスタンプを付けています。ここでよくある落とし穴がサブシェルのコストです。$(date) はコマンド置換であり、1行ログを出すたびに外部プロセスが起動します。
高頻度にログを出すスクリプトでは printf '%(%Y-%m-%d %H:%M:%S)T' -1(bash 4.2以降)を使うとコマンド置換を避けられます。
# bash 4.2以降: サブシェルを使わないタイムスタンプ取得 printf -v ts '%(%Y-%m-%d %H:%M:%S)T' -1 echo "${ts} [INFO] 処理開始"
スクリプト名は ${BASH_SOURCE[0]##*/} または $(basename "$0") で取得できます。スクリプトが別スクリプトから source されたときも BASH_SOURCE を使うと正しいファイル名を記録できます。
ファイルとコンソールに同時出力する(tee活用)
先の _log 関数では echo "${line}" | tee -a "${LOG_FILE}" を使っています。tee の役割は、標準入力から受け取った内容を標準出力(コンソール)とファイルの両方に書き出すことです。
3. 用途に応じた出力の切り替え
# ファイルのみに出力する(cronなどサイレント実行) echo "${line}" >> "${LOG_FILE}" # コンソール + ファイルの両方に出力する echo "${line}" | tee -a "${LOG_FILE}" # WARN以上だけ標準エラーへ出力する if [ "${level_num}" -ge 2 ]; then echo "${line}" | tee -a "${LOG_FILE}" >&2 else echo "${line}" >> "${LOG_FILE}" fi
シェルスクリプトの実務的な設計パターンについては、シェルスクリプト入門(shellscript.linuxmaster.jp)でも詳しく扱っています。
4. スクリプト全体の出力をまとめてログに記録する
関数ごとに tee を書くのが手間な場合は、スクリプト冒頭でファイルディスクリプタをリダイレクトする方法があります。
#!/bin/bash # スクリプト全体の stdout/stderr をログファイルへ記録する exec > >(tee -a /var/log/myscript.log) 2>&1 echo "この行もログに記録される" ls /nonexistent # エラーも記録される
exec > >(tee -a logfile) 2>&1 はスクリプト全体の標準出力と標準エラーをteeにリダイレクトします。個別のログ関数が不要な場面で有効ですが、ログレベルの区分がなくなる点に注意してください。
trap EXITでスクリプト終了時のログを自動記録する
スクリプトが正常終了したか異常終了したかを自動で記録するには、trap コマンドを使います。EXIT シグナルはスクリプトが終了するすべての経路(正常・エラー・シグナル受信)でトリガーされます。
#!/bin/bash set -euo pipefail LOG_FILE=/var/log/myscript.log SCRIPT_START=$(date '+%Y-%m-%d %H:%M:%S') # 終了時ハンドラ: 終了コードをログに記録する _on_exit() { local exit_code=$? local end_ts end_ts=$(date '+%Y-%m-%d %H:%M:%S') if [ "${exit_code}" -eq 0 ]; then echo "${end_ts} [INFO] スクリプト正常終了 (exit_code=0)" >> "${LOG_FILE}" else echo "${end_ts} [ERROR] スクリプト異常終了 (exit_code=${exit_code})" >> "${LOG_FILE}" fi } trap '_on_exit' EXIT echo "${SCRIPT_START} [INFO] 処理開始" >> "${LOG_FILE}"
_on_exit 関数内で $? を最初の行で受け取ることが重要です。後続のコマンドが $? を上書きしてしまうためです。
5. ERRシグナルで失敗行番号を記録する
set -e との組み合わせでは、ERR トラップがコマンド失敗時に発火します。EXIT と ERR を組み合わせると「どこで失敗し」「スクリプト全体がどの終了コードで終わったか」の両方が記録されます。
# ERR トラップ: 失敗したコマンドの行番号を記録する _on_err() { local exit_code=$? local line_no="${BASH_LINENO[0]}" echo "$(date '+%Y-%m-%d %H:%M:%S') [ERROR] ${BASH_SOURCE[1]##*/} line ${line_no} で失敗 (exit_code=${exit_code})" >> "${LOG_FILE}" } trap '_on_err' ERR
ログファイルの肥大化を防ぐ運用設計
毎日 cron で実行するスクリプトは、ログを上書きにするかローテーションと組み合わせるかを設計段階で決めておく必要があります。
6. 日付付きログファイル名で自動的に分割する
# 日付付きログファイル: /var/log/backup/2026-10-19.log のように自動分割 LOG_DIR=/var/log/backup mkdir -p "${LOG_DIR}" LOG_FILE="${LOG_DIR}/$(date '+%Y-%m-%d').log"
7. logrotateと連携する
固定パスのログファイルを使う場合は、logrotateに設定を追加してローテーションを自動化します。
# /etc/logrotate.d/myscript /var/log/myscript.log { daily rotate 30 compress missingok notifempty create 0644 root root }
compress で古いログを gzip 圧縮し、rotate 30 で30世代分を保持します。missingok はログファイルが存在しない場合にエラーを出さない設定で、スクリプトが一度も実行されていない状態でも logrotate が止まりません。
8. スクリプト側で古いログを自動削除する
# 30日以上古いログファイルを削除する find /var/log/backup -name '*.log' -mtime +30 -delete
-mtime +30 は最終更新日時が30日より前のファイルを対象にします。定期実行スクリプトの冒頭または末尾に置いておくと、ディスクの肥大化を自動で抑止できます。
よくあるトラブルと対処
「Permission denied」でログが書き込めない
LOG_FILE に指定したパスの親ディレクトリが存在しないか、書き込み権限がない場合に発生します。スクリプト冒頭で mkdir -p と権限確認を入れてください。
LOG_DIR=/var/log/backup mkdir -p "${LOG_DIR}" || { echo "ログディレクトリ作成失敗: ${LOG_DIR}"; exit 1; } LOG_FILE="${LOG_DIR}/myscript.log"
ERRトラップが発火しない
set -e を有効にしても、関数の戻り値を if や || で評価しているコマンドは「失敗を想定している」とみなされ、ERRトラップが発火しません。また、サブシェル内($(...))の失敗も親シェルのERRトラップを発火させない場合があります。デバッグ時は set -x で実行トレースを有効にして動作を確認します。
# ERRトラップが発火しないケース(if文の中は例外) if some_command; then log_info "成功" fi # ERRトラップが発火するケース(直接呼び出し) some_command log_info "成功"
teeのパイプでexitコードが消える
cmd | tee file の形では、パイプ全体の終了コードは最後のコマンド(tee)の終了コードになります。set -o pipefail を有効にするとパイプ内の最初の失敗コマンドの終了コードが使われます。set -euo pipefail を記事の冒頭に書く習慣を持つとこの問題を防げます。
本記事のまとめ
シェルスクリプトのログ設計に必要な要素をまとめます。
| やりたいこと | 実装方法 |
|---|---|
| ログレベルを区分する | _log() 関数 + LOG_LEVEL 変数 |
| タイムスタンプを付ける | date '+%Y-%m-%d %H:%M:%S' または printf '%(%Y-%m-%d %H:%M:%S)T' -1 |
| ファイルとコンソールに同時出力 | echo ... | tee -a logfile |
| 全体の出力をまとめて記録 | exec > >(tee -a logfile) 2>&1 |
| 終了時に結果を自動記録 | trap '_on_exit' EXIT |
| 失敗行番号を記録 | trap '_on_err' ERR + BASH_LINENO[0] |
| ログを日付ごとに分割 | LOG_FILE="dir/$(date '+%Y-%m-%d').log" |
| 古いログを自動削除 | find dir -name '*.log' -mtime +30 -delete |
ログ機能を最初から設計に組み込んでおくと、本番で問題が起きたときに原因特定が格段に速くなります。まず _log 関数と trap EXIT の組み合わせを既存スクリプトに追加するところから始めてみてください。
3,100名以上が実践した「型」を無料で公開中
プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。
姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら
- 前のページへ:シェルスクリプトにbash補完を追加する設計|complete・compgenで独自コマンドのタブ補完を実装する方法
- この記事の属するカテゴリ:シェルスクリプトへ戻る

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