はじめに
GMOコネクトの星です。
「動いているから触らない」——長年運用されてきたバッチ用シェルスクリプト群を、Git移行のタイミングでAIにレビューさせてみたら、想像以上のバグが眠っていました😇
特に衝撃だったのが、ログに「CSV変換処理を開始します」と出力するつもりが「20SV変換処理を開始します」と出ていた件。dateコマンドのフォーマット文字列中で%Cが「世紀」に展開されるという、知っていれば一発で気づくけど知らなければ絶対見逃す系の罠でした。
その過程で見つけた罠と、修正プロセスの記録です。
先にまとめ
検出した不具合パターンをまとめるとこうなります。
| 分類 | 内容 | 影響度 | 該当数 |
|---|---|---|---|
| %C誤変換 |
dateのフォーマット文字列にメッセージを含め、%Cが世紀に変換される |
ログ出力が化ける | 2箇所 |
| タイムスタンプ不正 | バッチ開始前にdateを取得し、完了ログに開始時刻が出力される |
障害調査の妨げ | 3箇所 |
| 変数タイポ |
A_PATHがA_PARHになっている |
動作はするが可読性低下 | 1箇所 |
| コメント残存 | コピー元処理の説明がそのまま残っている | 保守時の混乱 | 1箇所 |
| 排他ロック二重化 | 共通のロック処理と自前のロックが両方存在 | 整理の余地あり | 2ファイル |
| mkdir二重記載 | 同じディレクトリ作成が2箇所に存在 | 無害だが冗長 | 1箇所 |
| 終了コード未処理 | Java実行後の$?未チェック |
異常終了の見逃し | 複数箇所 |
対象8ファイルから計10件以上の改善点を検出。うち実害のある5件(%C誤変換2箇所、タイムスタンプ不正3箇所)は即時修正し、残りは後続タスクとして記録しました。
やったこと: AIによるシェルスクリプト一括レビュー
背景
Javaバッチの実行ラッパーとして使われているシェルスクリプト群を、SVNからGitリポジトリへ移行する作業が発生しました。対象は約8ファイル。いずれも以下のような共通構造を持っています。
#!/bin/sh
echo -e "\\n\\n[$0]\\n$(date)\\n"
# 環境設定の読み込み
. 環境設定シェル
# 排他ロック取得
. ロックシェル.sh "バッチ名.sh" 5
if [ ロックシェルの判定結果 ]; then
echo "ロックされている旨のログ出力文言"
exit 1
fi
# Java実行
java -Xms256M -Xmx256M バッチアプリ バッチ名
# 終了処理
「せっかくGitに移行するなら、このタイミングでコードレビューもやっておこう」ということで、Claude Codeに1ファイルずつ処理フローの分析と改善点の洗い出しを依頼しました。
レビューの進め方
やり方はシンプルです。Claude Codeでバッチリポジトリを開き、「このシェルスクリプトの処理フローを分析して、改善点を洗い出してください」と1ファイルずつ依頼しました。
ポイントは2段階に分けたことです。
- 処理フロー分析: まずスクリプトが何をやっているかを説明させる
- 改善点の洗い出し: フローを理解した上で、問題点を列挙させる
いきなり「バグを見つけて」と言うより、処理フローを一度言語化させることで、AIが文脈を把握した状態で問題点を指摘してくれます。
そして1ファイル目で見つかったパターン(タイムスタンプの問題、dateの書式の罠など)を、残りの7ファイルにも横展開して確認させました。同じテンプレートからコピーで量産されたスクリプト群なので、1つで見つかったバグは高確率で他にも存在します。
ハマり1: 「CSV」が「20SV」になる — dateコマンドの%Cの罠
問題のコード
CSV変換バッチのログ出力に、以下のようなコードがありました。
# NG: フォーマット文字列の中に%記号付きのメッセージが含まれている
date +"%Y/%m/%d %H:%M:%S %CSV変換処理を開始します"
問題なさそうに見えますよね。ところが実際の出力がこちら。
2026/04/06 10:30:00 20SV変換処理を開始します
「CSV」が「20SV」になっています。
原因
dateコマンドの+以降は、すべてフォーマット文字列として解釈されます。フォーマット文字列中に%Cという並びがあると、これは「世紀」(21世紀なら20)を意味する書式指定子として展開されます。
dateのフォーマット指定子の一部を挙げると:
| 指定子 | 意味 | 出力例 |
|---|---|---|
%Y |
4桁の年 | 2026 |
%C |
世紀(年の上2桁) | 20 |
%S |
秒 | 00 |
%H |
時 | 10 |
%M |
分 | 30 |
フォーマット文字列の中にメッセージを含めると、%と英大文字の組み合わせが書式指定子として解釈されるリスクがあります。今回は%CSVの%Cが世紀(20)に展開され、結果として20SVになりました。
dateは%に続く文字を書式指定子として処理するので、フォーマット文字列中に%が残っていると、後続の英字が食われます。
修正
フォーマット文字列とメッセージを分離します。
# OK: dateの出力とメッセージを分離
echo $(date +"%Y/%m/%d %H:%M:%S") "CSV変換処理を開始します"
出力:
2026/04/06 10:30:00 CSV変換処理を開始します
この修正はCSV変換バッチ内の2箇所(開始ログと終了ログ)に適用しました。
なぜ長年気づかなかったのか
このバグが発覚しなかった理由はいくつか考えられます。
- ログファイルを目視確認する運用が定着していなかった
- 「20SV」という文字列を見ても、そもそもログのフォーマットを深く見ていなかった
- バッチの成否はJavaの終了コードで判断しており、ログメッセージの内容は問われなかった
仕様どおりに文字列を読むAIにとっては、こういう「見慣れてスルーする」系のバグのほうがむしろ得意なようです。
ハマり2: タイムスタンプが「完了時刻」ではなく「開始時刻」
問題のコード
バックアップファイル削除バッチに、以下のような構造がありました。
d=$(date +"%Y-%m-%d_%H:%M:%S")
# Java実行(数分〜数十分かかる)
java -Xms256M -Xmx256M バッチアプリ シェル名
if [ 実行結果判定 ]; then
echo "$d エラー時の文言"
exit 1
fi
echo "$d 処理成功時の文言"
dateの取得がJava実行の前にあるため、完了ログや異常終了ログに記録される時刻は「バッチ開始時の時刻」です。Javaの処理に10分かかった場合、完了ログの時刻は実際より10分前の時刻になります。
障害発生時にログの時刻を頼りに調査する場面で、この10分のズレは混乱の原因になります。
修正
dateの取得を、使用する直前に移動しました。
# Java実行
java -Xms256M -Xmx256M バッチアプリ シェル名
if [ $? != 0 ]; then
d=$(date +"%Y-%m-%d_%H:%M:%S") # エラー発生時点の時刻
echo "$d エラー時の文言"
exit 1
fi
d=$(date +"%Y-%m-%d_%H:%M:%S") # 完了時点の時刻
echo "$d 処理成功時の文言"
このパターンは、バックアップファイル削除バッチと外部システム情報連携バッチの計3箇所で見つかりました。いずれも同じテンプレートからコピーされた痕跡があり、「1つのバグが量産される」典型例です。
ハマり3: その他のアンチパターン集
AIレビューでは上記以外にも、いくつかの改善点が見つかりました。
変数のタイポ
A_PARH=$1 # A_PATH のタイポ
シェルスクリプトでは未定義変数を参照してもエラーにならない(空文字列になる)ため、こういったタイポが実害を出さないまま放置されがちです。set -u(未定義変数参照でエラー)を設定していない環境ではなおさらです。
今回のケースではA_PARHという名前で一貫して使われていたため動作に支障はありませんでしたが、可読性と保守性の観点から修正しました。
排他ロックの二重化
一部のバッチで、共通のロックシェルによる排他制御と、スクリプト独自のln -sによるロックが両方存在していました。
# 共通ロック機構(全バッチ共通)
. ロックシェル "シェル名" ロック時間
# さらに独自のロック(このバッチだけ)
lockfile=ロックファイル.lock
if ln -s $$ ${lockfile} 2> /dev/null; then
break
else
# ...
fi
歴史的経緯で追加されたと思われますが、ロック機構が2つあると「どちらが正か」が曖昧になります。今回は移行優先のため記録に留め、整理は後続タスクとしました。
コピー元バッチのコメント残存
バッチCに、コピー元の別バッチの説明コメントがそのまま残っていました。コメントと実際の処理内容が一致しないのは保守時に混乱を招くため修正しました。
AIによる横展開レビューの手法
「同一構造のスクリプト群」はAIレビューに最適
対象の8ファイルは、すべて以下の共通構造を持っていました。
- 環境設定の読み込み
- 排他ロック取得
- Java実行
- 終了コード処理
- ログ出力
この「テンプレからコピーで量産された」構造は、AIレビューとの相性が抜群です。1ファイル目で見つかったパターン(dateの%C問題、タイムスタンプ取得タイミング)を、「他のファイルにも同じ問題がないか確認して」と指示するだけで、残り7ファイルを横断的にチェックできます。
人間がこれをやると、途中で目が慣れて見落とします(正直、飽きます)。AIはそこを気にしなくていい。
人間の判断が必要な場面
一方で、AIが出した改善提案のすべてを採用したわけではありません。
- 共通フレームワーク化: 8ファイルの共通部分を切り出す提案 → 移行作業のスコープを超えるため見送り
-
set -euo pipefailの追加: 安全だが既存の動作に影響する可能性 → 今回は見送り - 排他ロックの整理: 正しいが影響範囲の調査が必要 → 後続タスクとして記録
「AIが見つけた問題」と「今修正すべき問題」は別です。AIに網羅的に拾わせて、スコープとリスクは人間が判断する。この切り分けがうまくいきました。
安全なシェルスクリプトの作法
dateのフォーマット文字列とメッセージは必ず分離する
# NG
date +"%Y/%m/%d %H:%M:%S 処理開始"
# OK
echo $(date +"%Y/%m/%d %H:%M:%S") "処理開始"
date +の後ろはすべてフォーマット文字列です。英大文字を含むメッセージを混ぜると、意図しない変換が発生します。
タイムスタンプは使用する直前で取得する
# NG: 処理前に取得した時刻を完了ログに使う
d=$(date +"...")
long_running_process
echo "$d 完了"
# OK: 完了直前に取得
long_running_process
d=$(date +"...")
echo "$d 完了"
コピー&ペーストでバッチを量産したら、コメントとファイル名を確認する
テンプレートからコピーした場合、少なくとも以下を確認しましょう。
- コメントがコピー元の説明のままになっていないか
- ファイル名やバッチ名の変数がコピー元のままになっていないか
- 不要な処理(コピー元にしかない機能)が残っていないか
まとめ
-
dateのフォーマット文字列にメッセージを混ぜてはいけない。%Cが世紀に展開されて「CSV」が「20SV」になる - タイムスタンプは使う直前に取得する。処理前に取ると完了ログの時刻がズレる
- テンプレからコピーで量産されたスクリプト群は、AIレビューで横展開すると同じバグを一網打尽にできる
最後に、GMOコネクトではサービス開発支援や技術支援をはじめ、
幅広い支援を行っておりますので、何かありましたらお気軽にお問合せください。