バッチファイルで中間データを一時ファイルに書き出す処理はよくありますが、「固定ファイル名を使い回していたら別プロセスと衝突してデータが壊れた」「処理途中でエラーが出て一時ファイルが残り続けた」「%TEMP% の場所を間違えて本番フォルダに一時ファイルが散乱した」といったトラブルは珍しくありません。
本記事では、一時ファイルを衝突なく作成し・確実に削除するためのパターンを、純粋 cmd 方式と PowerShell 連携方式の両面から解説します。
%TEMP% と %TMP% の違いと使い方
Windows には一時ファイル用の環境変数が 2 つあります。
| 変数 | 設定場所 | 既定値(Windows 10/11) | 特徴 |
|---|---|---|---|
%TEMP% |
ユーザー環境変数 | C:\Users\ユーザー名\AppData\Local\Temp |
ユーザーごとに分離・書き込み権限あり |
%TMP% |
ユーザー環境変数 | 同上(%TEMP% と同じ値が多い) | 古いアプリとの互換性のために残存 |
%SYSTEMROOT%\Temp |
システム環境変数 | C:\Windows\Temp |
全ユーザー共有・管理者権限必要 |
バッチファイルでは %TEMP% を第一選択にします。ユーザーごとに分離されているため、複数ユーザーが同じバッチを同時実行しても互いに干渉しません。また SSD 上に配置されることが多く I/O も高速です。
@echo off
setlocal enableextensions enabledelayedexpansion
rem %TEMP% が未設定・存在しない場合に備えたフォールバック
set "TMPROOT=%TEMP%"
if not defined TMPROOT set "TMPROOT=%TMP%"
if not defined TMPROOT set "TMPROOT=C:\Windows\Temp"
if not exist "%TMPROOT%\" (
echo エラー: 一時フォルダが見つかりません: %TMPROOT% 1>&2
exit /b 1
)
固定ファイル名を使うと何が起きるか
「%TEMP%\work.tmp」のような固定名を使い回すと以下の問題が発生します。
| 問題 | 発生条件 | 影響 |
|---|---|---|
| 同名衝突 | 同じバッチを複数プロセスで並列実行 | 一方のデータが上書きされてデータ破損 |
| 前回の残骸を誤読 | 前回の実行でクリーンアップが走らなかった | 古いデータを正しいものと誤認して処理が進む |
| 自動削除の誤爆 | 他のバッチが同じ名前で作成・削除 | 処理中のファイルが削除されてエラー |
一時ファイル名には必ずユニークな識別子(日時・ランダム値・プロセス ID)を含めます。
方法1:純粋 cmd で一時ファイルを作成する
%RANDOM%(0〜32767 のランダム整数)と日時を組み合わせて衝突しにくい名前を生成します。完全なユニーク性の保証はないため、作成前に if not exist で確認します。
@echo off
setlocal enableextensions enabledelayedexpansion
set "TMPROOT=%TEMP%"
rem 日時 + RANDOM で衝突しにくいファイル名を生成
set "TMPFILE="
for /l %%i in (1,1,10) do (
if not defined TMPFILE (
set "CAND=%TMPROOT%\bat_%DATE:~0,4%%DATE:~5,2%%DATE:~8,2%_%TIME:~0,2%%TIME:~3,2%%TIME:~6,2%_!RANDOM!.tmp"
rem スペースを除去(TIME は 1 桁時刻に先行スペースが入る)
set "CAND=!CAND: =0!"
if not exist "!CAND!" set "TMPFILE=!CAND!"
)
)
if not defined TMPFILE (
echo エラー: 一時ファイル名の生成に失敗しました 1>&2
exit /b 1
)
rem 空ファイルを作成
type nul > "%TMPFILE%"
if errorlevel 1 (
echo エラー: 一時ファイルの作成に失敗しました 1>&2
exit /b 1
)
echo 一時ファイル: %TMPFILE%
方法2:PowerShell の GetTempFileName() を使う(推奨)
.NET の [IO.Path]::GetTempFileName() は、ファイルを作成しながらパスを返すアトミックな操作です。TOCTOU(Time of Check to Time of Use)競合が発生しないため、最も安全です。
@echo off
setlocal enableextensions
rem [IO.Path]::GetTempFileName() は空ファイルを作成してパスを返す
for /f "usebackq delims=" %%P in (
`powershell -NoProfile -Command "[IO.Path]::GetTempFileName()"`
) do set "TMPFILE=%%P"
if not defined TMPFILE (
echo エラー: PowerShell 呼び出しに失敗しました 1>&2
exit /b 1
)
if not exist "%TMPFILE%" (
echo エラー: 一時ファイルが作成されませんでした 1>&2
exit /b 1
)
echo 一時ファイル: %TMPFILE%
[IO.Path]::GetTempFileName() は %TEMP% フォルダ内にtmpXXXX.tmp(X は 16 進数)という名前の空ファイルを実際に作成してから、そのフルパスを返します。ファイルの確認と作成が不可分なアトミック操作であるため、2 つのプロセスが同時に呼び出しても必ず異なるパスが返されます。
一方
%RANDOM% 方式は「確認」と「作成」の間に別プロセスが同名ファイルを作れる余地があります。一時ディレクトリを作成する
展開・変換などで複数の中間ファイルが必要な場合は一時ディレクトリを確保します。[IO.Path]::GetRandomFileName() はランダムな文字列を返す(ファイルは作らない)ため、これをフォルダ名として mkdir で作成します。
@echo off
setlocal enableextensions
set "TMPROOT=%TEMP%"
rem GetRandomFileName() でランダム文字列を生成(例: abc1de2f.ghi)
for /f "usebackq delims=" %%N in (
`powershell -NoProfile -Command "[IO.Path]::GetRandomFileName()"`
) do set "TMPDIR=%TMPROOT%\%%N"
if not defined TMPDIR (
echo エラー: 一時フォルダ名の生成に失敗しました 1>&2
exit /b 1
)
mkdir "%TMPDIR%"
if errorlevel 1 (
echo エラー: 一時フォルダの作成に失敗しました 1>&2
exit /b 1
)
echo 一時フォルダ: %TMPDIR%
try-finally パターン:処理後に必ずクリーンアップする
バッチには例外処理機構がないため、成功・失敗どちらの場合もかならず :cleanup ラベルを通るよう設計します。これが最も重要な「一時ファイルを残さない」ための設計パターンです。
@echo off
setlocal enableextensions enabledelayedexpansion
set "TMPROOT=%TEMP%"
set "TMPFILE="
set "EXITCODE=0"
rem === 一時ファイル作成 ===
for /f "usebackq delims=" %%P in (
`powershell -NoProfile -Command "[IO.Path]::GetTempFileName()"`
) do set "TMPFILE=%%P"
if not defined TMPFILE (set "EXITCODE=1" & goto :cleanup)
rem === メイン処理 ===
echo 処理中のデータ > "%TMPFILE%"
if errorlevel 1 (set "EXITCODE=2" & goto :cleanup)
rem (処理の続き...)
rem === 成功終了 ===
set "EXITCODE=0"
goto :cleanup
rem === クリーンアップ(常に実行) ===
:cleanup
if defined TMPFILE if exist "%TMPFILE%" del /q "%TMPFILE%" 2>nul
endlocal & exit /b %EXITCODE%
endlocal で setlocal スコープを終了してから exit /b で終了コードを返します。これを 1 行にまとめることで、endlocal 後に %EXITCODE% が展開されなくなる問題を回避しています。変数スコープの詳細はバッチファイルの変数展開トラブル完全解決ガイドを参照してください。
一時ファイルと一時ディレクトリを両方使うテンプレート
@echo off
setlocal enableextensions enabledelayedexpansion
set "TMPROOT=%TEMP%"
set "TMPFILE="
set "TMPDIR="
set "EXITCODE=0"
rem --- 初期化チェック ---
if not exist "%TMPROOT%\" (
echo エラー: TEMPが見つかりません 1>&2 & exit /b 1
)
rem --- 一時ファイル作成 ---
for /f "usebackq delims=" %%P in (
`powershell -NoProfile -Command "[IO.Path]::GetTempFileName()"`
) do set "TMPFILE=%%P"
if not defined TMPFILE (set "EXITCODE=2" & goto :cleanup)
rem --- 一時ディレクトリ作成 ---
for /f "usebackq delims=" %%N in (
`powershell -NoProfile -Command "[IO.Path]::GetRandomFileName()"`
) do set "TMPDIR=%TMPROOT%\%%N"
if not defined TMPDIR (set "EXITCODE=3" & goto :cleanup)
mkdir "%TMPDIR%" 2>nul
if errorlevel 1 (set "EXITCODE=4" & goto :cleanup)
rem ========== メイン処理 ==========
echo 処理データ > "%TMPFILE%"
if errorlevel 1 (set "EXITCODE=10" & goto :cleanup)
copy /y "%TMPFILE%" "%TMPDIR%\work.txt" >nul
if errorlevel 1 (set "EXITCODE=11" & goto :cleanup)
rem ここに実際の処理を追加
rem =================================
set "EXITCODE=0"
:cleanup
if defined TMPFILE if exist "%TMPFILE%" del /q "%TMPFILE%" 2>nul
if defined TMPDIR if exist "%TMPDIR%\" rmdir /s /q "%TMPDIR%" 2>nul
endlocal & exit /b %EXITCODE%
一時ファイルの内容を処理する
コマンドの出力を一時ファイルに書き出してから処理する場面は多くあります。
@echo off
setlocal enableextensions
for /f "usebackq delims=" %%P in (
`powershell -NoProfile -Command "[IO.Path]::GetTempFileName()"`
) do set "TMPFILE=%%P"
rem コマンド出力を一時ファイルに書き込む
dir /b "C:\work\*.log" > "%TMPFILE%" 2>nul
rem 一時ファイルを読み込んで処理
for /f "usebackq eol=| delims=" %%L in ("%TMPFILE%") do (
echo 処理中: %%L
)
rem 削除
del /q "%TMPFILE%" 2>nul
endlocal
for /f ... in (`command`) はコマンドの全出力が完了してから処理を開始します。出力が大量の場合、バックティック形式では cmd.exe のバッファが溢れる場合があります。一時ファイル経由にすることで大量出力を安全に処理できます。また出力結果を複数回参照したい場合も一時ファイルが便利です。削除できないときのリトライ処理
ウイルス対策ソフトによる一時スキャンや、別プロセスがファイルを開いたままの状態では削除が失敗します。短い待機時間を挟んでリトライすることで解決することがほとんどです。
@echo off
:delete_file
rem 引数1: 削除するファイルパス
set "TARGET=%~1"
if not defined TARGET exit /b 0
if not exist "%TARGET%" exit /b 0
set "RETRY=0"
:del_retry
del /q "%TARGET%" 2>nul
if not exist "%TARGET%" (
echo 削除完了: %TARGET%
exit /b 0
)
set /a RETRY+=1
if %RETRY% lss 5 (
echo 削除待機中 (%RETRY%/5)...
timeout /t 2 /nobreak >nul
goto :del_retry
)
echo 警告: ファイルを削除できませんでした: %TARGET% 1>&2
exit /b 1
@echo off
set "TMPDIR=C:\Temp\mywork.tmp"
set "RETRY=0"
:rmdir_retry
if not exist "%TMPDIR%\" exit /b 0
rmdir /s /q "%TMPDIR%" 2>nul
if not exist "%TMPDIR%\" (
echo フォルダ削除完了
exit /b 0
)
set /a RETRY+=1
if %RETRY% lss 3 (
timeout /t 3 /nobreak >nul
goto :rmdir_retry
)
rem 最終手段: PowerShell で強制削除
powershell -NoProfile -Command "Remove-Item -LiteralPath '%TMPDIR%' -Recurse -Force" 2>nul
if not exist "%TMPDIR%\" exit /b 0
echo エラー: フォルダを削除できませんでした 1>&2
exit /b 1
並列実行での衝突を防ぐ(複数プロセスで同じバッチを実行)
同じバッチファイルを複数のタスクスケジューラジョブや並列実行で動かす場合、%RANDOM% だけでは衝突する可能性があります。Windows の %ERRORLEVEL% は使えませんが、PowerShell でプロセス ID を取得してファイル名に含めることで確実にユニーク化できます。
@echo off
setlocal enableextensions
rem 現在のプロセスIDを取得(PowerShell 経由)
for /f "usebackq delims=" %%I in (
`powershell -NoProfile -Command "$PID"`
) do set "PID_CURR=%%I"
rem PID + RANDOM + 日時 でほぼ衝突しないファイル名
set "TMPFILE=%TEMP%\bat_%PID_CURR%_%RANDOM%.tmp"
type nul > "%TMPFILE%"
echo 一時ファイル: %TMPFILE%
rem 処理後クリーンアップ
del /q "%TMPFILE%" 2>nul
endlocal
古い一時ファイルを定期クリーンアップする
バッチが異常終了した場合などに一時ファイルが残ることがあります。定期的に古い一時ファイルを削除するクリーンアップスクリプトを合わせて用意しておくと安全です。
@echo off
setlocal enableextensions
rem %TEMP% 内の bat_ で始まる .tmp ファイルのうち、1日以上前のものを削除
forfiles /p "%TEMP%" /m "bat_*.tmp" /d -1 /c "cmd /c del /q @path" 2>nul
rem 空の一時フォルダを削除(フォルダ内が空のもの)
for /d %%D in ("%TEMP%\????????.???") do (
dir /b "%%D" 2>nul | findstr "." >nul
if errorlevel 1 (
echo 空フォルダを削除: %%D
rmdir "%%D" 2>nul
)
)
endlocal
forfiles の /d -1 は「1日以上前に更新されたファイル」を意味します。古いファイルを削除する forfiles の詳しい使い方は古いファイルを自動削除する方法完全ガイドを参照してください。長いパス・日本語パスへの対応
%TEMP% のパスにユーザー名が含まれ、日本語ユーザー名だったり深い階層になった場合も想定して対処します。
@echo off set "TMPFILE=C:\Users\日本語ユーザー名\AppData\Local\Temp\work.tmp" rem \\?\ プレフィックスで MAX_PATH 制限を回避 set "LONG_TMPFILE=\\?\%TMPFILE%" del /q "%LONG_TMPFILE%" 2>nul rem または PowerShell の Remove-Item -LiteralPath を使う powershell -NoProfile -Command "Remove-Item -LiteralPath '%TMPFILE%' -Force" 2>nul
よくある失敗パターン
| 失敗パターン | 問題 | 対処法 |
|---|---|---|
固定パス %TEMP%\work.tmp を使う |
並列実行で上書き・読み間違い | GetTempFileName() または日時+RANDOM で名前を生成 |
クリーンアップなしで exit |
一時ファイルが永久に残る | :cleanup ラベルを用意して必ず削除 |
| 削除成功確認なしで削除 | ロック中の削除失敗を見逃す | 削除後 if exist で確認し、必要ならリトライ |
type nul > file と if not exist を分ける |
確認と作成の間に別プロセスが同名ファイルを作れる | GetTempFileName() でアトミックに作成 |
| クリーンアップでパスをハードコード | 変数が未定義のまま del を呼ぶ |
if defined TMPFILE if exist "%TMPFILE%" の二重チェック |
%ERRORLEVEL% を cleanup 後に参照 |
del が上書きして EXITCODE を見失う | クリーンアップ前に set "EXITCODE=%ERRORLEVEL%" で保存 |
よくある質問(FAQ)
%RANDOM% 方式は「ファイルの存在確認」と「ファイルの作成」が別々のステップのため、確認後・作成前の間に別プロセスが同名ファイルを作成できる(TOCTOU競合)可能性があります。GetTempFileName() は作成と同時にパスを返すため、この問題が原理的に発生しません。シングルプロセス環境や再現性の低い競合を許容できる場合は %RANDOM% 方式でも十分です。endlocal で変数はリセットされますが、クリーンアップは endlocal の直前で行うため問題ありません。endlocal & exit /b %EXITCODE% の形にすることで、endlocal 実行前に %EXITCODE% が展開されます。forfiles /p "%TEMP%" /m "bat_*.tmp" /d -1 /c "cmd /c del /q @path" で1 日以上経過した一時ファイルをまとめて削除できます。または Windows 10/11 のストレージセンサー(設定→システム→ストレージ)で自動クリーンアップを有効にする方法もあります。%TEMP% はカレントユーザーのプロファイル内にあり、そのユーザーだけが読み書きできます(他のユーザーからは見えません)。管理者権限が必要な処理で共有が必要な場合は C:\Windows\Temp を使いますが、管理者権限が必要でありセキュリティリスクも上がるため慎重に扱ってください。%TEMP%(通常 C: ドライブ)への書き込みがボトルネックになります。環境変数 TMPROOT に高速な SSD ドライブを指定するか、PowerShell の New-TemporaryFile cmdlet を使ってシステム最適のパスを選ばせる方法もあります。まとめ
バッチファイルで一時ファイルを安全に扱うポイントをまとめます。
- 場所は
%TEMP%を起点(ユーザーごと分離・書き込み権限あり) - ファイル名は
GetTempFileName()でアトミック生成(TOCTOU 競合なし) - ディレクトリは
GetRandomFileName()+mkdir(ランダム名で衝突回避) - 必ず
:cleanupラベルを用意(成功・失敗どちらでもクリーンアップを通す) - 削除は
if definedとif existの二重チェック(未定義変数での誤削除防止) - 削除失敗はリトライ(ウイルス対策ソフトのロックは数秒で解放される)
- EXITCODE を事前に保存(
delが上書きする前に保存)
ファイルの削除コマンドの詳細はファイルを削除する方法完全ガイド、日時ファイル名の付け方は日付と時刻をファイル名に挿入する方法完全ガイドも参照してください。

