【テクニカル・上級編】【ディレクトリ整理】ファイルが存在しない「空フォルダ」を検出して一括再帰削除するメンテナンスツール – VBScript (Visual Basic Scripting Edition)解析バイブル

スポンサーリンク

【ディレクトリ整理】孤立した空フォルダを駆逐せよ:FSO再帰走査による極限のクリーンアップ・エンジンの構築

レガシーシステムの裏側で、バッチ処理やファイル連携が吐き出し続ける無数のゴミファイル。それらを処理した後に残されるのが、中身の無い「空のディレクトリ」だ。
「たかが空フォルダくらい、放っておいても実害はない」などと考えているとしたら、それはエンジニアとしての致命的な感覚麻痺である。

Windowsのファイルシステム(NTFS)において、フォルダ(ディレクトリ)もまた一種のメタデータレコードである。数万、数百万の空フォルダが放置されたファイルシステムは、MFT(Master File Table)の断片化を加速させ、ディレクトリトラバーサル時のI/Oパフォーマンスを確実に劣化させる。さらに、古いバックアップツールや監視エージェントが、この無意味な空フォルダの群れに阻まれてタイムアウトを起こすという悲劇も後を絶たない。

我々は今、VBScript(Visual Basic Scripting Edition)のプリミティブなパワーを極限まで引き出し、ファイルシステムを深部から浄化する「完全自律型・空フォルダ再帰削除エンジン」をここに実装する。

—

FSOの深層:なぜ「上から下へ」の走査では空フォルダが消せないのか?

多くの初学者が陥る罠がこれだ。ルートディレクトリから順方向(トップダウン)にフォルダを走査し、ファイル数やサブフォルダ数をカウントして削除しようとする。

このアプローチは完全に破綻している。

例えば、以下のようなツリー構造を想定してほしい。
`Root / SubA / SubB / (空)`

トップダウンで `Root` を評価した時点では、`SubA` の中には `SubB` というフォルダが存在するため、「空ではない」と判定される。次に `SubA` を評価しても同様だ。結果として、最下層の `SubB` を消し忘れたまま処理が終了する。

解決策:底からの逆流(ボトムアップ・レカージョン)

空フォルダを確実に、かつ一網打尽に刈り取る唯一のアルゴリズムは、「最下層のリーフ(子を持たないフォルダ)から根に向かって遡る(ボトムアップ)」ことである。
再帰呼び出しの「帰りがけ(Post-order traversal)」のタイミングで削除判定を行う。これこそが、ファイルシステム操作における数理的真理である。

—

実装:極限まで最適化されたVBScriptコード

以下のコードは、エラーハンドリング、オブジェクトのライフサイクル管理、そしてメモリリークの完全排除を考慮して設計されたプロダクション品質のスクリプトである。

‘ ==============================================================================
‘ Script Name: PurgeEmptyDirectories.vbs
‘ Description: 指定されたルートパス以下の空フォルダをボトムアップで再帰的に完全消去する
‘ Author: Chief Architect (Legacy Systems Maintenance)
‘ ==============================================================================

Option Explicit

‘ 実行パラメータの定義
Const TARGET_ROOT = “C:\DataWork\TempProcessing” ‘ ターゲットのルートパス
Const LOG_MODE = True コンソール出力フラグ

Dim fso
Set fso = CreateObject(“Scripting.FileSystemObject”)

‘ 開始時刻の計測
Dim startTime
startTime = Timer

Call LogMessage(“=== 空フォルダ一貫クリーンアップ・エンジン起動 ===”)
Call LogMessage(“対象ルート: ” & TARGET_ROOT)

If fso.FolderExists(TARGET_ROOT) Then
Dim deletedCount
deletedCount = 0

‘ 再帰的パージの実行(ルート自体は削除対象外とするため、サブフォルダ群を渡す)
Dim rootFolder
Set rootFolder = fso.GetFolder(TARGET_ROOT)

‘ ボトムアップ処理の本体呼び出し
deletedCount = PurgeRecursive(rootFolder)

Set rootFolder = Nothing
Call LogMessage(“=== 処理完了 ===”)
Call LogMessage(“削除された空フォルダ総数: ” & deletedCount & ” 個”)
Call LogMessage(“処理時間: ” & FormatNumber(Timer – startTime, 3) & ” 秒”)
Else
Call LogMessage(“[ERROR] 指定されたルートディレクトリが存在しません: ” & TARGET_ROOT)
End If

‘ オブジェクトの明示的破棄(VBScriptにおけるGCの不完全性を補う必須作法)
Set fso = Nothing

WScript.Quit(0)

‘ ==============================================================================
‘ 関数名: PurgeRecursive
‘ 概要: フォルダを再帰的に走査し、帰りがけの駄賃として空フォルダを削除する
‘ 引数: objFolder (Folder Object)
‘ 戻り値: Long (削除したフォルダの累積数)
‘ ==============================================================================
Function PurgeRecursive(objFolder)
Dim subFolder, count
count = 0

‘ 【重要】VBScriptのFor Each内での削除はコレクションの整合性を破壊するため、
‘ サブフォルダを一度配列または別ループで安全に処理するか、
‘ 「帰りがけ」に評価する構造にする必要がある。

Dim subFoldersColl
Set subFoldersColl = objFolder.SubFolders

‘ 子フォルダが存在する場合は、さらに深層へ潜る(再帰)
If subFoldersColl.Count > 0 Then
‘ コレクションを直接イテレートすると削除時にインデックスがズレるため、
‘ 一度配列に退避させるか、ボトムアップの特性を利用して安全に再帰させる。
‘ ここではVBScriptの仕様上、オブジェクト参照を維持したまま各要素を評価する。

Dim colItems()
Dim i
i = 0

‘ 配列への動的リサイズ(パフォーマンスとメモリのトレードオフを考慮)
ReDim colItems(subFoldersColl.Count – 1)
For Each subFolder In subFoldersColl
Set colItems(i) = subFolder
i = i + 1
Next

‘ 各サブフォルダに対して再帰呼び出し
For i = 0 To UBound(colItems)
count = count + PurgeRecursive(colItems(i))
Set colItems(i) = Nothing
Next
End If

‘ — ここからが「帰りがけ(Post-order)」の処理 —
‘ 子フォルダの再帰処理が終わったので、現在のフォルダの状態を再評価する。
‘ ※親フォルダを削除判定する前に、子フォルダが削除されて空になっている可能性があるため。

Dim currentSubCount, currentFileCount
currentSubCount = objFolder.SubFolders.Count
currentFileCount = objFolder.Files.Count

‘ ファイルもサブフォルダも存在しない場合、それは「真の空フォルダ」である
If currentSubCount = 0 And currentFileCount = 0 Then
‘ ルートディレクトリ自体は削除してはならないため、安全装置をかける
If StrComp(objFolder.Path, TARGET_ROOT, vbTextCompare) <> 0 Then
On Error Resume Next
objFolder.Delete True ‘ 強制削除フラグ(読取専用属性等でコケるのを防ぐ)

If Err.Number = 0 Then
Call LogMessage(“[PURGED] 空フォルダを削除しました: ” & objFolder.Path)
count = count + 1
Else
Call LogMessage(“[WARN] 削除失敗(アクセス権限またはロック中): ” & objFolder.Path & ” (Error: ” & Hex(Err.Number) & “)”)
Err.Clear
End If
On Error GoTo 0
End If
End If

Set subFoldersColl = Nothing
PurgeRecursive = count
End Function

‘ ==============================================================================
‘ ユーティリティ: ログ出力
‘ ==============================================================================
Sub LogMessage(msg)
If LOG_MODE Then
WScript.Echo “[” & Now & “] ” & msg
End If
End Sub

—

アーキテクチャの急所:シニアエンジニアが押さえるべき3つのポイント

1. VBScriptにおけるコレクション・イテレーションの罠

通常の言語であれば、ループ中で要素を削除しても安全にハンドリングできる場合があるが、`Scripting.FileSystemObject` の `SubFolders` や `Files` コレクションは、イテレーション中に実体が削除されると、内部ポインタが破損し、実行時エラーや未定義動作を引き起こす。
今回のコードでは、一度VBScriptの動的配列 `colItems()` にオブジェクト参照を退避させ、インデックスベースで安全に再帰処理を行っている。この一手間を惜しむ者が、現場で「Automation error」の魔物に足元をすすられるのだ。

2. 例外の局所化(`On Error Resume Next` の正しい使い方)

レガシーなシステムにおいて、共有フォルダや一時ファイル領域には「SYSTEM権限や別プロセスが握っていてアクセス拒否(Error 70: Permission Denied)」を返すフォルダが確実に存在する。
ここでスクリプト全体を停止させてはメンテナンスツールとして失格である。
削除処理の部分だけに厳密に `On Error Resume Next` を閉じ込め、エラー番号を精査してログに記録しつつ、処理を止めずに次のフォルダへ進む設計(Fail-Safe)を徹底している。

3. メモリ管理とCOMオブジェクトの解放

VBScriptの背後で動いているFSOやFolderオブジェクトは、COMコンポーネントである。スクリプト終了時に自動解放されるからといって、再帰ループの内部で参照を放置すると、メモリ消費量が肥大化し、最悪の場合はIISやタスクスケジューラのプロセス空間を圧迫する。
コード内で適宜 `Set xxx = Nothing` を明示的に記述しているのは、メモリリークを根絶するためのプロフェッショナルとしての流儀である。

—

運用への組み込み:タスクスケジューラと完全自動化

このスクリプト(`PurgeEmptyDirectories.vbs`)を、日次あるいは週次のシステムメンテナンスバッチに組み込むことで、ファイルサーバやローカルストレージの健康状態を常に最高峰に維持することが可能になる。

@ECHO OFF
:: =====================================================================
:: Batch Wrapper: メンテナンス実行ラッパー
:: =====================================================================
CHCP 65001 > NUL
SET SCRIPT_PATH=”C:\Scripts\PurgeEmptyDirectories.vbs”

ECHO システムクリーンアップを開始します…
CSCRIPT //nologo %SCRIPT_PATH%

ECHO メンテナンスが終了しました。
PAUSE

GUIのIDEや過剰なフレームワークに頼らず、OSのネイティブ環境だけで完結するこのVBScriptソリューションこそ、過酷なエンタープライズの現場を陰で支え続ける「真の自動化技術」である。
無駄を削ぎ落としたコードの切れ味を、ぜひ自身の管理する環境で体感してほしい。

タイトルとURLをコピーしました