「古いログファイルを毎日手動で削除している」「バックアップが溜まりすぎてディスクが枯渇した」――そんな悩みをバッチファイル1本で解決できます。
本記事では forfiles コマンドを軸に、安全なドライラン・世代管理・PowerShell併用まで段階的に解説します。コピーして即使える実践例も3本収録しています。
目次
- forfilesコマンドとは
- 方法比較表
- 方法1:forfiles基本形
- 方法2:ドライラン付き安全削除
- 方法3:forfiles詳細オプション
- 方法4:PowerShellで削除
- 実践例A:ログファイル7日保持
- 実践例B:バックアップ世代管理30日
- 実践例C:複数フォルダ・複数拡張子一括クリーンアップ
- 落とし穴5選
- FAQ
- まとめ
forfilesコマンドとは
forfiles はWindows標準搭載のコマンドで、「最終更新日時がN日前より古いファイル」を一覧・実行できます。XP以降のすべてのWindowsに同梱されているため、外部ツール不要で使えるのが最大のメリットです。
基本構文は次のとおりです。
forfiles /p フォルダ /s /d -日数 /m パターン /c "cmd /c コマンド"
| オプション | 意味 |
|---|---|
/p パス |
検索対象フォルダ(省略時はカレント) |
/s |
サブフォルダも再帰検索 |
/d -N |
現在日からN日前より古いファイル |
/m パターン |
ファイル名パターン(例:*.log) |
/c "コマンド" |
マッチしたファイルに実行するコマンド |
方法比較表
| 方法 | 難易度 | ドライラン | 再帰 | フォルダ削除 | 向き不向き |
|---|---|---|---|---|---|
| forfiles基本形 | ★☆☆ | × | ○ | △ | 手軽にファイル削除 |
| forfiles+ドライラン | ★★☆ | ○ | ○ | △ | 本番前の安全確認 |
| forfiles詳細オプション | ★★☆ | △ | ○ | ○ | 複雑な条件指定 |
| PowerShell | ★★★ | ○ | ○ | ○ | 柔軟・高機能な処理 |
方法1:forfiles基本形
最もシンプルな形です。C:\Logs内の .log ファイルのうち、30日以上前に更新されたものを削除します。
@echo off forfiles /p C:\Logs /s /d -30 /m *.log /c "cmd /c del @path" echo 削除完了
@path はマッチしたファイルのフルパスに自動展開される組み込み変数です。パスにスペースが含まれる場合もそのまま使えます(ダブルクォートで囲まれた状態で渡されます)。
対象ファイルが1件も存在しない場合、forfiles はエラーコード1を返します。スクリプト後続処理でエラー判定をしている場合は後述の落とし穴5選を参照してください。
方法2:ドライラン付き安全削除
実際に削除する前に「何が削除されるか」を確認できるドライランモードを組み込んだ版です。本番環境に適用する前に必ず一度ドライランで動作確認することを強く推奨します。
@echo off
setlocal
:: ===== 設定 =====
set TARGET=C:\Logs
set DAYS=30
set EXT=*.log
set DRY_RUN=1
:: DRY_RUN=1: 削除せず一覧表示のみ / DRY_RUN=0: 実際に削除
:: =================
if %DRY_RUN%==1 (
echo [DRY RUN] 以下のファイルが削除対象です:
forfiles /p %TARGET% /s /d -%DAYS% /m %EXT% /c "cmd /c echo @path"
echo [DRY RUN] 実際に削除するには DRY_RUN=0 に変更してください。
) else (
echo 削除開始...
forfiles /p %TARGET% /s /d -%DAYS% /m %EXT% /c "cmd /c del @path"
echo 削除完了。
)
endlocal
DRY_RUN=1 のままスクリプトを実行すると、削除対象ファイルの一覧だけ表示されます。内容を確認したら DRY_RUN=0 に変更して本番削除を実行してください。
方法3:forfiles詳細オプション
forfilesで使える組み込み変数と、空フォルダを後処理で削除するパターンを紹介します。
forfiles組み込み変数一覧
| 変数 | 内容 | 例 |
|---|---|---|
@file |
ファイル名(拡張子付き) | app.log |
@fname |
ファイル名(拡張子なし) | app |
@ext |
拡張子 | .log |
@path |
フルパス(クォート付き) | "C:\Logs\app.log" |
@relpath |
相対パス(クォート付き) | ".\sub\app.log" |
@isdir |
ディレクトリなら TRUE | FALSE |
@fsize |
ファイルサイズ(バイト) | 1024 |
@fdate |
最終更新日 | 2024/01/15 |
@ftime |
最終更新時刻 | 13:45:00 |
古いファイル削除後に空フォルダも削除する
@echo off
setlocal
set TARGET=C:\Backup
set DAYS=30
:: まずファイルを削除
forfiles /p %TARGET% /s /d -%DAYS% /m *.* /c "cmd /c if @isdir==FALSE del @path"
:: 次に空フォルダを再帰的に削除(最深部から繰り返す)
:loop
for /f "delims=" %%d in ('dir /s /b /ad "%TARGET%" ^| sort /r') do (
rd "%%d" 2>nul
)
for /f %%x in ('dir /s /b /ad "%TARGET%" 2^>nul') do goto loop
echo フォルダ整理完了。
endlocal
ファイル削除後に空になったフォルダを rd で削除します。sort /r で深い階層から処理することで、親フォルダが空になった場合も正しく削除できます。
方法4:PowerShellで削除
PowerShellを使うと、LastWriteTime プロパティで柔軟な日付判定ができます。バッチから呼び出す形で使います。
@echo off
:: PowerShellで30日以前のファイルを削除(ドライランモード付き)
set TARGET=C:\Logs
set DAYS=30
set DRY_RUN=1
powershell -NoProfile -ExecutionPolicy Bypass -Command ^
"$dryRun = [bool]%DRY_RUN%; ^
$cutoff = (Get-Date).AddDays(-%DAYS%); ^
$files = Get-ChildItem -Path '%TARGET%' -Recurse -File ^
^| Where-Object { $_.LastWriteTime -lt $cutoff }; ^
if ($dryRun) { ^
Write-Host '[DRY RUN] 削除対象: '$files.Count' 件'; ^
$files ^| ForEach-Object { Write-Host ' '$_ }; ^
} else { ^
$files ^| Remove-Item -Force; ^
Write-Host '削除完了: '$files.Count' 件'; ^
}"
PowerShellの Get-Date と AddDays() は時刻単位の精度で比較できます。「N日前の今の時刻より古い」という厳密な判定が可能です(forfilesは日付のみ比較)。
PowerShell単体スクリプト(.ps1)として書く場合はバッチのエスケープが不要になり、さらに可読性が上がります。詳細はバッチでファイルを削除する方法完全ガイドを参照してください。
実践例A:ログファイル7日保持スクリプト
Webサーバーや業務アプリが吐き出すログファイルを7日間保持し、古いものは自動削除する定型スクリプトです。実行ログも残すため、いつ何件削除したかが後から確認できます。
@echo off
setlocal enabledelayedexpansion
:: ===== 設定 =====
set TARGET=C:\Logs\App
set DAYS=7
set EXT=*.log
set LOGFILE=C:\Logs\cleanup_%date:~0,4%%date:~5,2%%date:~8,2%.log
:: =================
echo [%date% %time%] ログクリーンアップ開始 >> %LOGFILE%
set COUNT=0
for /f "usebackq delims=" %%f in (`forfiles /p "%TARGET%" /s /d -%DAYS% /m %EXT% /c "cmd /c echo @path" 2^>nul`) do (
del %%f
echo 削除: %%f >> %LOGFILE%
set /a COUNT+=1
)
echo [%date% %time%] 削除完了: !COUNT! 件 >> %LOGFILE%
endlocal
実行ログは日付付きファイル名で保存されるため、ログ自体が溜まりすぎることもありません。ログファイルの管理方法についてはバッチファイルで実行ログを記録する方法も参照してください。
実践例B:バックアップ世代管理30日スクリプト
日次バックアップが溜まっていく場合に、30日以上前のバックアップフォルダごと削除する世代管理スクリプトです。誤削除防止のため削除前に確認ダイアログを表示します。
@echo off
setlocal
:: ===== 設定 =====
set BACKUP_ROOT=D:\Backup
set KEEP_DAYS=30
set LOGFILE=D:\Backup\gen-manage.log
:: =================
:: 削除対象フォルダを一覧表示
echo 以下のバックアップを削除します(%KEEP_DAYS%日以上前):
forfiles /p "%BACKUP_ROOT%" /d -%KEEP_DAYS% /c "cmd /c if @isdir==TRUE echo @path"
set /p CONFIRM=本当に削除しますか? [y/N]:
if /i not "%CONFIRM%"=="y" (
echo キャンセルしました。
exit /b 0
)
:: フォルダごと削除
echo [%date% %time%] 世代管理削除開始 >> %LOGFILE%
forfiles /p "%BACKUP_ROOT%" /d -%KEEP_DAYS% /c "cmd /c if @isdir==TRUE (rd /s /q @path && echo 削除: @path >> %LOGFILE%)"
echo [%date% %time%] 完了 >> %LOGFILE%
echo 削除完了。
endlocal
ログローテーションと組み合わせることで、バックアップストレージを自動的に一定量に保てます。ログローテーションの詳細はバッチファイルでログローテーションを実装する方法を参照してください。
実践例C:複数フォルダ・複数拡張子一括クリーンアップ
複数のフォルダと拡張子をまとめて処理したい場合のパターンです。設定部分を編集するだけで対象を増減できます。
@echo off
setlocal enabledelayedexpansion
:: ===== 設定:フォルダ・拡張子・保持日数 =====
set RULE_COUNT=4
set RULE_1_PATH=C:\Logs\Web
set RULE_1_EXT=*.log
set RULE_1_DAYS=7
set RULE_2_PATH=C:\Logs\App
set RULE_2_EXT=*.log
set RULE_2_DAYS=14
set RULE_3_PATH=D:\Temp
set RULE_3_EXT=*.*
set RULE_3_DAYS=3
set RULE_4_PATH=D:\Backup\Daily
set RULE_4_EXT=*.zip
set RULE_4_DAYS=30
:: ===========================================
for /l %%i in (1,1,%RULE_COUNT%) do (
set P=!RULE_%%i_PATH!
set E=!RULE_%%i_EXT!
set D=!RULE_%%i_DAYS!
echo [%%i] !P! の !E! を !D! 日保持で削除中...
forfiles /p "!P!" /s /d -!D! /m !E! /c "cmd /c del @path" 2>nul
echo 完了。
)
echo すべてのルール処理完了。
endlocal
ルールは RULE_COUNT を増やして RULE_N_PATH/EXT/DAYS 変数を追加するだけで拡張できます。
落とし穴5選
落とし穴1:対象ファイルがゼロ件のときエラーコード1が返る
forfiles は削除対象が1件もない場合にエラーコード1を返します。これはWindowsの仕様です。スクリプトの後続処理でエラーコードを判定している場合は、forfiles ... 2>nul || (echo 対象なし) と明示的にハンドリングしてください。
:: 対象なしを正常扱いにする例 forfiles /p C:\Logs /d -30 /m *.log /c "cmd /c del @path" 2>nul if errorlevel 1 echo [INFO] 削除対象ファイルなし(正常)
落とし穴2:/d -0 は全ファイルが対象になる
/d -0 は「今日より0日前より古い」= 「今日より前のすべてのファイル」を意味します。意図せず全ファイルを削除してしまいます。最低でも /d -1(昨日以前)を指定しましょう。
落とし穴3:読み取り専用ファイルは del で削除できない
読み取り専用属性のファイルは del 単体では削除できずエラーになります。削除前に attrib -r で属性を解除するか、del /f オプション(強制削除)を使います。
forfiles /p C:\Logs /d -30 /m *.log /c "cmd /c attrib -r @path && del /f @path"
落とし穴4:スペースを含むパスは /p をクォートで囲む
/p に渡すパスにスペースが含まれる場合はダブルクォートが必要です。
:: NG: スペースを含むパスは失敗する forfiles /p C:\My Logs /d -30 /m *.log /c "cmd /c del @path" :: OK: クォートで囲む forfiles /p "C:\My Logs" /d -30 /m *.log /c "cmd /c del @path"
落とし穴5:子要素が残っているフォルダは @isdir==TRUE で削除できない
forfilesでフォルダを対象にしても、子ファイルが残っているフォルダは rd で削除できません。先にファイルを消してから rd で空フォルダを削除する、もしくは rd /s /q でサブツリーごと削除する必要があります。空フォルダ削除の詳細はバッチファイルで空フォルダを削除する方法を参照してください。
FAQ
/d は最終更新日時のみ対象です。作成日時で絞り込むにはPowerShellの CreationTime プロパティを使ってください。Get-ChildItem | Where-Object { $_.CreationTime -lt (Get-Date).AddDays(-30) }Where-Object { $_.Name -notlike "important*" } で除外パターンを指定するか、forfiles内で if not @file=="important.log" del @path のように名前チェックを入れます。echo @path,@fdate,@fsize >> log.csv を実行します。ヘッダーはスクリプト冒頭で echo path,date,size > log.csv と書き出してください。forfiles /p \\\\server\\share\\logs /d -30 /m *.log /c "cmd /c del @path" のようにUNCパスをそのまま渡せます。ただしネットワークの応答が遅い場合はタイムアウトに注意してください。cmd.exe、「引数」に /c "C:\path\to\cleanup.bat" >> "C:\Logs\task.log" 2>&1 を設定します。>> でログを残すと実行履歴の確認が容易になります。2^>nul を付けるとエラーメッセージを抑制しつつ処理を継続できます。削除できなかったファイルをログに残す場合は del @path 2^>nul || echo FAILED: @path >> error.log のようにリダイレクトを組み合わせます。まとめ
| 用途 | 推奨方法 |
|---|---|
| 手軽にファイル削除したい | 方法1:forfiles基本形 |
| 本番適用前に安全確認したい | 方法2:ドライラン付き |
| 空フォルダも片付けたい | 方法3:forfiles詳細+rd |
| 日付判定を柔軟にしたい・PowerShellに慣れている | 方法4:PowerShell |
| 複数フォルダをまとめて管理したい | 実践例C:複数ルール |
ファイル削除全般の詳細はバッチファイルでファイルを削除する方法完全ガイドもあわせてご覧ください。