【bat】バッチファイルでフォルダを再帰的に処理する完全ガイド|for /r・xcopy・robocopy・除外フォルダ・ドライラン・ログ記録・PowerShellまで

bat

「サブフォルダも含めて全ファイルに処理を適用したい」「特定の拡張子だけを再帰的に検索してコピーしたい」——このような要件はバッチファイルで頻繁に登場します。

Windowsには for /rxcopy /srobocopy /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 の構文で、指定フォルダ以下のすべてのサブフォルダを含むファイルに繰り返し処理を適用できます。ループ変数に付く修飾子(チルダ展開)でパス・ファイル名・拡張子・サイズを個別に取得できます。

for /r の基本構文と変数展開チートシート
@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 /rin(...) に複数のパターンをスペース区切りで並べると、複数の拡張子を同時にフィルタリングできます。

複数の拡張子(*.csv *.tsv *.txt)を同時に再帰処理する
@echo off
setlocal

set "ROOTDIR=C:\data"

REM in(...) にスペース区切りで複数パターンを指定
for /r "%ROOTDIR%" %%F in (*.csv *.tsv *.txt) do (
    echo 対象: %%F
)

endlocal
拡張子ごとに処理を分岐する(%%~xF で判定)
@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 を使うとファイルではなくサブフォルダのみを再帰的に列挙できます。フォルダ構造の確認や、各サブフォルダに対して処理を適用する用途に使います。

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 による除外判定が必要です。.gitnode_modulesbackup などのフォルダを除外する実践パターンを紹介します。

特定のフォルダ名を含むパスを除外して再帰処理する
@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
robocopy の /xd で除外するほうが確実な場合がある
コピー・ミラーリングのユースケースでは for /r よりrobocopy /xd backup /xd .git のほうが除外指定が直感的で確実です。詳細はフォルダをコピーする完全ガイドを参照してください。

ドライランモードで実行前に対象ファイルを確認する

大量ファイルの削除・移動などの不可逆な処理は、実行前に対象を確認するドライランモードを実装するのが安全です。引数 --dry-run の有無で動作を切り替えます。

–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
ループ内のカウンタには enabledelayedexpansion が必須
ループブロック内で %OK% を参照するとブロック開始時の値(0)が返ります。setlocal enabledelayedexpansion を使い、ループ内では !OK! で参照してください。ログ設計の詳細はログを出力する方法完全ガイドも参照してください。

再帰コピー・ミラーリングは robocopy を使う

フォルダを丸ごと再帰コピーするなら for /r よりもrobocopy が圧倒的に便利です。除外フォルダ指定・差分コピー・ログ出力を標準サポートしています。

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 -RecurseWhere-Object で柔軟に実装できます。

Get-ChildItem -Recurse で特定拡張子・サイズ以上のファイルを再帰検索する
@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 }"
Get-ChildItem で深度を制限して再帰処理する(-Depth オプション)
@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 に出力しました
Get-ChildItem で特定フォルダを除外して再帰削除する(確認付き)
@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 + ' 件削除')"
-Depth は PowerShell 5.0 以降
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: フォルダのみを再帰列挙。各フォルダに処理を適用する用途に
  • 除外フォルダ: ループ内で %%~dpFfindstr にかけて特定フォルダを除外
  • ドライランモード: 引数 --execute なしは確認のみ、ありで本番実行。大量削除・移動の前には必須
  • 成否カウント + ログ: enabledelayedexpansion で !OK! / !NG! をカウントして記録
  • robocopy /e /xd: 再帰コピーは robocopy が便利。除外フォルダ・差分・ログを標準サポート
  • PowerShell Get-ChildItem -Recurse: 深度制限・複合フィルタ・サイズ条件など for /r では難しい条件に対応

関連記事: 複数フォルダをループして一括処理する完全ガイド / フォルダ内ファイル一覧を取得する完全ガイド / ワイルドカードでファイルを移動する完全ガイド

よくある質問(FAQ)

Qfor /r でパスにスペースが含まれるファイルが正しく処理されません。
Aループ変数 %%F には既にフルパスが入っているため、"%%F" のようにダブルクォートで囲んで使うのが基本です。copy %%F D:\backup\ ではスペースで分断されます。copy "%%F" "D:\backup\" と書いてください。起点フォルダのパスにもスペースが含まれる場合はfor /r "%ROOTDIR%" %%F in (*)%ROOTDIR% もクォートが必要です。
Qfor /r で処理したいのに、.gitnode_modules の中のファイルも処理されてしまいます。
Afor /r に除外フォルダを直接指定する方法はありません。ループ内で echo %%~dpF | findstr /i /c:"\node_modules\" でパスを検査し、マッチした場合は goto :CONTINUE でスキップする方法が一般的です。再帰コピーなら robocopy /xd node_modules .git の方が除外指定が簡潔です。
Qループ内のカウンタ変数が常に 0 のまま終わります。
Afor ブロック内で %COUNT% を参照すると、ブロックのパース時(実行前)に展開されるため常に最初の値になります。setlocal enabledelayedexpansion を追加して、ループ内では !COUNT!(感嘆符)で参照してください。set /a "COUNT+=1" の加算自体は動作しますが、ループ内で参照・比較する場合は遅延展開が必須です。
Qドライランで表示された件数と本番実行後の実際の削除件数が一致しません。
Aforfiles /d -N による日付判定はドライランと本番実行の間に時間が経過すると結果が変わる可能性があります(日付がまたがった場合など)。また、削除後に for /r が空フォルダを処理しようとしてエラーになるケースもあります。カウントの不一致が問題になる場合は、ドライランで出力したリストをファイルに保存し、そのリストを読み込んで削除する2段階方式を検討してください。
Qfor /r でシンボリックリンクやジャンクションがあると無限ループになりますか?
AWindows の for /r はシンボリックリンクやジャンクションをフォルダとして認識し、その先まで再帰的に処理します。循環参照があると無限ループになる危険性があります。対策として robocopy/sl オプションでシンボリックリンク自体をコピーし、リンク先には入りません。PowerShell では Get-ChildItem -Attributes !ReparsePoint でシンボリックリンクを除外できます。