【実務・中級編】【実務中級者向け】Scripting.DictionaryとFSOを組み合わせた複数テキストファイルの一括置換テンプレートエンジン – VBScript (Visual Basic Scripting Edition)解析バイブル

スポンサーリンク

はじめに:なぜ令和の現場で「VBScriptによるテンプレートエンジン」なのか

現場のインフラ構築、ネットワーク機器のコンフィグ生成、あるいはクラスタ環境向け個別設定ファイルの大量展開——。そうしたタスクに直面したとき、モダンなPython環境やPowerShellの実行ポリシー(ExecutionPolicy)の制約に阻まれ、足踏みした経験はないだろうか。

外部ランタイムのインストールが厳格に禁止された閉域網サーバーやレガシー端末において、「ダブルクリック一発で、依存関係ゼロで確実に動作する」という要件を満たせる唯一無二の兵器が、Windows標準のWSH(Windows Script Host)であり、VBScriptだ。

しかし、現場で書かれているスクリプトの多くは目を覆いたくなるほど稚拙だ。1行ずつ`ReadLine`しながら愚直に文字列を探索し、ディスクI/Oを無駄に浪費し、文字コードの扱いに失敗してファイル破損を引き起こす——。

本稿では、実務中級者に向けて、`Scripting.FileSystemObject`(FSO)と`Scripting.Dictionary`を極限まで論理的に組み合わせ、何百・何千という個別設定ファイルをミリ秒単位で生成する堅牢なテンプレートエンジンのアーキテクチャを伝授する。

—

1. 凡百のコードが陥る3つの致命的アンチパターン

優れた設計を理解するには、まず「なぜ素人のコードが遅く、脆いのか」を解明しなければならない。

① ディスクI/Oの頻殺(`ReadLine` ループの罠)

テンプレートを1行読み込むごとに置換処理を行い、出力ファイルへ1行書き込む手法だ。ディスクI/Oのオーバーヘッドはメモリ上の文字列操作に比べて桁違いに重い。数MBのテンプレートや数千ファイルの処理において、この設計は致命的なボトルネックとなる。テンプレートは一度だけメモリに丸ごと吸い上げ(`ReadAll`)、オンメモリで完結させるのが鉄則だ。

② `Dictionary` の大文字・小文字不整合(`CompareMode` 未指定)

`Scripting.Dictionary` のデフォルトの比較モードは `BinaryCompare`(バイナリ準拠:大文字と小文字を区別する)だ。これを知らずにテンプレート側のタグを `${username}`、定義テーブルを `UserName` で登録すると、置換が完全に空振りする。実務ツールでは、意図しないタグ記述の揺らぎを許容できるよう、明示的にテキスト比較モード(`TextCompare`)を設定する必要がある。

③ タグの部分前方一致による「誤爆」置換

単純な `Replace` 関数をループで回す際、タグの命名規則に注意を払わないと悲劇が起きる。
例えば、辞書の中に以下の2つが存在する場合を考えてほしい。

  • `${USER}` = `Alice`
  • `${USER_NAME}` = `Alice_Smith`

もし辞書のイテレーション順が `${USER}` から始まった場合、テンプレート内の `${USER_NAME}` は `${USER}` の部分だけが先に置換され、`Alice_NAME` という残骸に化けてしまう。これを防ぐには正規表現境界の利用か、あるいは明確なデリミタ設計(接頭辞・接尾辞の統一)が必須だ。

—

2. アーキテクチャ設計:堅牢性を担保する4つの要件

今回構築するテンプレートエンジンの論理構造は極めてシンプルだが、隙のない防御的プログラミングを施す。

[パラメータソース (Dictionary)]
│
▼
┌────────────────────────────────────────┐
│ TemplateEngine Core │
│ 1. テンプレートの一括メモリロード │
│ 2. キャッシュされたバッファの高速置換 │
│ 3. ディレクトリ存在検証 & 自動生成 │
└────────────────────────────────────────┘
│
▼
[出力ファイル (FSO TextStream)]

1. メモリファーストの徹底: テンプレートは最初に `ReadAll` で文字列バッファ化し、ループ内ではディスクから再読み込みしない。
2. プレースホルダー形式の統一: 置換タグは `${KeyName}` に統一し、正規表現を用いずとも単純な文字列置換で安全に衝突を回避できる構造を採る。
3. 階層ディレクトリの自動補完: 出力先フォルダが存在しない場合にスクリプトが落ちるのを防ぐため、出力前にディレクトリの再帰的検証を行う。
4. 文字コードの境界意識: FSOは基本的にANSI(Shift-JIS)とUTF-16(LE)しかネイティブ対応していない。BOMなしUTF-8を扱う必要がある実務シーンへの処方箋も後述する。

—

3. 実践プロダクションコード

以下のコードは、そのまま `.vbs` として保存して実行可能な完全な実装だ。実務での再利用性を高めるため、エンジン本体を関数群として疎結合に保ち、パラメータテーブルから反復生成する構造に仕立ててある。

‘==============================================================================
‘ Module : BatchTemplateEngine.vbs
‘ Description: DictionaryとFSOを連携させた高速テンプレート展開スクリプト
‘ Target : Windows Script Host (WSH) / VBScript
‘ Author : Lead Architecture Engineer
‘==============================================================================
Option Explicit

‘ メインルーチンの実行
Call Main()

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

‘————————————————————————–
‘ パス定義(実行スクリプトと同一フォルダを基準にする)
‘————————————————————————–
Dim scriptDir, templatePath, outputDir
scriptDir = fso.GetParentFolderName(WScript.ScriptFullName)
templatePath = fso.BuildPath(scriptDir, “template_config.txt”)
outputDir = fso.BuildPath(scriptDir, “dist”)

‘ サンプル用のテンプレートファイルを自動生成(検証用)
Call PrepareDummyTemplate(fso, templatePath)

‘ 出力先ディレクトリの担保
Call EnsureDirectoryExists(fso, outputDir)

‘————————————————————————–
‘ テンプレート文字列の一括キャッシュ(ディスクI/Oの最小化)
‘————————————————————————–
Dim templateBuffer
templateBuffer = LoadTemplateContent(fso, templatePath)

If Len(templateBuffer) = 0 Then
WScript.Echo “[ERROR] テンプレートが空、または読み込みに失敗しました。”
Set fso = Nothing
Exit Sub
End If

‘————————————————————————–
‘ 定義テーブル(生成したいファイル群のパラメータ)の作成
‘ ※実務ではCSVやExcelからループ生成する箇所
‘————————————————————————–
Dim targetList
Set targetList = CreateObject(“Scripting.Dictionary”)

‘ Node 01 パラメータ
Dim param01
Set param01 = CreateDictionary()
param01.Add “HOSTNAME”, “web-srv-01”
param01.Add “IP_ADDRESS”, “192.168.10.11”
param01.Add “ENVIRONMENT”, “Production”
param01.Add “MAX_CLIENTS”, “1024”
targetList.Add “web-srv-01.conf”, param01

‘ Node 02 パラメータ
Dim param02
Set param02 = CreateDictionary()
param02.Add “HOSTNAME”, “web-srv-02”
param02.Add “IP_ADDRESS”, “192.168.10.12”
param02.Add “ENVIRONMENT”, “Production”
param02.Add “MAX_CLIENTS”, “1024”
targetList.Add “web-srv-02.conf”, param02

‘ Node 03 パラメータ(ステージング)
Dim param03
Set param03 = CreateDictionary()
param03.Add “HOSTNAME”, “stg-srv-01”
param03.Add “IP_ADDRESS”, “192.168.20.11”
param03.Add “ENVIRONMENT”, “Staging”
param03.Add “MAX_CLIENTS”, “256”
targetList.Add “stg-srv-01.conf”, param03

‘————————————————————————–
‘ 一括展開エンジンのキック
‘————————————————————————–
Dim fileName, currentParam, generatedText, outFilePath
Dim successCount, fileKeys
successCount = 0
fileKeys = targetList.Keys

Dim i
For i = 0 To UBound(fileKeys)
fileName = fileKeys(i)
Set currentParam = targetList.Item(fileName)

‘ オンメモリ一括置換
generatedText = RenderTemplate(templateBuffer, currentParam)

‘ ファイル出力
outFilePath = fso.BuildPath(outputDir, fileName)
If WriteContentToFile(fso, outFilePath, generatedText) Then
successCount = successCount + 1
WScript.Echo “[SUCCESS] ” & fileName & ” を生成しました。”
Else
WScript.Echo “[FAILURE] ” & fileName & ” の生成に失敗しました。”
End If

‘ ループ内参照の解放
Set currentParam = Nothing
Next

WScript.Echo “処理完了: ” & CStr(successCount) & ” / ” & CStr(targetList.Count) & ” 件 生成成功”

‘ オブジェクトの明示的クリーンアップ
Set targetList = Nothing
Set fso = Nothing
End Sub

‘==============================================================================
‘ 高速レンダリングエンジン
‘ 引数のTemplateTextに対し、Dictionary内のキー `${Key}` をValueに置換
‘==============================================================================
Function RenderTemplate(ByVal templateText, ByRef paramDict)
Dim rendered, key, tag
rendered = templateText

‘ paramDict のキーを巡回
For Each key In paramDict.Keys
tag = “${” & CStr(key) & “}”
‘ TextCompare(1) でケースの揺らぎを吸収しつつ高速置換
rendered = Replace(rendered, tag, CStr(paramDict.Item(key)), 1, -1, 1)
Next

RenderTemplate = rendered
End Function

‘==============================================================================
‘ テンプレートのオンメモリ読み込み(一括取得)
‘==============================================================================
Function LoadTemplateContent(ByRef fso, ByVal filePath)
On Error Resume Next
LoadTemplateContent = “”

If Not fso.FileExists(filePath) Then
WScript.Echo “[ERROR] テンプレートファイルが存在しません: ” & filePath
Exit Function
End If

Dim stream
‘ ForReading(1), TristateFalse(0: ASCII/ANSI)
Set stream = fso.OpenTextFile(filePath, 1, False, 0)
If Err.Number <> 0 Then
WScript.Echo “[ERROR] ファイルオープンに失敗: ” & Err.Description
Err.Clear
Exit Function
End If

‘ ディスクから一括読み出し
LoadTemplateContent = stream.ReadAll
stream.Close
Set stream = Nothing
On Error GoTo 0
End Function

‘==============================================================================
‘ ファイルへのアトミック書き込み
‘==============================================================================
Function WriteContentToFile(ByRef fso, ByVal destinationPath, ByVal contentText)
On Error Resume Next
WriteContentToFile = False

Dim stream
‘ ForWriting(2), Create(True), TristateFalse(0: ASCII/ANSI)
Set stream = fso.OpenTextFile(destinationPath, 2, True, 0)
If Err.Number <> 0 Then
Err.Clear
Exit Function
End If

stream.Write contentText
stream.Close
Set stream = Nothing

If Err.Number = 0 Then
WriteContentToFile = True
End If
On Error GoTo 0
End Function

‘==============================================================================
‘ Dictionaryファクトリ(TextCompareモードを強制)
‘==============================================================================
Function CreateDictionary()
Dim dict
Set dict = CreateObject(“Scripting.Dictionary”)
‘ 1: TextCompare (大文字・小文字を同一視して安全性を確保)
dict.CompareMode = 1
Set CreateDictionary = dict
End Function

‘==============================================================================
‘ ディレクトリの存在検証と自動作成
‘==============================================================================
Sub EnsureDirectoryExists(ByRef fso, ByVal dirPath)
If Not fso.FolderExists(dirPath) Then
fso.CreateFolder(dirPath)
End If
End Sub

‘==============================================================================
‘ 検証用ダミーテンプレート作成プロシージャ
‘==============================================================================
Sub PrepareDummyTemplate(ByRef fso, ByVal templatePath)
If Not fso.FileExists(templatePath) Then
Dim stream, dummyContent
Set stream = fso.OpenTextFile(templatePath, 2, True, 0)
dummyContent = “# ========================================” & vbCrLf & _
“# Configuration for: ${HOSTNAME}” & vbCrLf & _
“# Generated automatically by VBScript Engine” & vbCrLf & _
“# ========================================” & vbCrLf & _
“ServerEnvironment = ${ENVIRONMENT}” & vbCrLf & _
“ListenAddress = ${IP_ADDRESS}:8080” & vbCrLf & _
“MaxConnections = ${MAX_CLIENTS}” & vbCrLf & _
“LogLevel = INFO” & vbCrLf
stream.Write dummyContent
stream.Close
Set stream = Nothing
End If
End Sub

—

4. プロダクション運用の急所:設計者が把握すべき仕様限界

上記のコードを実戦投入する際、シニアエンジニアとして以下の2点を必ず仕様レベルで押さえておかなければならない。

① FSOの文字コード限界と `ADODB.Stream` への昇格判断

FSOの `OpenTextFile` は、ANSI(日本語WindowsではCP932/Shift-JIS)とUTF-16(LE)しか扱えない。
もしターゲットシステムが「BOMなしUTF-8」を要求するLinuxサーバのコンフィグファイル(NginxやPostgreSQLなど)である場合、FSOで書き出すと改行コードやマルチバイト文字(日本語コメント等)が文字化けの原因となる。

UTF-8出力を完全に制御したい場合は、FSOの書き出し部分を以下の `ADODB.Stream` パターンに差し替えるのがプロの作法だ。

‘ BOMなしUTF-8で保存するテクニック(抜粋)
Sub WriteTextUtf8NoBom(ByVal filePath, ByVal contentText)
Dim adodbStream, binaryStream
Set adodbStream = CreateObject(“ADODB.Stream”)

adodbStream.Type = 2 ‘ adTypeText
adodbStream.Charset = “UTF-8”
adodbStream.Open
adodbStream.WriteText contentText

‘ BOM(最初の3バイト: EF BB BF)をスキップするためバイナリモードで抜き出す
adodbStream.Position = 0
adodbStream.Type = 1 ‘ adTypeBinary
adodbStream.Position = 3 ‘ スキップ

Set binaryStream = CreateObject(“ADODB.Stream”)
binaryStream.Type = 1
binaryStream.Open
adodbStream.CopyTo binaryStream

binaryStream.SaveToFile filePath, 2 ‘ adSaveCreateOverWrite

binaryStream.Close
adodbStream.Close
Set binaryStream = Nothing
Set adodbStream = Nothing
End Sub

② COMオブジェクトの破棄とメモリリークの抑止

VBScriptには世代別ガベージコレクションは存在せず、参照カウント方式(Reference Counting)によってオブジェクトが管理されている。
サブルーチンや関数内で生成したローカルなオブジェクト変数は、スコープを抜ける際に参照カウントが減算されるが、巨大なループ処理の最中に明示的に `Set dict = Nothing` を怠ると、参照カウントの循環参照やスコープの残留によってメモリフットプリントが肥大化し続ける場合がある。
特にループ内部で大量のDictionaryを動的生成・破棄する場合は、スコープの終了時あるいはループの末尾で必ず `Nothing` を代入する規律を徹底せよ。

—

5. まとめ

  • I/Oの局所化: テンプレートは `ReadAll` で1度だけメモリに吸い上げ、置換処理はオンメモリの `Replace` 関数で完結させる。
  • CompareModeの徹底: `Scripting.Dictionary` を生成する際は、常に `CompareMode = 1`(TextCompare)を明示して意図せぬ大文字小文字の置換ミスを封じる。
  • プレースホルダーの衝突回避: 単純な文字列ではなく `${TAG}` のように境界を定義することで、部分一致による文字列破壊を完全に防ぐ。
  • 出力文字コードの要件確認: Shift-JISで十分なら軽量なFSO、BOMなしUTF-8が必須なら `ADODB.Stream` へ切り替える。

「枯れた技術」とは、裏を返せば「すべての落とし穴が解明され尽くした技術」である。VBScriptの内部挙動をロジカルに支配し、一切の無駄を削ぎ落としたスクリプトを組むことで、最新のフレームワークにも劣らない高速かつ信頼性の高い自動化基盤を現場にもたらすことができる。この設計美学を、ぜひ日々のツール開発に反映させてほしい。

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