【bat】バッチファイルで前回処理の状態を引き継ぐ完全ガイド|フラグファイル・状態ファイル・差分処理・二重起動防止・再開機能まで徹底解説

【bat】バッチファイルで前回処理の状態を引き継ぐ完全ガイド|フラグファイル・状態ファイル・差分処理・二重起動防止・再開機能まで徹底解説 bat

タスクスケジューラで毎日実行するバッチを作ったとき、「前回は成功したのか」「どこまで処理したのか」「今日はもう実行済みか」を次回起動時に知る手段がないと、ミスや重複処理が発生します。

こうした問題を解決するのが状態引き継ぎの仕組みです。フラグファイル・状態ファイル・実行ログの3つを使い分けることで、バッチファイルは「前回の結果を知った上で動く」インテリジェントなスクリプトになります。

この記事では、フラグファイルによる成功/失敗管理から、二重起動防止・差分処理・複数バッチ間の状態共有・失敗箇所からの再開機能まで、実務で直接使えるパターンを体系的に解説します。複数バッチファイルを一括管理・実行するマスタースクリプト完全ガイドも合わせて読むと、状態管理をマスタースクリプトに組み込む応用まで学べます。

この記事で学べること
フラグファイルで成功・失敗・実行中を管理する基本パターン、実行中フラグ(ロックファイル)で二重起動を防ぐ方法、状態ファイルで最終実行日時・処理ステップを保存する方法、差分処理(前回実行日以降のファイルのみ処理)の実装、複数バッチ間で状態を共有する方法(環境ファイル・一時ファイル)、失敗したステップから再開する再開機能の実装、本番運用向け完成版スクリプト
スポンサーリンク

なぜ状態引き継ぎが必要か

バッチファイルは毎回「白紙の状態」で起動します。前回の実行が成功したのか失敗したのか、どこで止まったのかを知る手段がなければ、エラーが続いていることに気づかなかったり、同じ処理を二重に実行したりするリスクがあります。

課題 状態引き継ぎなし 状態引き継ぎあり
前回の成否確認 確認手段がない フラグファイルで即時確認
二重起動 同時実行で競合が発生 実行中フラグで防止
差分処理 全件を毎回処理(非効率) 前回日時以降のみ処理
処理の再開 最初からやり直し 失敗ステップから再開
実行履歴 いつ実行されたか不明 ログファイルで追跡可能
バッチ間連携 手動でパラメータを渡す 状態ファイル経由で自動共有

状態を記録する手段として、主に3種類のファイルを使います。

種類 用途
フラグファイル 成功・失敗・実行中などの状態を「存在の有無」で表す success.flag / running.lock
状態ファイル 最終実行日時・ステップ番号など「値」を記録する last_run.txt / .process_state
実行ログ 処理の詳細な履歴・エラー内容を時系列で記録する process_20240115.log

フラグファイルで成功・失敗・実行中を管理する

フラグファイルとは、ファイルの存在有無だけで状態を表す仕組みです。中身は問わず「あるか・ないか」で判定するため、実装がシンプルです。バッチファイルでフラグを更新する方法完全ガイドでフラグの更新パターンを、外部ファイルのフラグを読み込んで処理を分岐する方法で読み込みと分岐パターンを詳しく解説しています。

成功フラグ:前回が成功しているか確認する

成功フラグの基本パターン
@echo off
setlocal enabledelayedexpansion

set "BASE=%~dp0"
set "SUCCESS_FLAG=%BASE%flags\success.flag"
set "LOG=%BASE%logs\process_%DATE:~0,4%%DATE:~5,2%%DATE:~8,2%.log"

if not exist "%BASE%flags" mkdir "%BASE%flags"
if not exist "%BASE%logs"  mkdir "%BASE%logs"

:: 前回の成功フラグを確認
if exist "%SUCCESS_FLAG%" (
    echo [INFO] 前回の処理は成功しています。フラグをリセットして再実行します。
    del "%SUCCESS_FLAG%"
) else (
    echo [INFO] 前回の処理が失敗または未実行です。処理を開始します。
)

echo [%DATE% %TIME%] 処理開始 >> "%LOG%"

:: ────── メイン処理 ──────
call :do_main_process
set /a ERR=!ERRORLEVEL!
:: ────────────────────────

if !ERR! EQU 0 (
    echo success > "%SUCCESS_FLAG%"
    echo [%DATE% %TIME%] 処理成功 >> "%LOG%"
    echo 処理が正常に完了しました。
) else (
    echo [%DATE% %TIME%] 処理失敗 ERRORLEVEL=!ERR! >> "%LOG%"
    echo 処理が失敗しました。ログを確認してください。
    exit /b !ERR!
)

endlocal
exit /b 0

:do_main_process
:: ここに実際の処理を書く
echo データ処理実行中...
exit /b 0

実行中フラグ(ロックファイル):二重起動を防ぐ

タスクスケジューラで起動するバッチが、前回の実行がまだ終わっていないうちに再起動してしまう問題は、実行中フラグ(ロックファイル)で防げます。起動時にフラグを作成し、終了時に削除することで「実行中かどうか」を外から判断できます。

二重起動防止パターン(ロックファイル方式)
@echo off
setlocal enabledelayedexpansion

set "BASE=%~dp0"
set "LOCK=%BASE%process.lock"
set "LOG=%BASE%logs\process_%DATE:~0,4%%DATE:~5,2%%DATE:~8,2%.log"

if not exist "%BASE%logs" mkdir "%BASE%logs"

:: ── 二重起動チェック ──────────────────────────────────
if exist "%LOCK%" (
    :: ロックファイルが残っている → 実行中か確認
    set /p LOCK_PID=<"%LOCK%"
    tasklist /FI "PID eq !LOCK_PID!" /FO CSV 2>nul | find "!LOCK_PID!" > nul
    if !ERRORLEVEL! EQU 0 (
        echo [ABORT] 別のプロセス (PID: !LOCK_PID!) が実行中です。終了します。
        echo [%DATE% %TIME%] 二重起動を検知。PID=!LOCK_PID! >> "%LOG%"
        exit /b 1
    ) else (
        echo [WARN] ロックファイルが残っていますが、該当プロセスは終了済みです。リセットします。
        del "%LOCK%"
    )
)

:: ── ロック取得(自PIDを書き込む) ────────────────────
:: PowerShell で現在の PID を取得(wmic は Windows 11 で非推奨)
for /f %%P in ('powershell -NoProfile -Command "$PID"') do set "MYPID=%%P"
echo !MYPID! > "%LOCK%"
echo [%DATE% %TIME%] 実行開始 PID=!MYPID! >> "%LOG%"

:: ── メイン処理 ────────────────────────────────────────
echo メイン処理実行中...
timeout /t 5 /nobreak > nul
echo メイン処理完了

echo [%DATE% %TIME%] 処理完了 >> "%LOG%"

:: ── ロック解放 ────────────────────────────────────────
del "%LOCK%"
endlocal
exit /b 0
ロックファイルが残るケース
バッチが強制終了された場合、ロックファイルが残ったままになります。上のコードでは「ロックファイルがあっても該当PIDが存在しない場合はリセット」する処理を入れています。ただし、PIDが別のプロセスに再利用された場合の誤検知リスクは残ります。運用時はlogs\ フォルダのロックファイルをメンテナンス手順に含めることを推奨します。

失敗フラグ:前回エラーを記録して次回に報告する

失敗フラグ + 前回エラー内容の引き継ぎ
@echo off
setlocal enabledelayedexpansion

set "BASE=%~dp0"
set "FAIL_FLAG=%BASE%flags\last_error.flag"
set "LOG=%BASE%logs\process_%DATE:~0,4%%DATE:~5,2%%DATE:~8,2%.log"

if not exist "%BASE%flags" mkdir "%BASE%flags"
if not exist "%BASE%logs"  mkdir "%BASE%logs"

:: 前回エラーフラグを確認して警告
if exist "%FAIL_FLAG%" (
    set /p PREV_ERR=<"%FAIL_FLAG%"
    echo [WARN] 前回の処理が失敗しました(原因: !PREV_ERR!)。今回再試行します。
    echo [%DATE% %TIME%] 前回エラーから再試行: !PREV_ERR! >> "%LOG%"
    del "%FAIL_FLAG%"
)

:: メイン処理
call :main_task
set /a ERR=!ERRORLEVEL!

if !ERR! NEQ 0 (
    :: 失敗フラグにエラー内容を書き込む
    echo ERRORLEVEL=!ERR! DATE=%DATE% TIME=%TIME% > "%FAIL_FLAG%"
    echo [%DATE% %TIME%] 処理失敗 ERRORLEVEL=!ERR! >> "%LOG%"
    echo 失敗フラグを作成しました。次回起動時に警告が表示されます。
    exit /b !ERR!
)

echo [%DATE% %TIME%] 処理成功 >> "%LOG%"
exit /b 0

:main_task
:: サンプル: ランダムに失敗
set /a RAND=%RANDOM% %% 2
exit /b !RAND!

状態ファイルで最終実行日時・進捗ステップを管理する

フラグファイルが「あるかないか」の2値管理なのに対し、状態ファイルは「最後にいつ実行したか」「どのステップまで完了したか」などの具体的な値を保存できます。

最終実行日時を保存して差分処理に活用する

前回の実行日時を保存しておくと、「前回以降に変更されたファイルだけ処理する」差分処理が実現できます。バックアップや差分同期のバッチで特に有効なパターンです。

最終実行日時の保存と読み込み
@echo off
setlocal enabledelayedexpansion

set "BASE=%~dp0"
set "STATE=%BASE%.last_run"
set "LOG=%BASE%logs\backup_%DATE:~0,4%%DATE:~5,2%%DATE:~8,2%.log"

if not exist "%BASE%logs" mkdir "%BASE%logs"

:: ── 前回の実行日時を読み込む ─────────────────────────
set "LAST_RUN="
if exist "%STATE%" (
    set /p LAST_RUN=<"%STATE%"
    echo [INFO] 前回実行日時: !LAST_RUN!
    echo [%DATE% %TIME%] 前回: !LAST_RUN! >> "%LOG%"
) else (
    echo [INFO] 初回実行です。全ファイルを処理します。
)

:: ── メイン処理 ────────────────────────────────────────
set "SRC=C:\data\source"
set "DST=C:\dataackup"

if defined LAST_RUN (
    :: 前回実行日以降に更新されたファイルのみコピー
    :: forfiles で最終更新日が LAST_RUN 以降のファイルをコピー
    echo [INFO] 差分コピー開始...
    robocopy "%SRC%" "%DST%" /MAXAGE:1 /LOG+:"%LOG%" /NJH /NJS
) else (
    :: 初回: 全ファイルコピー
    echo [INFO] 全件コピー開始...
    robocopy "%SRC%" "%DST%" /E /LOG+:"%LOG%" /NJH /NJS
)
set /a ERR=!ERRORLEVEL!

:: robocopy は成功でも ERRORLEVEL=1 を返すことがある(コピーあり)
if !ERR! GEQ 8 (
    echo [ERROR] robocopyが失敗しました。ERRORLEVEL=!ERR! >> "%LOG%"
    exit /b !ERR!
)

:: ── 今回の実行日時を状態ファイルに保存 ──────────────
echo %DATE% %TIME% > "%STATE%"
echo [%DATE% %TIME%] 処理完了。次回は差分のみ実行されます。 >> "%LOG%"
echo 完了。次回実行日時を保存しました。

endlocal
exit /b 0
robocopyのEXIT CODEに注意
robocopy は「コピーしたファイルがある場合に ERRORLEVEL=1」を返します。0は「コピーなし(差分なし)」、8以上がエラーです。通常の IF ERRORLEVEL 1 判定では誤ってエラーと判断するため、IF !ERR! GEQ 8 でチェックしてください。

処理ステップ番号を保存して途中から再開する

長時間かかる処理を複数ステップに分けている場合、途中で失敗したステップ番号を状態ファイルに保存しておくと、次回実行時に「どこから再開するか」を自動的に判断できます。

ステップ番号の保存と再開
@echo off
setlocal enabledelayedexpansion

set "BASE=%~dp0"
set "STATE=%BASE%.process_step"
set "LOG=%BASE%logs\process_%DATE:~0,4%%DATE:~5,2%%DATE:~8,2%.log"

if not exist "%BASE%logs" mkdir "%BASE%logs"

:: ── 前回の完了ステップを読み込む ─────────────────────
set /a START_STEP=1
if exist "%STATE%" (
    set /p DONE_STEP=<"%STATE%"
    set /a START_STEP=!DONE_STEP! + 1
    echo [RESUME] ステップ !DONE_STEP! まで完了済み。ステップ !START_STEP! から再開します。
    echo [%DATE% %TIME%] ステップ !DONE_STEP! から再開 >> "%LOG%"
) else (
    echo [START] 最初から実行します。
)

:: ── ステップ別処理 ────────────────────────────────────
set /a TOTAL_STEPS=5

set /a STEP=0
:step_loop
set /a STEP+=1
if !STEP! GTR %TOTAL_STEPS% goto :all_done

:: 前回完了ステップより前はスキップ
if !STEP! LSS !START_STEP! (
    echo [SKIP] ステップ !STEP! は前回完了済み
    goto :step_loop
)

:: 各ステップの処理
echo [Step !STEP!/%TOTAL_STEPS%] 処理中...
call :execute_step !STEP!
set /a ERR=!ERRORLEVEL!

if !ERR! NEQ 0 (
    :: 失敗したステップの前のステップ番号を保存(次回はここから再開)
    set /a DONE=!STEP! - 1
    if !DONE! GTR 0 (
        echo !DONE! > "%STATE%"
    ) else (
        if exist "%STATE%" del "%STATE%"
    )
    echo [FAIL] ステップ !STEP! 失敗。ステップ !DONE! まで記録しました。 >> "%LOG%"
    echo 次回起動時にステップ !STEP! から再開します。
    exit /b !ERR!
)

echo [OK] ステップ !STEP! 完了 >> "%LOG%"
goto :step_loop

:all_done
:: 全ステップ完了 → 状態ファイルを削除(次回は初回扱い)
if exist "%STATE%" del "%STATE%"
echo [%DATE% %TIME%] 全ステップ完了 >> "%LOG%"
echo すべてのステップが正常に完了しました。
endlocal
exit /b 0

:execute_step
:: ステップ番号に応じた処理
if "%~1"=="1" echo   → データ取得
if "%~1"=="2" echo   → データ変換
if "%~1"=="3" echo   → DB登録
if "%~1"=="4" echo   → レポート生成
if "%~1"=="5" echo   → クリーンアップ
exit /b 0

実行ログで処理履歴を記録する

バッチファイルでログを出力する方法完全ガイドでログ出力の基本を解説していますが、状態引き継ぎの観点では「前回何が起きたか」を後から追跡できるログが重要です。バッチファイルで実行時刻・経過時間をログに記録する完全ガイドの経過時間計測と組み合わせると、ボトルネックの特定もできます。

成功・失敗・スキップを記録する実行ログ
@echo off
setlocal enabledelayedexpansion

set "BASE=%~dp0"
set "LOGDIR=%BASE%logs"
set "LOG=%LOGDIR%\process_%DATE:~0,4%%DATE:~5,2%%DATE:~8,2%.log"
set "SUMMARY=%LOGDIR%\summary.log"

if not exist "%LOGDIR%" mkdir "%LOGDIR%"

:: ── ログヘッダー ──────────────────────────────────────
echo ============================================ >> "%LOG%"
echo  実行開始: %DATE% %TIME% >> "%LOG%"
echo ============================================ >> "%LOG%"

set /a OK=0
set /a FAIL=0
set /a SKIP=0

:: ── 各処理の実行と記録 ──────────────────────────────
call :log_run "データ取得"    "step1_fetch.bat"
call :log_run "データ変換"    "step2_convert.bat"
call :log_run "DB登録"        "step3_import.bat"
call :log_run "レポート生成"  "step4_report.bat"

:: ── サマリー記録 ──────────────────────────────────────
echo ============================================ >> "%LOG%"
echo  完了: %DATE% %TIME% >> "%LOG%"
echo  成功=%OK% 失敗=%FAIL% スキップ=%SKIP% >> "%LOG%"
echo ============================================ >> "%LOG%"

:: サマリーファイル(最新の実行結果を蓄積)
echo %DATE% %TIME% OK=%OK% FAIL=%FAIL% SKIP=%SKIP% >> "%SUMMARY%"

echo 実行結果: 成功 %OK% / 失敗 %FAIL% / スキップ %SKIP%
if !FAIL! GTR 0 exit /b 1
exit /b 0

:: ─────────────────────────────────────────────────────
:log_run
:: 引数: %1=処理名  %2=スクリプトファイル名
setlocal
set "LABEL=%~1"
set "SCRIPT=%~2"

:: 経過時間計測(開始)
for /f "tokens=1-3 delims=:." %%H in ("%TIME: =0%") do set /a TS=%%H*3600+%%I*60+%%J

echo [START] !LABEL! (%TIME%) >> "%LOG%"
call "%BASE%!SCRIPT!" >> "%LOG%" 2>&1
set /a ERR=!ERRORLEVEL!

:: 経過時間計測(終了)
for /f "tokens=1-3 delims=:." %%H in ("%TIME: =0%") do set /a TE=%%H*3600+%%I*60+%%J
set /a ELAPSED=TE-TS
if !ELAPSED! LSS 0 set /a ELAPSED+=86400

if !ERR! EQU 0 (
    echo [OK]   !LABEL! - !ELAPSED!秒 >> "%LOG%"
    endlocal & set /a OK+=1
    exit /b 0
)
echo [FAIL] !LABEL! - !ELAPSED!秒 - ERRORLEVEL=!ERR! >> "%LOG%"
endlocal & set /a FAIL+=1
exit /b !ERR!
summaryログで実行傾向を把握する
summary.log に毎回の成功/失敗件数を追記していくと、「最近○回連続で失敗している」「特定の曜日だけ失敗が多い」といった傾向の把握が簡単になります。バッチファイルでログを解析・分岐する完全ガイドの集計パターンと組み合わせると、サマリーから自動アラートを出す仕組みも作れます。

複数バッチ間で状態を共有する

マスタースクリプトと子バッチが分かれている構成では、子バッチ内でSETした変数はマスタースクリプトには引き継がれません(スコープが異なるため)。複数のバッチ間で情報を受け渡すには、ファイル経由で共有する必要があります。

環境ファイル(.env形式)で設定を共有する

バッチ共通の設定値や、前段のバッチが生成した値を後段のバッチで使いたい場合は、環境ファイルに書き出して for /f で読み込む方法が有効です。

process.env(共有設定ファイル)
:: バッチ共通の設定値と前段処理の結果を記録する
BASE_DIR=C:atch\project
LOG_DIR=C:atch\project\logs
DB_HOST=db-server-01
LAST_FETCH_DATE=2024-01-15
FETCH_COUNT=1523
環境ファイルを読み込むバッチ
@echo off
setlocal enabledelayedexpansion

set "ENV_FILE=C:atch\project\process.env"

if not exist "%ENV_FILE%" (
    echo [ERROR] 環境ファイルが見つかりません: %ENV_FILE%
    exit /b 1
)

:: 環境ファイルから変数をロード(:: コメント行はスキップ)
for /f "usebackq eol=: tokens=1* delims==" %%K in ("%ENV_FILE%") do (
    if not "%%K"=="" if not "%%L"=="" (
        set "%%K=%%L"
    )
)

:: 読み込んだ変数を使う
echo [INFO] ベースディレクトリ: !BASE_DIR!
echo [INFO] 最終取得日: !LAST_FETCH_DATE!
echo [INFO] 前回取得件数: !FETCH_COUNT!

:: 処理...
echo 処理実行中...

endlocal

一時ファイルで処理結果を次のバッチに渡す

前段バッチ: 結果を一時ファイルに書き出す
@echo off
setlocal

set "RESULT_FILE=%TEMP%atch_result.tmp"

:: データ取得処理
set /a FETCH_COUNT=1523
set "LAST_DATE=%DATE%"

:: 結果を一時ファイルに書き出す
(
    echo FETCH_COUNT=%FETCH_COUNT%
    echo LAST_DATE=%LAST_DATE%
    echo FETCH_STATUS=SUCCESS
) > "%RESULT_FILE%"

echo 取得完了: %FETCH_COUNT%件
endlocal
exit /b 0
後段バッチ: 一時ファイルから結果を読み込む
@echo off
setlocal enabledelayedexpansion

set "RESULT_FILE=%TEMP%atch_result.tmp"

if not exist "%RESULT_FILE%" (
    echo [ERROR] 前段バッチの結果ファイルがありません。前段バッチが失敗した可能性があります。
    exit /b 1
)

:: 一時ファイルから変数をロード
for /f "usebackq eol=: tokens=1* delims==" %%K in ("%RESULT_FILE%") do (
    set "%%K=%%L"
)

echo [INFO] 前段の取得件数: !FETCH_COUNT!
echo [INFO] 取得日: !LAST_DATE!

if /i "!FETCH_STATUS!" NEQ "SUCCESS" (
    echo [ERROR] 前段バッチが成功していません。処理を中断します。
    exit /b 1
)

:: 後段処理(例: 取得したデータを変換・登録)
echo !FETCH_COUNT!件のデータを変換・登録します...

:: 使用済みの一時ファイルを削除
del "%RESULT_FILE%"

endlocal
exit /b 0
環境ファイルと一時ファイルの使い分け
設定値や定数など変わらない値は環境ファイル(リポジトリに含める)、実行ごとに変わる処理結果は一時ファイル(%TEMP%以下)に書くのが適切です。一時ファイルは処理完了後に必ず削除してください。
注意: tokens=1* delims== の読み込みでは、値に = が含まれると正しく取得できません。パスワードなど = を含む値は別ファイルで管理するか、Base64エンコードして保存してください。

実用パターン集

「今日はすでに実行済み」を防ぐ:同日二重実行チェック

タスクスケジューラで1日1回動かすバッチに、万が一の重複実行防止を加えるパターンです。

同日二重実行防止
@echo off
setlocal enabledelayedexpansion

set "BASE=%~dp0"
set "TODAY=%DATE:~0,4%%DATE:~5,2%%DATE:~8,2%"
set "TODAY_FLAG=%BASE%flags\done_%TODAY%.flag"
set "LOG=%BASE%logs\process_%TODAY%.log"

if not exist "%BASE%flags" mkdir "%BASE%flags"
if not exist "%BASE%logs"  mkdir "%BASE%logs"

:: 本日実行済みチェック
if exist "%TODAY_FLAG%" (
    echo [SKIP] 本日(%DATE%)はすでに実行済みです。
    echo [%DATE% %TIME%] 二重実行スキップ >> "%LOG%"
    exit /b 0
)

echo [START] 処理開始: %DATE% %TIME%
echo [%DATE% %TIME%] 処理開始 >> "%LOG%"

:: メイン処理
echo データを処理中...
:: ... 処理内容 ...

set /a ERR=!ERRORLEVEL!

if !ERR! EQU 0 (
    :: 成功したら本日実行済みフラグを作成
    echo %DATE% %TIME% > "%TODAY_FLAG%"
    echo [%DATE% %TIME%] 処理完了 >> "%LOG%"
    echo 完了しました。
) else (
    echo [%DATE% %TIME%] 処理失敗 ERR=!ERR! >> "%LOG%"
    echo 処理が失敗しました。フラグは作成しません(次回も実行されます)。
    exit /b !ERR!
)

:: 古いフラグを自動削除(30日以上前)
forfiles /p "%BASE%flags" /s /m "done_*.flag" /d -30 /c "cmd /c del @path" 2>nul

endlocal
exit /b 0

完成版:状態管理付き業務バッチスクリプト

これまでのパターンをすべて統合した、実務で使える完成版スクリプトです。二重起動防止・ステップ別再開・実行ログ・エラー通知の仕組みを備えています。

state_manager.bat(完成版)
@echo off
setlocal enabledelayedexpansion
:: ============================================================
:: state_manager.bat - 状態管理付き業務バッチスクリプト
::
:: 使い方:
::   state_manager.bat           通常実行
::   state_manager.bat --force   前回の状態を無視して最初から実行
:: ============================================================

set "BASE=%~dp0"
set "FLAGDIR=%BASE%flags"
set "LOGDIR=%BASE%logs"
set "LOCK=%FLAGDIR%
unning.lock"
set "STATE=%FLAGDIR%\.current_step"
set "LOG=%LOGDIR%\process_%DATE:~0,4%%DATE:~5,2%%DATE:~8,2%_%TIME:~0,2%%TIME:~3,2%.log"
set /a TOTAL_STEPS=4

:: 引数解析
set "FORCE=0"
if /i "%~1"=="--force" set "FORCE=1"

:: ディレクトリ作成
if not exist "%FLAGDIR%" mkdir "%FLAGDIR%"
if not exist "%LOGDIR%"  mkdir "%LOGDIR%"

:: ── 二重起動チェック ──────────────────────────────────
if exist "%LOCK%" (
    set /p LOCK_INFO=<"%LOCK%"
    echo [ABORT] 実行中のプロセスがあります: !LOCK_INFO!
    echo [%DATE% %TIME%] 二重起動検知 !LOCK_INFO! >> "%LOG%"
    exit /b 1
)
echo PID=%~0 DATE=%DATE% TIME=%TIME% > "%LOCK%"
echo ======================================== >> "%LOG%"
echo  START: %DATE% %TIME% >> "%LOG%"
echo ======================================== >> "%LOG%"

:: ── 前回のステップを読み込む ─────────────────────────
set /a START_STEP=1
if %FORCE%==0 (
    if exist "%STATE%" (
        set /p DONE_STEP=<"%STATE%"
        set /a START_STEP=!DONE_STEP! + 1
        echo [RESUME] ステップ !DONE_STEP! から再開 >> "%LOG%"
        echo [RESUME] ステップ !START_STEP! から再開します。
    )
) else (
    if exist "%STATE%" del "%STATE%"
    echo [FORCE] 最初から実行します。
)

:: ── ステップ実行 ──────────────────────────────────────
set /a STEP=0
:run_loop
set /a STEP+=1
if !STEP! GTR %TOTAL_STEPS% goto :run_done

if !STEP! LSS !START_STEP! (
    echo [SKIP] Step !STEP! 完了済み
    goto :run_loop
)

:: 経過時間計測
for /f "tokens=1-3 delims=:." %%H in ("!TIME: =0!") do set /a TS=%%H*3600+%%I*60+%%J

echo [Step !STEP!/%TOTAL_STEPS%] 実行中...
call :do_step_!STEP!
set /a ERR=!ERRORLEVEL!

for /f "tokens=1-3 delims=:." %%H in ("!TIME: =0!") do set /a TE=%%H*3600+%%I*60+%%J
set /a ELAPSED=TE-TS
if !ELAPSED! LSS 0 set /a ELAPSED+=86400

if !ERR! NEQ 0 (
    :: 失敗: 前ステップまでを保存
    set /a PREV=!STEP!-1
    if !PREV! GTR 0 (echo !PREV! > "%STATE%") else (if exist "%STATE%" del "%STATE%")
    echo [FAIL] Step !STEP! - !ELAPSED!秒 - ERR=!ERR! >> "%LOG%"
    echo [FAIL] Step !STEP! が失敗しました(!ELAPSED!秒)。
    echo        --force を付けると最初から、引数なしで次回起動するとここから再開します。
    del "%LOCK%" 2>nul
    exit /b !ERR!
)
echo [OK]   Step !STEP! - !ELAPSED!秒 >> "%LOG%"
echo [OK]   Step !STEP! - !ELAPSED!秒
goto :run_loop

:run_done
:: 全ステップ完了
if exist "%STATE%" del "%STATE%"
del "%LOCK%" 2>nul
echo ======================================== >> "%LOG%"
echo  COMPLETE: %DATE% %TIME% >> "%LOG%"
echo ======================================== >> "%LOG%"
echo.
echo すべてのステップが正常に完了しました。

endlocal
exit /b 0

:: ── 各ステップの処理 ──────────────────────────────────
:do_step_1
echo   データ取得中...
timeout /t 1 /nobreak > nul
exit /b 0

:do_step_2
echo   データ変換中...
timeout /t 1 /nobreak > nul
exit /b 0

:do_step_3
echo   DB登録中...
timeout /t 1 /nobreak > nul
exit /b 0

:do_step_4
echo   クリーンアップ中...
timeout /t 1 /nobreak > nul
exit /b 0
–forceオプションで初回扱いにする
state_manager.bat --force を実行すると、状態ファイルを削除して最初から実行します。前回の失敗状態をリセットしたいときや、テスト実行時に便利です。

よくある質問(FAQ)

Qフラグファイルと状態ファイルはどこに置くべきですか?
Aバッチファイルと同じフォルダ内の flags\ サブフォルダを推奨します。%~dp0flags\ で相対パスで参照できるため、フォルダを移動しても動作します。C:\Windows\Temp など共有テンポラリに置くと、他のバッチと名前が衝突するリスクがあります。
Qバッチが強制終了されたときロックファイルが残りますが、どう対処しますか?
A2つの対策があります。①PIDをロックファイルに書いておき、次回起動時にそのPIDのプロセスが存在するか確認する(存在しなければ古いロックとしてリセット)。②ロックファイルの最終更新時刻を確認し、一定時間(例:1時間)以上古ければ自動リセットする。どちらを使うかは業務の性質に応じて選んでください。
Qset /p でファイルを読み込む際の注意点はありますか?
Aset /p VAR=<file.txt は1行目しか読み込めません。複数行のファイルを読む場合は for /f "usebackq delims=" %%L in ("file.txt") do set "VAR=%%L" を使い、最後の行に上書きされていく仕組みを利用するか、行ごとに処理してください。また、ファイルの末尾に改行がない場合、set /p が正常に読めないことがあります。
Qタスクスケジューラで実行するバッチで状態ファイルのパスはどう書けばいいですか?
A相対パス(%~dp0flags\)を使えばバッチファイルの場所を基準にするため問題ありません。タスクスケジューラは「作業ディレクトリ」を別途設定しますが、%~dp0 はバッチ自身の場所を参照するため影響を受けません。schtasksコマンドでタスクスケジューラを完全制御する完全ガイドでタスクスケジューラの設定方法を詳しく解説しています。
Q状態ファイルが破損した場合(書き込み中にバッチが強制終了など)はどうなりますか?
A状態ファイルが空になったり不完全な内容になる可能性があります。対策として、①書き込みは一時ファイルに行い、完了後に本来のファイルにリネームする(アトミックな書き込み)、②読み込み時に値が空や不正な場合は初回扱いにするフォールバックを入れる、の2つが有効です。完全な整合性が必要な場合はテキストファイルではなくSQLiteなどの組み込みDBの利用も検討してください。

まとめ

実現したいこと 使う仕組み 実装のポイント
前回の成否を次回に伝える フラグファイル(success.flag / error.flag) if exist で存在確認・終了後に作成/削除
二重起動を防ぐ ロックファイル(running.lock) PIDを記録してプロセス存在確認
差分処理(前回以降のみ) 最終実行日時の状態ファイル set /p で読み込み・完了後に上書き保存
途中から再開する 処理ステップ番号の状態ファイル 失敗時に前ステップ番号を保存・次回はそこから
同日二重実行を防ぐ 日付付きフラグファイル(done_20240115.flag) 今日の日付ファイルがあればスキップ
複数バッチ間で値を共有する 環境ファイル / 一時ファイル for /f でロード・処理後に削除
実行履歴を記録する 日付付きログ + summaryログ タイムスタンプ + OK/FAIL/SKIPを記録
関連記事
バッチファイルでフラグを更新する方法完全ガイド:変数フラグ・ファイルフラグの更新パターン。
外部ファイルのフラグを読み込んで処理を分岐する方法:フラグファイルを読み込んで処理を分岐する方法。
複数バッチファイルを一括管理・実行するマスタースクリプト完全ガイド:状態管理をマスタースクリプトに組み込む応用例。
バッチファイルで実行時刻・経過時間をログに記録する完全ガイド:ステップごとの経過時間を記録するパターン。
バッチファイルでログローテーションを実装する方法:古いログファイルとフラグファイルを自動削除する方法。