cronの実行環境にはターミナルがありません。標準出力はメール送信か /dev/null に消えるだけで、エラーが出ていても気づけません。バックアップが何日も失敗し続けていたのに確認できなかった、という経験のある方もいるでしょう。
この記事では、bashスクリプトにログ出力の「型」を組み込む設計パターンを解説します。ログ関数(log_info / log_warn / log_error)の実装から、ファイルとコンソールへの同時出力(tee)、cron実行時の注意点まで、現場ですぐ使えるコードと実行例を紹介します。動作確認環境はRHEL 9.4 / Ubuntu 24.04 LTSです。
この記事のポイント
・log_info/log_warn/log_errorの3関数でログレベルを統一できる
・execとteeを組み合わせるとファイルと画面に同時出力できる
・log_errorはstderrに出すとパイプラインで通知フィルタを挟める
・cron実行時はPATH・LANGが最小化され文字化けや「コマンドが見つからない」の原因になる
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
なぜシェルスクリプトにログ設計が必要なのか
インタラクティブに動かすスクリプトなら、echoで標準出力に流すだけで十分です。問題はcronや自動化タスクとして動かすときです。cronが実行するスクリプトには、主に3つの「ログが消える」原因があります。
・標準出力の行き先がない:ターミナルに接続されていないため、MAILTOが設定されていなければ出力はそのまま捨てられる
・環境変数が最小構成:PATHやLANGがデフォルト値のため、コマンドが見つからなかったり日本語が文字化けする
・タイムスタンプがない:echoを使うだけでは「いつ何が起きたか」を後から追えない
「スクリプトは動いているはずなのにログが空」という状況を防ぐには、ログ出力の仕組みをスクリプト自身に持たせる必要があります。echoを散在させるだけでは不十分で、「いつ・どのレベルの出力か・どのファイルに書くか」を設計段階で決めることが重要です。
ログ関数を実装する基本設計
1. タイムスタンプとログレベルを付ける基本設計
まず、最小構成のログ関数ライブラリです。スクリプト内に直接書いても、別ファイルに分離してsourceで読み込む形でも使えます。#!/bin/bash # ログ関数ライブラリ(/usr/local/lib/log_functions.sh) LOG_FILE="/var/log/myapp/$(date +%Y%m%d).log" _log() { local level="$1" shift local timestamp timestamp=$(date '+%Y-%m-%d %H:%M:%S') echo "${timestamp} [${level}] $*" } log_info() { _log "INFO" "$@"; } log_warn() { _log "WARN" "$@"; } log_error() { _log "ERROR" "$@"; }
#!/bin/bash # メインスクリプト(backup.sh) source /usr/local/lib/log_functions.sh log_info "バックアップを開始します" log_warn "対象ディレクトリが空です: /data/work" log_error "rsync に失敗しました(終了コード: $?)"
[miyazaki@web01 ~]$ bash backup.sh 2026-07-28 09:14:03 [INFO] バックアップを開始します 2026-07-28 09:14:03 [WARN] 対象ディレクトリが空です: /data/work 2026-07-28 09:14:03 [ERROR] rsync に失敗しました(終了コード: 1)
なお、ログのタイムスタンプが正確であるためには、サーバー自体の時刻同期が前提です。ntpd 時刻同期設定で時刻ずれが発生していないことを確認してから本番投入してください。
2. log_errorは標準エラーに出力する設計
ERRORだけを標準エラー(fd 2)に向けておくと、呼び出し元でエラーのみを別処理に渡せます。#!/bin/bash # log_errorだけstderrへ向ける(fd 2) log_error() { _log "ERROR" "$@" >&2; }
#!/bin/bash # ERRORだけをエラー専用ログに、INFOはメインログへ bash backup.sh 1>> /var/log/myapp/info.log 2>> /var/log/myapp/error.log
3. LOG_LEVEL変数でデバッグ出力を制御する
本番環境ではINFO以上のみを出力し、調査時だけDEBUGを有効にする設計です。環境変数で動的に切り替えられます。#!/bin/bash # ログレベル制御(0=DEBUG, 1=INFO, 2=WARN, 3=ERROR) LOG_LEVEL=${LOG_LEVEL:-1} _log() { local level_name="$1" local level_num="$2" shift 2 if [ "${level_num}" -ge "${LOG_LEVEL}" ]; then local timestamp timestamp=$(date '+%Y-%m-%d %H:%M:%S') echo "${timestamp} [${level_name}] $*" fi } log_debug() { _log "DEBUG" 0 "$@"; } log_info() { _log "INFO" 1 "$@"; } log_warn() { _log "WARN" 2 "$@"; } log_error() { _log "ERROR" 3 "$@" >&2; }
[miyazaki@web01 ~]$ LOG_LEVEL=0 bash backup.sh 2026-07-28 09:14:05 [DEBUG] rsync コマンドを構築中 2026-07-28 09:14:05 [INFO] バックアップを開始します 2026-07-28 09:14:06 [INFO] 転送完了: 142 files [miyazaki@web01 ~]$ bash backup.sh 2026-07-28 09:14:05 [INFO] バックアップを開始します 2026-07-28 09:14:06 [INFO] 転送完了: 142 files
LOG_LEVEL を指定しないため、自動的にINFO以上の出力になります。調査時だけ LOG_LEVEL=0 bash backup.sh を手動実行すればDEBUGログが確認できます。出力をファイルとコンソールに同時に残す設計
1. execとteeを使った全体転送設計
スクリプト全体の出力をファイルとコンソールに同時に書き出す設計です。スクリプト冒頭に数行追加するだけで実現できます。#!/bin/bash # ===== ログ出力設定(スクリプト冒頭に置く)===== LOG_DIR="/var/log/myapp" LOG_FILE="${LOG_DIR}/$(date +%Y%m%d).log" mkdir -p "${LOG_DIR}" # 以降の全出力をファイルとコンソールに二重出力する exec > >(tee -a "${LOG_FILE}") 2>&1 # ================================================ source /usr/local/lib/log_functions.sh log_info "スクリプト開始: $0"
exec > >(tee -a "${LOG_FILE}") 2>&1 の意味:・
exec > >(...):スクリプトの標準出力を、丸括弧内のコマンドの標準入力に接続する・
tee -a "${LOG_FILE}":入力を画面(標準出力)とファイル(追記)に同時に書き出す・
2>&1:標準エラーも標準出力と同じ経路(tee)に統合するこの3行以降のechoやlog関数はすべて、画面とログファイルの両方に出力されます。
2. 実サーバーでの確認例
[miyazaki@web01 ~]$ bash backup.sh 2026-07-28 09:15:00 [INFO] スクリプト開始: backup.sh 2026-07-28 09:15:00 [INFO] バックアップを開始します 2026-07-28 09:15:02 [INFO] /data/web01 -> /backup/web01/20260728 2026-07-28 09:15:02 [INFO] 転送完了: 1,842 files, 2.3GB 2026-07-28 09:15:02 [INFO] スクリプト終了
[miyazaki@web01 ~]$ ls -la /var/log/myapp/ -rw-r--r-- 1 miyazaki miyazaki 312 Jul 28 09:15 20260728.log [miyazaki@web01 ~]$ cat /var/log/myapp/20260728.log 2026-07-28 09:15:00 [INFO] スクリプト開始: backup.sh 2026-07-28 09:15:00 [INFO] バックアップを開始します 2026-07-28 09:15:02 [INFO] /data/web01 -> /backup/web01/20260728 2026-07-28 09:15:02 [INFO] 転送完了: 1,842 files, 2.3GB 2026-07-28 09:15:02 [INFO] スクリプト終了
ロギング設計を含むシェルスクリプトの設計力を体系的に身につけたい方は、シェルスクリプト実践講座(Linux Master Pro)もご覧ください。現場で使える設計パターンを体系的に学べます。
cronで動かすときのログ設計の注意点
1. PATHを先頭に明示する
cronの実行環境のPATHは/usr/bin:/bin 程度しかありません。スクリプト内で rsync・aws・python3 など /usr/local/bin にあるコマンドを使う場合、cronから実行すると「コマンドが見つからない」エラーになります。#!/bin/bash # cronで使う場合はスクリプト先頭でPATHを明示する PATH=/usr/local/bin:/usr/bin:/bin export PATH LOG_DIR="/var/log/myapp" LOG_FILE="${LOG_DIR}/$(date +%Y%m%d).log" mkdir -p "${LOG_DIR}" exec > >(tee -a "${LOG_FILE}") 2>&1
2. LANGを設定して文字化けを防ぐ
cronのデフォルト環境ではLANG が設定されていない場合があり、日本語を扱うスクリプトで文字化けが起きます。#!/bin/bash PATH=/usr/local/bin:/usr/bin:/bin LANG=ja_JP.UTF-8 export PATH LANG
3. cronエントリはシンプルに保つ
スクリプト内でteeによる二重出力を設計してあるため、cronエントリ側でリダイレクトを追加する必要はありません。# cronエントリ(スクリプト内でログを管理するためシンプルに書ける) 0 2 * * * /usr/local/bin/backup.sh
>> logfile を重複して書く必要がありません。メンテナンス箇所を一箇所に集約できます。トラブルシュート
「ログファイルが空のまま」の原因と対処
・exec行の前にエラーが発生している:exec > >(tee ...) の前にPATHやLOG_DIRの設定が失敗し、スクリプトが早期終了している可能性があります。bash -x backup.sh でトレース実行して確認してください・ログディレクトリが存在しない:
mkdir -p "${LOG_DIR}" をexec行の前に書いていない場合、teeの書き込み先が存在せずサイレントに失敗します。必ずmkdirをexecより前に実行してください・プロセス置換が動作しない環境:
>(tee ...) はbash専用です。shebangが #!/bin/sh になっているとdash等では動作しません。head -1 backup.sh でshebang行を確認してください「cronから動かすと文字化けする」の対処
# crontabに一時的に以下を追加してcronの環境変数を確認する * * * * * env > /tmp/cron_env.txt 2>&1
/tmp/cron_env.txt に出力された内容でLANGが設定されているか確認します。未設定なら前節のようにスクリプト先頭で LANG=ja_JP.UTF-8 を明示してください。確認後はcrontabからこの行を削除してください。「teeを使うとERRORが意図しない箇所に出る」の対処
exec > >(tee ...) 2>&1 で標準エラーを標準出力に統合しているため、log_error 内で >&2 に書き出した内容もtee経由でコンソールとファイルの両方に出ます。これは正常な動作です。ERRORを専用ファイルに分離したい場合は、前節のfd分離設計をteeと組み合わせて調整してください。本記事のまとめ
| やりたいこと | 設計パターン |
|---|---|
| タイムスタンプ付きログを出力する | log_info/log_warn/log_error 関数を実装する |
| ERRORだけを別ファイルに分離する | log_error() で >&2 に出して呼び出し側で 2>> error.log |
| デバッグ出力を本番では出さない | LOG_LEVEL 変数で 0 から 3 のレベル制御を実装する |
| ファイルとコンソールに同時に出力する | exec > >(tee -a logfile) 2>&1 を冒頭に置く |
| cronの「コマンドが見つからない」を防ぐ | PATH=/usr/local/bin:/usr/bin:/bin を先頭に明示する |
| cronの文字化けを防ぐ | LANG=ja_JP.UTF-8 をスクリプト先頭で設定する |
シェルスクリプト講座を見る >>
3,100名以上が実践した「型」を無料で公開中
プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。
姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら
- 前のページへ:シェルスクリプトのプロセス置換設計|一時ファイルなしでコマンド出力をファイルとして渡す方法
- この記事の属するカテゴリ:シェルスクリプトへ戻る

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