「サブフォルダも含めて全ファイルに処理を適用したい」「特定の拡張子だけを再帰的に検索してコピーしたい」——このような要件はバッチファイルで頻繁に登場します。
Windowsには for /r・xcopy /s・robocopy /e・PowerShell Get-ChildItem -Recurse など複数の再帰処理手段があり、用途によって使い分けが必要です。本記事では各手法の特徴を比較し、除外フォルダの指定・ドライランモード・ログ記録まで実践コードで解説します。
- for /r でサブフォルダを含む全ファイルに処理を適用する基本パターン
- for /r の変数展開(%%~nxF・%%~dpF・%%~zF など)の使い方
- 複数の拡張子を同時にフィルタリングして再帰処理する方法
- for /r /d でサブフォルダのみを再帰的に列挙する方法
- 特定フォルダ(.git・node_modules など)を除外して再帰処理する方法
- ドライラン(–dry-run)モードで削除・移動の実行前に対象を確認する方法
- 処理件数・失敗件数をカウントしてログに記録する方法
- PowerShell Get-ChildItem -Recurse で深度制限・複合フィルタを使う方法
再帰処理手法の比較
| 手法 | 用途 | 拡張子フィルタ | 除外指定 | ログ対応 |
|---|---|---|---|---|
| for /r | ファイル単位の任意処理 | ◎(ワイルドカード) | △(if で除外) | ◎(自前実装) |
| xcopy /s /e | ディレクトリ構造ごとコピー | ◎(/include) | △(/exclude) | ○(stdout) |
| robocopy /e | 高機能コピー・ミラーリング | ◎(マスク指定) | ◎(/xd・/xf) | ◎(/log) |
| PowerShell Get-ChildItem -Recurse | 複合フィルタ・深度制限 | ◎(-Filter・-Include) | ◎(Where-Object) | ◎(Out-File) |
ファイル1件ずつに任意の処理(リネーム・内容編集など)を適用するなら for /r が基本です。再帰コピーは robocopy /e(除外指定・ログ・差分コピーが必要)かxcopy /s(シンプルなコピー)を使います。深度制限や複合フィルタが必要なら PowerShell に切り替えてください。フォルダ単位のループ(for /d・ネストループ)は複数フォルダをループして一括処理する完全ガイドも参照してください。
for /r の基本と変数展開の使い方
for /r "起点フォルダ" %%変数 in (パターン) do の構文で、指定フォルダ以下のすべてのサブフォルダを含むファイルに繰り返し処理を適用できます。ループ変数に付く修飾子(チルダ展開)でパス・ファイル名・拡張子・サイズを個別に取得できます。
@echo off
setlocal
REM for /r "起点" %%F in (パターン) → サブフォルダを含む全ファイルをループ
for /r "C:\data" %%F in (*) do (
echo フルパス : %%F
echo ファイル名 : %%~nxF
echo 名前のみ : %%~nF
echo 拡張子のみ : %%~xF
echo 親フォルダ : %%~dpF
echo サイズ(byte): %%~zF
echo 更新日時 : %%~tF
echo ---
)
endlocal
@echo off
setlocal
set "ROOTDIR=C:\projects"
REM .log ファイルのみ再帰的に処理する
for /r "%ROOTDIR%" %%F in (*.log) do (
echo 処理: %%~nxF 場所: %%~dpF
REM 処理本体...
)
endlocal
for /r %%F in (*.txt) のように起点フォルダを省略すると、カレントディレクトリが起点になります。バッチのある場所を起点にしたい場合は for /r "%~dp0" %%F in (...) と%~dp0(スクリプトの親フォルダ)を指定するのが確実です。複数の拡張子を同時にフィルタリングして再帰処理する
for /r の in(...) に複数のパターンをスペース区切りで並べると、複数の拡張子を同時にフィルタリングできます。
@echo off
setlocal
set "ROOTDIR=C:\data"
REM in(...) にスペース区切りで複数パターンを指定
for /r "%ROOTDIR%" %%F in (*.csv *.tsv *.txt) do (
echo 対象: %%F
)
endlocal
@echo off
setlocal
for /r "C:\data" %%F in (*.csv *.log *.txt) do (
if /i "%%~xF"==".csv" (
echo [CSV] %%~nxF
REM CSVの処理...
) else if /i "%%~xF"==".log" (
echo [LOG] %%~nxF
REM ログの処理...
) else (
echo [TXT] %%~nxF
)
)
endlocal
拡張子の取得についてはファイルの拡張子を取得する方法も参照してください。
サブフォルダのみを再帰的に列挙する(for /r /d)
for /r /d を使うとファイルではなくサブフォルダのみを再帰的に列挙できます。フォルダ構造の確認や、各サブフォルダに対して処理を適用する用途に使います。
@echo off
setlocal
set "ROOTDIR=C:\projects"
REM /D を付けるとディレクトリのみを対象にする
for /r "%ROOTDIR%" /D %%D in (*) do (
echo フォルダ: %%D
)
endlocal
@echo off
setlocal enabledelayedexpansion
set "ROOTDIR=C:\data"
echo フォルダ,ファイル数
for /r "%ROOTDIR%" /D %%D in (*) do (
set "CNT=0"
for %%F in ("%%D\*") do (
if not "%%~aF"=="d" set /a "CNT+=1"
)
echo %%D,!CNT!
)
endlocal
特定フォルダを除外して再帰処理する
for /r は除外フォルダを直接指定できないため、ループ内で if による除外判定が必要です。.git・node_modules・backup などのフォルダを除外する実践パターンを紹介します。
@echo off
setlocal
set "ROOTDIR=C:\projects"
for /r "%ROOTDIR%" %%F in (*.js *.ts) do (
REM .git・node_modules を含むパスをスキップ
echo %%~dpF | findstr /i /c:"\\.git\\" /c:"\\node_modules\\" >nul 2>&1
if %ERRORLEVEL% neq 0 (
echo 処理: %%F
)
)
endlocal
@echo off
setlocal enabledelayedexpansion
set "ROOTDIR=C:\data"
REM 除外フォルダ名リスト(| 区切り)
set "EXCLUDES=\backup\|\archive\|\temp\|\tmp\"
for /r "%ROOTDIR%" %%F in (*.csv) do (
set "FPATH=%%~dpF"
set "SKIP=0"
REM パスに除外フォルダが含まれるか確認
echo !FPATH! | findstr /i /c:"\backup\" /c:"\archive\" /c:"\temp\" >nul 2>&1
if !ERRORLEVEL! equ 0 set "SKIP=1"
if "!SKIP!"=="0" (
echo 処理: %%~nxF 場所: !FPATH!
) else (
echo スキップ: %%~nxF 場所: !FPATH!
)
)
endlocal
コピー・ミラーリングのユースケースでは
for /r よりrobocopy /xd backup /xd .git のほうが除外指定が直感的で確実です。詳細はフォルダをコピーする完全ガイドを参照してください。ドライランモードで実行前に対象ファイルを確認する
大量ファイルの削除・移動などの不可逆な処理は、実行前に対象を確認するドライランモードを実装するのが安全です。引数 --dry-run の有無で動作を切り替えます。
@echo off
setlocal
set "ROOTDIR=C:\data"
set "DRY_RUN=1"
REM 引数で --dry-run を解除する
if /i "%~1"=="--execute" set "DRY_RUN=0"
if "%DRY_RUN%"=="1" (
echo [DRY-RUN] 削除対象ファイルの一覧(実際には削除しません)
) else (
echo [EXECUTE] 削除を実行します
)
set "COUNT=0"
for /r "%ROOTDIR%" %%F in (*.tmp *.bak) do (
set /a "COUNT+=1"
if "%DRY_RUN%"=="1" (
echo [確認] %%F
) else (
del "%%F" >nul 2>&1
echo [削除] %%F
)
)
echo.
echo 対象ファイル数: %COUNT%
if "%DRY_RUN%"=="1" echo 本番実行: %~nx0 --execute
endlocal
@echo off
setlocal enabledelayedexpansion
set "SRC=C:\data"
set "DST=D:\backup"
set "DRY_RUN=1"
if /i "%~1"=="--execute" set "DRY_RUN=0"
set "COPIED=0"
set "SKIPPED=0"
for /r "%SRC%" %%F in (*.csv *.xlsx) do (
REM SRC からの相対パスを DST に再現する
set "REL=%%F"
set "REL=!REL:%SRC%=%DST%!"
if not exist "!REL!" (
if "%DRY_RUN%"=="1" (
echo [コピー予定] %%~nxF
) else (
if not exist "%%~dpF" mkdir "%%~dpF" 2>nul
copy "%%F" "!REL!" >nul
echo [コピー完了] %%~nxF
)
set /a "COPIED+=1"
) else (
set /a "SKIPPED+=1"
)
)
echo コピー: !COPIED! スキップ: !SKIPPED!
endlocal
処理件数・失敗件数をカウントしてログに記録する
再帰処理の結果(成功件数・失敗件数・処理時間)をログファイルに記録しておくと、問題発生時の原因調査や定期実行後の確認に役立ちます。
@echo off
setlocal enabledelayedexpansion
set "ROOTDIR=C:\data"
set "LOGFILE=C:\logs\process_%DATE:~0,4%%DATE:~5,2%%DATE:~8,2%.log"
if not exist "C:\logs\" mkdir "C:\logs"
set "OK=0"
set "NG=0"
echo [%DATE% %TIME%] 処理開始: %ROOTDIR% >> "%LOGFILE%"
for /r "%ROOTDIR%" %%F in (*.csv) do (
REM 処理本体(例: xcopy)
xcopy "%%F" "D:\backup\%%~dpnxF" /y >nul 2>&1
if !ERRORLEVEL! equ 0 (
set /a "OK+=1"
echo [OK] %%~nxF >> "%LOGFILE%"
) else (
set /a "NG+=1"
echo [NG] %%~nxF >> "%LOGFILE%"
)
)
echo [%DATE% %TIME%] 処理完了 OK=%OK% NG=%NG% >> "%LOGFILE%"
echo 処理完了: OK=%OK% NG=%NG% ログ: %LOGFILE%
if %NG% gtr 0 exit /b 1
endlocal
ループブロック内で
%OK% を参照するとブロック開始時の値(0)が返ります。setlocal enabledelayedexpansion を使い、ループ内では !OK! で参照してください。ログ設計の詳細はログを出力する方法完全ガイドも参照してください。再帰コピー・ミラーリングは robocopy を使う
フォルダを丸ごと再帰コピーするなら for /r よりもrobocopy が圧倒的に便利です。除外フォルダ指定・差分コピー・ログ出力を標準サポートしています。
@echo off
setlocal
set "SRC=C:\projects"
set "DST=D:\backup\projects"
set "LOGFILE=C:\logs\robocopy_%DATE:~0,4%%DATE:~5,2%%DATE:~8,2%.log"
REM /e = 空フォルダも含めてサブフォルダを再帰コピー
REM /xd = 除外フォルダ(複数指定可)
REM /xf = 除外ファイルパターン
robocopy "%SRC%" "%DST%" *.* ^
/e ^
/xd ".git" "node_modules" "backup" ^
/xf "*.tmp" "*.bak" ^
/log:"%LOGFILE%" ^
/np /nfl
set RC=%ERRORLEVEL%
REM robocopy: 0=変更なし 1=コピー成功 8以上=エラー
if %RC% geq 8 (
echo [ERROR] robocopy 失敗: RC=%RC%
exit /b %RC%
)
echo [OK] コピー完了: RC=%RC%
endlocal
robocopy の詳細オプションはフォルダをコピーする完全ガイド、ファイル単位のコピーはファイルをコピーする完全ガイドも参照してください。
PowerShell Get-ChildItem -Recurse で複合フィルタ・深度制限を使う
for /r では難しい「深度を2階層に限定する」「複数の拡張子かつサイズ以上のファイルのみ」といった複合フィルタはGet-ChildItem -Recurse と Where-Object で柔軟に実装できます。
@echo off
REM PowerShell で *.log かつ 1MB 以上のファイルを再帰検索
powershell -NoProfile -Command ^
"Get-ChildItem -Path 'C:\data' -Recurse -Filter '*.log' | ^
Where-Object { $_.Length -gt 1MB } | ^
ForEach-Object { $_.FullName }"
@echo off
REM -Depth 2 = 起点から2階層まで(PS5.0以降)
powershell -NoProfile -Command ^
"Get-ChildItem -Path 'C:\projects' -Recurse -Depth 2 -Filter '*.cs' | ^
Where-Object { $_.DirectoryName -notmatch 'obj|bin' } | ^
Select-Object FullName, Length, LastWriteTime | ^
Export-Csv -Path 'C:\logs\files.csv' -NoTypeInformation -Encoding UTF8"
echo [OK] ファイル一覧を CSV に出力しました
@echo off
REM node_modules と .git を除外して *.tmp を再帰削除
powershell -NoProfile -Command ^
"$files = Get-ChildItem -Path 'C:\projects' -Recurse -Filter '*.tmp' | ^
Where-Object { $_.FullName -notmatch 'node_modules|\.git' }; ^
$files | ForEach-Object { ^
Write-Host ('削除: ' + $_.FullName); ^
Remove-Item $_.FullName -Force ^
}; ^
Write-Host ('完了: ' + $files.Count + ' 件削除')"
Get-ChildItem -Depth N は PowerShell 5.0(Windows 10 以降に標準搭載)で使えます。PowerShell 4.0 以前の環境では -Depth が使えないため、Where-Object { ($_.FullName -split '\\\\').Count -le (起点深度 + N) }で代替してください。実践パターン:再帰処理の完全構成
@echo off
setlocal enabledelayedexpansion
set "LOGDIR=C:\app\logs"
set "KEEP_DAYS=30"
set "DRY_RUN=1"
if /i "%~1"=="--execute" set "DRY_RUN=0"
set "DEL_COUNT=0"
set "DEL_SIZE=0"
echo [%DATE% %TIME%] ログ整理開始 (KEEP=%KEEP_DAYS%日 DRY_RUN=%DRY_RUN%)
for /r "%LOGDIR%" %%F in (*.log) do (
REM %KEEP_DAYS% 日以前のファイルかチェック
forfiles /p "%%~dpF" /m "%%~nxF" /d -%KEEP_DAYS% >nul 2>&1
if !ERRORLEVEL! equ 0 (
set /a "DEL_SIZE+=%%~zF"
set /a "DEL_COUNT+=1"
if "%DRY_RUN%"=="1" (
echo [確認] %%~nxF (%%~zF bytes, %%~tF)
) else (
del "%%F" >nul 2>&1
echo [削除] %%~nxF (%%~zF bytes)
)
)
)
echo 対象: %DEL_COUNT% 件 合計サイズ: %DEL_SIZE% bytes
if "%DRY_RUN%"=="1" echo 本番実行: %~nx0 --execute
endlocal
期間指定での古いファイル削除は期間以前に更新されたファイルを自動削除する完全ガイド、ファイル削除の安全なパターンはファイルを削除する完全ガイドも参照してください。
まとめ
- for /r “起点” %%F in (パターン): ファイル単位の任意処理に最も柔軟。変数展開(%%~nxF・%%~dpF・%%~zF)を使いこなすのがポイント
- 複数拡張子フィルタ:
in(*.csv *.tsv *.txt)でスペース区切りに並べる - for /r /d: フォルダのみを再帰列挙。各フォルダに処理を適用する用途に
- 除外フォルダ: ループ内で
%%~dpFをfindstrにかけて特定フォルダを除外 - ドライランモード: 引数
--executeなしは確認のみ、ありで本番実行。大量削除・移動の前には必須 - 成否カウント + ログ: enabledelayedexpansion で
!OK!/!NG!をカウントして記録 - robocopy /e /xd: 再帰コピーは robocopy が便利。除外フォルダ・差分・ログを標準サポート
- PowerShell Get-ChildItem -Recurse: 深度制限・複合フィルタ・サイズ条件など for /r では難しい条件に対応
関連記事: 複数フォルダをループして一括処理する完全ガイド / フォルダ内ファイル一覧を取得する完全ガイド / ワイルドカードでファイルを移動する完全ガイド
よくある質問(FAQ)
for /r でパスにスペースが含まれるファイルが正しく処理されません。%%F には既にフルパスが入っているため、"%%F" のようにダブルクォートで囲んで使うのが基本です。copy %%F D:\backup\ ではスペースで分断されます。copy "%%F" "D:\backup\" と書いてください。起点フォルダのパスにもスペースが含まれる場合はfor /r "%ROOTDIR%" %%F in (*) の %ROOTDIR% もクォートが必要です。for /r で処理したいのに、.git や node_modules の中のファイルも処理されてしまいます。for /r に除外フォルダを直接指定する方法はありません。ループ内で echo %%~dpF | findstr /i /c:"\node_modules\" でパスを検査し、マッチした場合は goto :CONTINUE でスキップする方法が一般的です。再帰コピーなら robocopy /xd node_modules .git の方が除外指定が簡潔です。for ブロック内で %COUNT% を参照すると、ブロックのパース時(実行前)に展開されるため常に最初の値になります。setlocal enabledelayedexpansion を追加して、ループ内では !COUNT!(感嘆符)で参照してください。set /a "COUNT+=1" の加算自体は動作しますが、ループ内で参照・比較する場合は遅延展開が必須です。forfiles /d -N による日付判定はドライランと本番実行の間に時間が経過すると結果が変わる可能性があります(日付がまたがった場合など)。また、削除後に for /r が空フォルダを処理しようとしてエラーになるケースもあります。カウントの不一致が問題になる場合は、ドライランで出力したリストをファイルに保存し、そのリストを読み込んで削除する2段階方式を検討してください。for /r でシンボリックリンクやジャンクションがあると無限ループになりますか?for /r はシンボリックリンクやジャンクションをフォルダとして認識し、その先まで再帰的に処理します。循環参照があると無限ループになる危険性があります。対策として robocopy は /sl オプションでシンボリックリンク自体をコピーし、リンク先には入りません。PowerShell では Get-ChildItem -Attributes !ReparsePoint でシンボリックリンクを除外できます。

