【入門編】【上級プロフェッショナル向け】NTFSジャンクションやシンボリックリンクによるFSO再帰走査の無限ループを検出・破棄する階層レベル制限アルゴリズム – VBScript (Visual Basic Scripting Edition)解析バイブル

スポンサーリンク
スポンサーリンク

迷宮(無限ループ)を突破せよ:FSO再帰探索を「プロフェッショナル」の領域へ引き上げる極意

こんにちは。自動化の荒波を共に渡るエンジニアの皆さん。
「マクロの記録」や、ネットで見つけた断片的なコードを組み合わせて、なんとか動くツールを作れるようになった……。もしあなたが今、そんな段階にいるのなら、おめでとうございます。あなたはすでに「自動化の入り口」を突破しています。

しかし、ここから先には「プロフェッショナル」と「ただ動くコードを書く人」を分かつ、巨大な壁が立ちはだかっています。

その壁の名は、「循環参照(無限ループ)」。

今日は、FileSystemObject(FSO)を使ってフォルダを深く探索する際、多くの初学者が陥り、ベテランですら一瞬油断すると足元を掬われる「NTFSジャンクション/シンボリックリンク」という罠、そしてそれを完璧に制御するための「階層レベル制限」と「訪問済みリスト(Dictionary)」を用いた防御アルゴリズムを伝授します。

ここをクリアすれば、あなたの書くVBScriptは「ただのスクリプト」から「堅牢なシステム」へと進化します。準備はいいですか?

1. なぜ、あなたのプログラムは「止まらなくなる」のか?

FSOを使って「フォルダの中にあるフォルダを、さらにその中にあるフォルダ……」と再帰的に探索するコードを書く際、私たちはつい、以下のような論理で進めてしまいます。

> 「フォルダを見つけたら、その中身をまた同じルールで探しに行く」

これは数学的には美しい再帰構造ですが、Windowsのファイルシステム(NTFS)においては、恐ろしい罠が潜んでいます。それが「ジャンクション(Junction)」や「シンボリックリンク(Symbolic Link)」です。

罠の正体:鏡の中の迷宮

例えば、`C:\Work` というフォルダの中に、`C:\Work\LinkToParent` という名前の「ジャンクション」があるとしましょう。このジャンクションは、中身を覗くと「親である `C:\Work`」を指しています。

もし、あなたのプログラムが何も対策せずにこのリンクを辿ると……

1. `C:\Work` を探索
2. その中の `LinkToParent` を発見
3. `LinkToParent`(中身は `C:\Work`)を探索
4. また `LinkToParent` を発見……

無限ループの完成です。 プログラムはメモリを食いつぶし、CPUを占有し、最終的にはスタックオーバーフロー(メモリの限界突破)でクラッシュします。

2. プロフェッショナルの防具:二重のガード機構

この迷宮を突破するためには、二つの「盾」を装備する必要があります。

① 階層レベル制限(Depth Limit)

「どれだけ深く潜っても、これ以上は行かない」という物理的な限界を決めることです。これにより、万が一ロジックにミスがあっても、プログラムが永遠に暴走し続けることを防ぎます。

② 訪問済みパスリスト(Visited Path List)

「一度通った道は、二度と通らない」という記憶です。`Scripting.Dictionary` オブジェクトを使い、これまでに探索したフォルダのフルパスを記録しておきます。もし次に進もうとした先が、既にリストにあるパスなら、そこは「循環している」と判断して探索を中止します。

3. 実装:堅牢な再帰探索アルゴリズム

それでは、魂を込めたコードを見ていきましょう。このコードはそのまま、実務のスクリプトのベースとして使えるレベルに仕上げています。

' ==========================================================================
' テーマ: NTFSジャンクション/シンボリックリンク対策済み・再帰フォルダ探索
' 設計思想: 
'   1. Dictionaryによるパス重複検知 (O(1) の高速検索)
'   2. 探索深度(Depth)による暴走防止
' ==========================================================================

Option Explicit

Dim fso, targetPath, visitedPaths, maxDepth
Set fso = CreateObject("Scripting.FileSystemObject")
Set visitedPaths = CreateObject("Scripting.Dictionary")

' --- 設定 ---
targetPath = "C:\Your\Target\Folder" ' 探索を開始するパス
maxDepth = 10                         ' 最大探索深度(これ以上深くは潜らない)

' --- 実行 ---
If fso.FolderExists(targetPath) Then
    WScript.Echo "探索を開始します: " & targetPath
    ' 再帰関数の呼び出し (フォルダオブジェクト, 訪問済み辞書, 現在の深さ, 最大深度)
    Call TraverseFolder(fso.GetFolder(targetPath), visitedPaths, 0, maxDepth)
    WScript.Echo "探索が正常に終了しました。"
Else
    WScript.Echo "エラー: 指定されたパスが存在しません。"
End If

' ==========================================================================
' サブルーチン: TraverseFolder (再帰探索のコア)
' ==========================================================================
Sub TraverseFolder(objFolder, objDict, currentDepth, limitDepth)
    Dim subFolder, folderPath
    folderPath = objFolder.Path

    ' --- ガード1: 階層レベル制限 ---
    If currentDepth > limitDepth Then
        ' WScript.Echo "[SKIP] 深度制限到達: " & folderPath
        Exit Sub
    End If

    ' --- ガード2: 訪問済みパスのチェック (循環参照対策) ---
    ' DictionaryのExistsメソッドは非常に高速です。
    If objDict.Exists(folderPath) Then
        ' WScript.Echo "[LOOP DETECTED] 循環参照を検知、スキップします: " & folderPath
        Exit Sub
    End If

    ' 現在のパスを「訪問済み」として記録
    ' Keyにパス、ValueにTrueを格納
    objDict.Add folderPath, True

    ' --- メイン処理: ここにやりたい処理(ファイル一覧取得など)を書く ---
    ' 例として、フォルダ名を表示
    WScript.Echo String(currentDepth * 2, " ") & "|-- " & objFolder.Name

    ' --- 再帰処理: サブフォルダを探索 ---
    For Each subFolder In objFolder.SubFolders
        ' 再帰呼び出し(深さを +1 する)
        Call TraverseFolder(subFolder, objDict, currentDepth + 1, limitDepth)
    Next

    ' 【重要】プロフェッショナルの知恵:
    ' 探索が終わった後に objDict.Remove(folderPath) をするかどうかは設計次第。
    ' 1. 完全に「一度通った場所は二度と通らない」なら、Removeしない。
    ' 2. 「同じ階層の別ルートからの重複」を許容するなら、Removeする。
    ' 今回の「循環参照対策」としては、Removeしない(=一度見たパスは二度と見ない)のが正解です。
End Sub

4. コードの深淵を読み解く:エンジニアの視点

このコードには、単なる「動くコード」を超えた、いくつかの重要な設計思想が隠されています。

なぜ `Scripting.Dictionary` なのか?

「訪問済みリスト」を作る際、配列(Array)を使いたくなるかもしれません。しかし、配列で「このパスは既にあるか?」を探すには、毎回最初から最後までチェック(線形探索)する必要があり、フォルダ数が増えると劇的に遅くなります。
一方、`Dictionary` オブジェクトはハッシュテーブルを利用しているため、パスがどれほど増えても、一瞬($O(1)$)で存在を確認できます。これが「パフォーマンスの重み」を知る者の選択です。

階層レベル(Depth)の意味

`currentDepth` を引数で渡していく手法は、再帰処理における王道です。`String(currentDepth * 2, ” “)` という部分でインデントを作っていますが、これはデバッグ時に「今、自分がどの階層にいるのか」を視覚的に理解するために極めて重要です。

オブジェクトのライフサイクル

VBScriptにおいて、`FSO` や `Dictionary` などの外部オブジェクトは、使い終わったら解放するのがマナーです。大規模なスクリプトでは、`Set obj = Nothing` を意識することで、メモリリークを防ぎ、システムの安定性を高めることができます。

5. まとめ:一歩先のエンジニアへ

今回の学びを振り返りましょう。

1. NTFSジャンクションは「無限ループ」の入り口である。
2. 「階層制限(Depth Limit)」で、物理的な暴走を止める。
3. 「訪問済みリスト(Dictionary)」で、論理的な循環を止める。

これらを実装できるようになれば、あなたのスクリプトは「何が起きても止まらない、信頼できるツール」へと昇華されます。

プログラミングは、単に命令を並べる作業ではありません。「起こりうる最悪の事態」を想像し、それをエレガントに回避する設計を施すこと。その積み重ねが、あなたを一流のエンジニアへと押し上げます。

さあ、このコードを自分のプロジェクトに組み込んで、その違いを体感してみてください。応援していますよ!

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