【実務・中級編】【実務中級者向け】FSOとWScript.Shellを組み合わせたシンボリックリンク/ジャンクションポイントの判別と安全スキップ – VBScript (Visual Basic Scripting Edition)解析バイブル

スポンサーリンク

VBScript再入門:FSOとWScript.Shellで構築する「無限ループに陥らない」NTFS再解析ポイント(ジャンクション/シンボリックリンク)絶対安全回避アルゴリズム

`Scripting.FileSystemObject`(以下FSO)を用いた再帰的なフォルダ走査スクリプトを書いていて、「なぜかスクリプトが応答なしになる」「スタックオーバーフローで異常終了する」という現象に遭遇したことはないでしょうか?

その原因の9割以上は、NTFSの「ジャンクション(Junction Point)」や「シンボリックリンク(Symbolic Link)」といった「再解析ポイント(Reparse Point)」を踏み抜き、循環参照(例: フォルダAの配下にフォルダAを指すジャンクションが存在する)による無限ループに陥っていることです。

特にWindows 7以降、`C:\Users\All Users`(`C:\ProgramData`へのジャンクション)や`C:\Users\[UserName]\Application Data`(`AppData\Roaming`へのジャンクション)など、OS標準で大量のジャンクションが配置されています。これらを無視してFSOで単純な再帰処理を組むのは、運用環境における巨大なタイムボムを抱えると同義です。

本稿では、FSOの限界を突破し、`WScript.Shell`とビット演算を組み合わせることで、ジャンクションやシンボリックリンクをミリ秒単位で判定・安全スキップし、堅牢にフォルダを完走させるプロダクションレベルの設計とVBScriptコードを提示します。

—

1. なぜFSO単体ではジャンクションで「爆竹」が起きるのか?

アーキテクチャの観点から問題の本質を整理しましょう。

FSOはWindows 95/NT時代に設計された抽象化層であり、後年にNTFSの主力となった「再解析ポイント(Reparse Point)」を正しく識別する高レベルAPIを持っていません。

[通常フォルダ (C:\Data\FolderA)]
└── [通常子フォルダ (Sub1)]
└── [ジャンクション (Junc1)] ──(参照)──> [親フォルダ (C:\Data)]

上記のような構造のフォルダを、FSOの `Folder.SubFolders` で素直に再帰呼び出し(Recursion)するとどうなるか。

1. `C:\Data` を走査
2. `FolderA` に入る
3. `Junc1` を見つける(FSOはこれを「普通のフォルダ」として取得する)
4. `Junc1` の中に入る = 実際は `C:\Data` に戻る
5. 無限再帰呼び出しが発生し、Call Stackが溢れてVBScriptがクラッシュ(あるいは永久ビジー)

「FSOだけで頑張る」のは不可能です。我々はOS層(NTFSのファイル属性)に一歩踏み込んで判定を行う必要があります。

—

2. 判別のアルゴリズム:ビット演算とWScript.Shellのハイブリッド

では、どうやってジャンクション/シンボリックリンクを判別するのか。鍵は ファイル/フォルダ属性のビットフラグ `1024 (0x400)` です。

① 高速判定:`Folder.Attributes` のビット判定(Fast Path)

WindowsのNTFSにおいて、再解析ポイントが設定されたファイルやフォルダには、属性値に `1024`(`FILE_ATTRIBUTE_REPARSE_POINT`)のビットが立ちます。FSOの `Folder.Attributes` からこのビットを判定するのが最も高速です。

‘ 属性値と 1024 の AND を取り、0でなければ再解析ポイント(ジャンクションまたはシンボリックリンク)
IsReparsePoint = ((objFolder.Attributes And 1024) <> 0)

② 詳細情報取得:`WScript.Shell` によるターゲット解体(Slow Path)

属性チェックで「再解析ポイントである」と判明したもののうち、「ログとしてリンク先を出力したい」「特定のシンボリックリンクだけは追跡したい」という要件がある場合は、`WScript.Shell` を用いて CLI(`cmd.exe /c dir /a` や `fsutil`)を呼び出し、リンク先パスを取得します。

全フォルダに対して CLI を発行するとプロセス生成コストで速度が壊滅しますが、「ビット演算で弾かれたものだけ Shell を呼ぶ」という二段階構え(Fast Path / Slow Path パターン)を取ることで、超高速かつ詳細な制御が可能になります。

—

3. 堅牢な設計における「3つの鉄則」

現場で動くスクリプトを書くために、以下の3つのアーキテクチャ設計を徹底してください。

1. オブジェクトの生成は再帰の外で行う(ライフサイクル管理)
再帰関数の中で `CreateObject(“Scripting.FileSystemObject”)` や `CreateObject(“WScript.Shell”)` を実行するのは致命的なアンチパターンです。メモリを消費し続け、ガベージコレクションが間に合わず内部リークを引き起こします。オブジェクトはスクリプト初期化時に1つだけ生成し、参照を回します。
2. 「Access Denied(エラー70)」の極小スコープ防衛
システムフォルダや他ユーザーのジャンクションには、厳重なACL(アクセス権)が設定されており、属性参照すら拒否されるケースがあります。`On Error Resume Next` はコード全体にかけず、「フォルダオブジェクト取得」「属性参照」のピンポイントでのみ適用し、直後に `Err.Clear` で解除します。
3. データベース/ファイル連携時の重複排除
走査結果をDB(SQL Server, SQLite, Access等)やCSVに書き込む際、ジャンクションを誤って展開すると「同じ実体ファイルが別パスで二重にDB登録される」というデータ汚染が起きます。データベースの設計上も、再解析ポイントは「属性:Link」としてメタデータのみを登録し、下層の探索はスキップするのが原則です。

—

4. 完全実装:プロダクショングレードのVBScriptコード

以下は、そのまま実務に組み込める完全なスクリプトです。コンソール(cscript.exe)での実行を想定しています。

‘===============================================================================
‘ [名称] Enterprise Safe Directory Walker
‘ [機能] NTFSジャンクションおよびシンボリックリンクを安全に判定・スキップする
‘ 堅牢なフォルダ走査エンジン
‘ [実行方法] cscript.exe //Nologo SafeDirectoryWalker.vbs “C:\TargetDir”
‘===============================================================================
Option Explicit

‘ NTFS 属性定数 (1024 = 0x400 = Reparse Point)
Const FILE_ATTRIBUTE_REPARSE_POINT = 1024

‘ グローバルインスタンス(再帰内での再生成を防ぎ、パフォーマンスを極限まで高める)
Dim g_FSO, g_WshShell

Set g_FSO = CreateObject(“Scripting.FileSystemObject”)
Set g_WshShell = CreateObject(“WScript.Shell”)

‘ メイン処理の呼び出し
Dim targetPath
If WScript.Arguments.Count > 0 Then
targetPath = WScript.Arguments(0)
Else
‘ 引数がない場合のテスト用パス(環境に合わせて変更してください)
targetPath = “C:\Users\Public”
End If

WScript.Echo “=== 走査開始: ” & targetPath & ” ===”

If g_FSO.FolderExists(targetPath) Then
Dim rootFolder
Set rootFolder = g_FSO.GetFolder(targetPath)

‘ 安全な再帰走査の実行
WalkDirectoryDirectorySafe rootFolder, 0
Else
WScript.Echo “[ERROR] 指定されたパスが存在しません: ” & targetPath
End If

WScript.Echo “=== 走査完了 ===”

‘ 明示的なオブジェクト解放
Set g_FSO = Nothing
Set g_WshShell = Nothing

‘——————————————————————————-
‘ [Sub] WalkDirectoryDirectorySafe
‘ [概要] フォルダを安全に再帰走査し、ジャンクション/シンボリックリンクをスキップする
‘——————————————————————————-
Sub WalkDirectoryDirectorySafe(ByRef currentFolder, ByVal depth)
On Error Resume Next

‘ 1. カレントフォルダ自体のファイル一覧を処理(ファイル側もリンクの可能性があるためチェック)
Dim colFiles, objFile
Set colFiles = currentFolder.Files

If Err.Number = 0 Then
For Each objFile In colFiles
‘ ファイル側のシンボリックリンクチェック
If IsReparsePoint(objFile) Then
WScript.Echo GetIndent(depth) & “[FILE-LINK SKIP] ” & objFile.Name
Else
‘ 通常ファイルの処理(例: DB登録やログ出力)
‘ ProcessFile objFile
End If
Next
Else
WScript.Echo GetIndent(depth) & “[ACCESS DENIED (Files)] ” & currentFolder.Path
Err.Clear
End If

‘ 2. サブフォルダの走査と再帰呼び出し
Dim colSubFolders, subFolder
Set colSubFolders = currentFolder.SubFolders

If Err.Number <> 0 Then
WScript.Echo GetIndent(depth) & “[ACCESS DENIED (SubFolders)] ” & currentFolder.Path
Err.Clear
Exit Sub
End If

For Each subFolder In colSubFolders
‘ — 最重要分岐:ジャンクション / シンボリックリンクの判定 —
If IsReparsePoint(subFolder) Then
‘ 【Slow Path】リンクのターゲット(参照先)を解析してログ出力する
Dim targetLink
targetLink = ResolveReparseTarget(subFolder.Path)

WScript.Echo GetIndent(depth) & “[JUNCTION/SYMLINK SKIP] ” & subFolder.Name & ” –> ” & targetLink

‘ ※注意:ここで再帰呼び出し(WalkDirectoryDirectorySafe)を行わずにスルーすることで
‘ 無限ループを100%回避する。
Else
‘ 通常フォルダの場合のみ、配下の処理を行う
WScript.Echo GetIndent(depth) & “[DIR] ” & subFolder.Name

‘ 再帰呼び出し(インデント深度を+1)
WalkDirectoryDirectorySafe subFolder, depth + 1
End If
Next

On Error GoTo 0
End Sub

‘——————————————————————————-
‘ [Function] IsReparsePoint
‘ [概要] 対象のFolder/Fileオブジェクトが再解析ポイント(ジャンクション/Symlink)か判定
‘——————————————————————————-
Function IsReparsePoint(ByRef objItem)
IsReparsePoint = False
On Error Resume Next

‘ Bitwise AND による高速判定
If (objItem.Attributes And FILE_ATTRIBUTE_REPARSE_POINT) <> 0 Then
IsReparsePoint = True
End If

If Err.Number <> 0 Then
‘ 属性取得に失敗した場合(権限エラー等)も安全のためTrue扱いにしてスキップさせる
IsReparsePoint = True
Err.Clear
End If
On Error GoTo 0
End Function

‘——————————————————————————-
‘ [Function] ResolveReparseTarget
‘ [概要] WScript.Shell を使用してジャンクション/シンボリックリンクの実際の参照先を取得する
‘——————————————————————————-
Function ResolveReparseTarget(ByVal folderPath)
ResolveReparseTarget = “(Unknown Target)”

On Error Resume Next
Dim cmd, objExec, strOutput, line
‘ dir /a コマンドで再解析ポイント情報を直接取得
cmd = “%COMSPEC% /c dir /a /L “”” & folderPath & “\..”””

Set objExec = g_WshShell.Exec(cmd)

‘ 標準出力のパース
Do While Not objExec.StdOut.AtEndOfStream
line = objExec.StdOut.ReadLine()
‘ カレントフォルダ名が含まれる行を探す
If InStr(line, “[” & g_FSO.GetFileName(folderPath) & “]”) > 0 Then
‘ 例: 2023/10/01 12:00 JuncName [C:\TargetLocation]
Dim posStart, posEnd
posStart = InStr(line, “[“)
posEnd = InStrRev(line, “]”)
If posStart > 0 And posEnd > posStart Then
ResolveReparseTarget = Mid(line, posStart + 1, posEnd – posStart – 1)
Exit Do
End If
End If
Loop

Set objExec = Nothing
On Error GoTo 0
End Function

‘——————————————————————————-
‘ [Function] GetIndent
‘ [概要] ログ表示用のインデント文字列を生成
‘——————————————————————————-
Function GetIndent(ByVal depth)
GetIndent = String(depth 2, ” “)
End Function

—

5. 開発リーダーが押さえるべき保守と運用のポイント

このコードを実務の自動化ツールに組み込む際、チームメンバーや後任者に指導すべき「アーキテクチャの意図」を整理しておきます。

① なぜ `DIR` コマンドの解析(Slow Path)を分けたのか?

`IsReparsePoint` 関数による判定(Bitwise AND)はメモリ上の属性フラグを見るだけなので、1秒間に数万個のフォルダを判定できます。一方、`WScript.Shell.Exec` はプロセスの生成コストを伴うため、1件あたり数十ミリ秒を消費します。
すべてのフォルダに対して `Exec` を実行するとスクリプトのパフォーマンスは100倍以上劣化します。「スキップの決断(Fast Path)」と「理由の調査(Slow Path)」を分離する設計思想をコードから読み取らせてください。

② 大規模ファイルサーバー走査時の「スタックオーバーフロー」対策

本コードは再帰(Recursion)構造をとっています。WindowsのNTFSフォルダ階層は通常数十階層程度なので問題ありませんが、極端に深い階層(パス長制限 260文字を超えるような構造)を走査する場合、VBScriptのコールスタックの上限(数十から数千レベル)に達する可能性があります。
超大規模なファイルサーバー走査ツールへ拡張する場合は、再帰呼び出しではなく、Queue(キュー)オブジェクトを用いたループ構造(幅優先探索:BFS)へリファクタリングすることを検討してください。

③ RDBMS / Excel 連携時の設計配慮

走査結果をデータベースやCSVに出力する場合、以下のカラム設計を行ってください。

  • `Path` (文字列): ファイル/フォルダのフルパス
  • `IsLink` (フラグ): 再解析ポイントかどうか(`1` or `0`)
  • `LinkTarget` (文字列): `IsLink = 1` の場合の参照先パス

ジャンクションを安全にスキップすることで、「同じファイルが別のパス名で二重・三重にDBにインサートされ、集計値や容量計算が狂う」という、データパイプラインでの深刻なロジックバグを根本から防ぐことができます。

—

結び:レガシー技術だからこそ「本質的理解」が差を生む

VBScriptは枯れた技術であり、現代ではPowerShellやPythonへの移行が進んでいます。しかし、レガシーシステムの維持運用、特定の制約下でのエージェントレス自動化など、VBScriptで堅牢なコードを書かなければならない現場は依然として存在します。

単に「動くコード」を書くレベルから脱却し、OS(NTFS)の内部構造、APIの特性、メモリとプロセスのコストを把握した上でコードを組み上げる。これこそが、トラブルフリーな自動化ツールを作り出す真のエンジニアリングです。このコードをベースに、あなたのプロジェクトの走査処理を「絶対落ちない堅牢な設計」へとアップデートしてください。

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