「ビルド後に残った空フォルダを消したい」「プロジェクト整理後に散らばった空ディレクトリを一掃したい」——バッチファイルで空フォルダを自動削除する場面は実務でよく登場します。本記事では基本の rd + dir 構文から、ネストした空フォルダを確実に消す多重パス方式、ドライラン(削除前確認)、除外フォルダ対応、削除ログ記録まで体系的に解説します。
- 指定フォルダ以下の空フォルダを再帰的に一括削除する基本スクリプト
- ネストした空フォルダ(空フォルダの中に空フォルダ)を確実に削除する複数パス方式
- 削除前に対象フォルダ一覧を表示するドライラン(確認モード)
- 特定のフォルダ名・パターンを除外して削除する方法
- 隠しファイル入りフォルダが削除されない原因と対処法
- 削除件数・削除ログを記録して安全に運用するパターン
rd コマンドによる削除はゴミ箱を経由しません。削除後の復元は基本的にできないため、初めて実行するスクリプトは必ず後述のドライラン(確認モード)で対象を確認してから本実行してください。
空フォルダ削除の方法比較
| 方法 | 対象範囲 | ネスト対応 | 特徴 |
|---|---|---|---|
for /f + dir + rd(1パス) |
全階層(再帰) | △ 1階層分のみ確実 | シンプル・基本形 |
for /f + dir + rd(複数パス) |
全階層(再帰) | ◎ 完全対応 | 実務推奨・確実性高い |
for /d + rd |
直下のみ | × 非対応 | 最もシンプル |
| PowerShell 連携 | 全階層(再帰) | ◎ 完全対応 | 柔軟・Windows 10/11 推奨 |
方法1: for /f + dir + rd(基本・1パス)
dir /ad /s /b でサブフォルダ一覧を取得し、rd(/s なし)で空フォルダだけを削除します。rd は中身があるフォルダをスキップするため、空でないフォルダは自動的に無視されます。
@echo off
setlocal
set ROOT=C:workproject
rem 変数が空でないことを確認(安全チェック)
if "%ROOT%"=="" (
echo [ERROR] ROOT が空です。処理を中止します
exit /b 1
)
rem サブフォルダ一覧を取得し、空フォルダだけ削除
rem rd(/s なし)は空でないフォルダをスキップする
for /f "delims=" %%D in ('dir "%ROOT%" /ad /s /b 2^>nul') do (
rd "%%D" >nul 2>&1
)
echo 完了
endlocal
dir /ad /s /b の出力順は親フォルダが子フォルダより先に来るため、親を先に rd しようとしても子フォルダが残っていてスキップされます。その後に子フォルダが空なので削除されますが、親フォルダは再度チェックされません。
「空フォルダの中に空フォルダ」が入れ子になっている場合は、後述の複数パス方式を使ってください。
方法2: 複数パスで確実に削除(実務推奨)
「削除できたフォルダがあれば繰り返す」というループを使い、ネストした空フォルダを完全に削除します。
@echo off
setlocal enabledelayedexpansion
set ROOT=C:workproject
if "%ROOT%"=="" (
echo [ERROR] ROOT が空です。処理を中止します
exit /b 1
)
set TOTAL_DELETED=0
:loop
set PASS_DELETED=0
for /f "delims=" %%D in ('dir "%ROOT%" /ad /s /b 2^>nul') do (
rd "%%D" >nul 2>&1
if not errorlevel 1 (
echo 削除: %%D
set /a PASS_DELETED+=1
set /a TOTAL_DELETED+=1
)
)
rem このパスで1件以上削除できた場合は再度ループ
if !PASS_DELETED! GTR 0 goto :loop
echo.
echo 完了: 合計 !TOTAL_DELETED! 個の空フォルダを削除しました
endlocal
1パス目: 最も深い階層の空フォルダが削除される
2パス目: 子が消えて空になった親フォルダが削除される
3パス目: さらに上の空フォルダが削除される……
削除件数がゼロになるまで繰り返すため、何階層でも確実に処理できます。
方法3: for /d で直下サブフォルダだけ削除
再帰不要・直下の空フォルダだけ削除すれば十分な場合のシンプルな方法です。
@echo off
setlocal
set ROOT=C:workproject
rem カレント直下のサブフォルダをループして空なら削除
for /d %%D in ("%ROOT%*") do (
rd "%%D" >nul 2>&1
if not errorlevel 1 (
echo 削除: %%~nxD
)
)
endlocal
ドライラン: 削除前に対象フォルダを確認する
実際に削除する前に対象フォルダの一覧を確認するモードです。初めてスクリプトを実行する前に必ず確認しましょう。
@echo off
setlocal enabledelayedexpansion
set ROOT=C:workproject
echo === ドライラン: 以下の空フォルダが削除対象です ===
set COUNT=0
for /f "delims=" %%D in ('dir "%ROOT%" /ad /s /b 2^>nul') do (
rem rd の代わりに dir で中身を確認し、空なら対象として表示
dir /b /a "%%D" 2>nul | findstr "." >nul
if errorlevel 1 (
echo [空] %%D
set /a COUNT+=1
)
)
if %COUNT%==0 (
echo 削除対象の空フォルダはありません
) else (
echo.
echo 対象: %COUNT% フォルダ
echo.
set /p CONFIRM=上記フォルダを削除しますか? (Y/N):
if /i "!CONFIRM!"=="Y" (
rem 実削除は複数パス方式で実行
:del_loop
set PASS=0
for /f "delims=" %%%%D in ('dir "%ROOT%" /ad /s /b 2^>nul') do (
rd "%%%%D" >nul 2>&1
if not errorlevel 1 set PASS=1
)
if "!PASS!"=="1" goto :del_loop
echo 削除完了
) else (
echo キャンセルしました
)
)
endlocal
特定のフォルダを除外して削除する
フォルダ名で除外(完全一致)
@echo off
setlocal enabledelayedexpansion
set ROOT=C:workproject
rem ".git" と "node_modules" フォルダを除外して空フォルダを削除
:loop
set PASS_DELETED=0
for /f "delims=" %%D in ('dir "%ROOT%" /ad /s /b 2^>nul') do (
set DNAME=%%~nxD
rem 除外フォルダ名をチェック
if /i "!DNAME!"==".git" goto :skip_%%~nD
if /i "!DNAME!"=="node_modules" goto :skip_%%~nD
if /i "!DNAME!"==".vs" goto :skip_%%~nD
rd "%%D" >nul 2>&1
if not errorlevel 1 (
echo 削除: %%D
set /a PASS_DELETED+=1
)
goto :next_%%~nD
:skip_%%~nD
echo 除外: %%D
:next_%%~nD
)
if !PASS_DELETED! GTR 0 goto :loop
echo 完了
endlocal
フォルダ名のパターンで除外(findstr 使用)
@echo off
setlocal enabledelayedexpansion
set ROOT=C:workproject
:loop
set PASS_DELETED=0
for /f "delims=" %%D in ('dir "%ROOT%" /ad /s /b 2^>nul') do (
rem フルパスに "keep" または ".git" が含まれる場合は除外
echo %%D | findstr /i "keep .git" >nul
if not errorlevel 1 (
echo 除外: %%~nxD
) else (
rd "%%D" >nul 2>&1
if not errorlevel 1 (
echo 削除: %%D
set /a PASS_DELETED+=1
)
)
)
if !PASS_DELETED! GTR 0 goto :loop
echo 完了
endlocal
削除件数と削除ログを記録する
@echo off
setlocal enabledelayedexpansion
set ROOT=C:workproject
set LOGFILE=%~dp0empty_folder_delete.log
(
echo ===========================
echo 実行日時: %DATE% %TIME%
echo 対象: %ROOT%
echo ===========================
) >> "%LOGFILE%"
set TOTAL=0
:loop
set PASS=0
for /f "delims=" %%D in ('dir "%ROOT%" /ad /s /b 2^>nul') do (
rd "%%D" >nul 2>&1
if not errorlevel 1 (
echo 削除: %%D
echo 削除: %%D >> "%LOGFILE%"
set /a TOTAL+=1
set PASS=1
)
)
if "!PASS!"=="1" goto :loop
echo 合計 !TOTAL! 件削除 >> "%LOGFILE%"
echo. >> "%LOGFILE%"
echo.
echo 完了: !TOTAL! 個の空フォルダを削除しました
echo ログ: %LOGFILE%
endlocal
実践例A: ビルド成果物フォルダのクリーンアップ
C++ / Java / .NET などのビルド後に残る空ディレクトリを一掃する例です。
@echo off
setlocal enabledelayedexpansion
set BUILD_ROOT=C:workuild
echo [BUILD CLEAN] 空フォルダを削除します: %BUILD_ROOT%
rem obj・bin フォルダ配下の空フォルダを削除
for %%BASE in (obj bin tmp intermediate) do (
set SUBDIR=%BUILD_ROOT%\%%BASE
if exist "!SUBDIR!" (
:clean_loop_%%BASE
set PASS=0
for /f "delims=" %%D in ('dir "!SUBDIR!" /ad /s /b 2^>nul') do (
rd "%%D" >nul 2>&1
if not errorlevel 1 set PASS=1
)
if "!PASS!"=="1" goto :clean_loop_%%BASE
echo [OK] %%BASE 配下の空フォルダを削除しました
)
)
echo [BUILD CLEAN] 完了
endlocal
実践例B: プロジェクト整理後の全体クリーンアップ(タスクスケジューラ対応)
@echo off
setlocal enabledelayedexpansion
set ROOT=C:workproject
set LOGFILE=C:worklogscleanup_%DATE:~0,4%%DATE:~5,2%%DATE:~8,2%.log
rem ログフォルダがなければ作成
if not exist "C:worklogs" mkdir "C:worklogs"
echo [%DATE% %TIME%] クリーンアップ開始: %ROOT% > "%LOGFILE%"
set TOTAL=0
:main_loop
set PASS=0
for /f "delims=" %%D in ('dir "%ROOT%" /ad /s /b 2^>nul') do (
rd "%%D" >nul 2>&1
if not errorlevel 1 (
echo %%D >> "%LOGFILE%"
set /a TOTAL+=1
set PASS=1
)
)
if "!PASS!"=="1" goto :main_loop
echo [%DATE% %TIME%] 完了: !TOTAL! 件削除 >> "%LOGFILE%"
echo 完了: !TOTAL! 件 ログ: %LOGFILE%
endlocal
よくある落とし穴
落とし穴1: 隠しファイル・Thumbs.db 入りフォルダが削除されない
rem rd は「ファイルやサブフォルダが1つでも存在する」フォルダをスキップする
rem Thumbs.db や desktop.ini など隠しファイルがあると削除できない
rem 対処: 先に隠しファイルを削除してから rd する
for /f "delims=" %%D in ('dir "%ROOT%" /ad /s /b 2^>nul') do (
rem Thumbs.db と desktop.ini を先に削除
del /F /A:H /Q "%%DThumbs.db" >nul 2>&1
del /F /A:H /Q "%%Ddesktop.ini" >nul 2>&1
rem 空になっていれば削除
rd "%%D" >nul 2>&1
)
落とし穴2: アクセス拒否でシステムフォルダが削除できない
rem システムが保護しているフォルダ(System Volume Information など)は rem 管理者権限でも rd できない場合がある(ERRORLEVEL=1 で無視される) rem 対処: エラーを無視して継続する(2>&1 >nul で非表示) rd "%%D" >nul 2>&1 rem → アクセス拒否のフォルダは自動スキップされるため問題なし
落とし穴3: 1パスのみではネスト空フォルダを削除できない
setlocal enabledelayedexpansion
rem 問題の構造例:
rem C:workprojecta ← 空フォルダだが子が先にリスト
rem C:workprojectasub ← 空フォルダ(先に削除される)
rem
rem 1パス目の dir 出力順(深さ優先):
rem 1: C:workprojecta → rd しようとするが sub が残っているので失敗
rem 2: C:workprojectasub → rd 成功(空)
rem → a は削除されずに残る
rem
rem 解決策: 複数パスで繰り返す
:loop
set PASS=0
for /f "delims=" %%D in ('dir "%ROOT%" /ad /s /b 2^>nul') do (
rd "%%D" >nul 2>&1
if not errorlevel 1 set PASS=1
)
if "!PASS!"=="1" goto :loop
落とし穴4: ROOT 変数が空だとカレントフォルダや意図しない上位を操作してしまう
rem 危険な例: ROOT が空だった場合
set ROOT=
for /f "delims=" %%D in ('dir "%ROOT%" /ad /s /b 2^>nul') do (
rd "%%D" >nul 2>&1
)
rem → %ROOT% が空なので dir "" → カレントディレクトリを対象に処理してしまう
rem 必ず変数の空チェックを行う
if "%ROOT%"=="" (
echo [ERROR] ROOT が空です。処理を中止します
exit /b 1
)
落とし穴5: del /S /Q でファイルを消しても空フォルダは残る
setlocal enabledelayedexpansion
rem del /S /Q でファイルを削除してもフォルダ構造は残る
del /S /Q "C:workproject*.*"
rem → フォルダは残ったまま(空フォルダが大量に残る)
rem ファイル削除後に空フォルダも削除するには rd ループを追加
del /S /Q "C:workproject*.*"
:remove_empty
set PASS=0
for /f "delims=" %%D in ('dir "C:workproject" /ad /s /b 2^>nul') do (
rd "%%D" >nul 2>&1
if not errorlevel 1 set PASS=1
)
if "!PASS!"=="1" goto :remove_empty
よくある質問(FAQ)
rd を実行する代わりに、中身が空かどうかを確認して表示します。
@echo off
setlocal
set ROOT=C:workproject
set COUNT=0
echo === 空フォルダ一覧 ===
for /f "delims=" %%D in ('dir "%ROOT%" /ad /s /b 2^>nul') do (
rem dir /b /a でフォルダ内の全アイテム(隠しファイル含む)を確認
dir /b /a "%%D" 2>nul | findstr "." >nul
if errorlevel 1 (
echo %%D
set /a COUNT+=1
)
)
echo =====================
echo 合計: %COUNT% フォルダ
endlocal
「サブフォルダはあるが全てのサブフォルダが空」という構造は、複数パス方式を使えば自動的に処理されます。深い階層から順に削除されていくため、最終的に親フォルダも空になり削除されます。
setlocal enabledelayedexpansion
rem 複数パス方式を使えば入れ子の空フォルダ構造も完全処理
:loop
set PASS=0
for /f "delims=" %%D in ('dir "%ROOT%" /ad /s /b 2^>nul') do (
rd "%%D" >nul 2>&1
if not errorlevel 1 set PASS=1
)
if "!PASS!"=="1" goto :loop
rem → 最終的に全ての空フォルダ(入れ子も含む)が削除される
複数パス方式の rd は ROOT フォルダ自体には実行しません(dir "%ROOT%" /ad /s /b は ROOT の サブフォルダ のみ列挙します)。ROOT フォルダ自体は削除されません。
rem dir "%ROOT%" /ad /s /b は ROOT の子・孫フォルダを列挙
rem ROOT 自体はリストに含まれないので削除されない
rem → ROOT フォルダが残るのは仕様どおりの動作です
set ROOT=C:workproject
for /f "delims=" %%D in ('dir "%ROOT%" /ad /s /b 2^>nul') do (
rd "%%D" >nul 2>&1
)
rem C:workproject 自体は削除されず、その中の空サブフォルダだけ削除される
findstr でパスをフィルタリングして除外します。通常 .git や node_modules は空でないため rd に失敗しますが、万が一のために除外設定を入れておくのが安全です。
for /f "delims=" %%D in ('dir "%ROOT%" /ad /s /b 2^>nul') do (
echo %%D | findstr /i ".git
ode_modules" >nul
if not errorlevel 1 (
echo [除外] %%~nxD
) else (
rd "%%D" >nul 2>&1
if not errorlevel 1 echo [削除] %%D
)
)
削除スクリプトをファイルに保存してタスクスケジューラに登録します。
rem 毎週日曜日の深夜3時に実行するタスクを登録(管理者権限で実行) schtasks /create /tn "WeeklyEmptyFolderClean" ^ /tr "cmd /c "C:atchdelete_empty_folders.bat"" ^ /sc weekly /d SUN /st 03:00 /ru "%USERNAME%" /f rem 登録確認 schtasks /query /tn "WeeklyEmptyFolderClean" /fo list rem タスク削除 schtasks /delete /tn "WeeklyEmptyFolderClean" /f
まとめ
| 目的 | 推奨方法 |
|---|---|
| 再帰的に空フォルダ削除(基本) | for /f + dir /ad /s /b + rd(1パス) |
| ネスト空フォルダを確実に削除 | 複数パス方式(PASS_DELETED が 0 になるまでループ) |
| 直下の空フォルダのみ削除 | for /d + rd |
| 削除前に対象確認(ドライラン) | dir /b /a + findstr で空フォルダ一覧表示 |
| 特定フォルダ名を除外 | findstr /i でパスをフィルタリング |
| 隠しファイル入りフォルダも削除 | del /F /A:H で隠しファイルを先に削除してから rd |
| 削除ログを記録 | 削除時に >> logfile で追記 |
| 安全な実行 | 変数空チェック(if "%ROOT%"=="")必須 |
フォルダ削除の基本コマンドについては ファイルを削除する方法完全ガイド を、フォルダのループ処理については 複数フォルダをループして一括処理する方法 も合わせて参照してください。