【実務・中級編】【上級プロフェッショナル向け】FSOで差分定義テキスト(Patch形式)をパースしターゲット設定ファイルを自動更新・適用する簡易パッチ適用エンジン – VBScript (Visual Basic Scripting Edition)解析バイブル

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

構成管理の極致:FSOを用いた堅牢な「パッチ適用エンジン」の設計と実装

「スクリプトを書いた。動いた。よし、完了だ」――もし君がそう考えているなら、そのツールは現場に投入した瞬間に、システムを破壊する時限爆弾に変わるだろう。

業務自動化、特に設定ファイル(INIや構成テキスト)の更新という領域は、一見単純に見えて、実は最も「設計の質」が問われる領域だ。ファイルロック、書き込み中のプロセス遮断、不完全なパッチによる設定の破損……。これらに対処できないコードは、自動化ではなく「自動的な障害発生器」に過ぎない。

本記事では、VBScriptの`FileSystemObject` (FSO) を使い倒し、単なる「書き換えスクリプト」を超えた、「バックアップ・アトミック性・検証」を備えたプロフェッショナル仕様のパッチ適用エンジンを構築する方法を伝授する。

1. プロフェッショナルの設計思想:なぜ「逐次書き換え」は悪なのか

初心者は、パッチ指示(例:`SET Key=Value`)を見つけるたびに、ターゲットファイルを一行ずつ走査して書き換えようとする。これは致命的な設計ミスだ。

なぜ逐次書き換えはダメなのか?

1. パフォーマンスの欠如: ファイルの読み書き(I/O)は極めて重い処理だ。ループ内で頻繁にファイルを開閉するのは、リソースの無駄遣いである。
2. 不完全な状態(Partial State)の発生: 書き換えの途中でエラーや電源断が発生した場合、ターゲットファイルは「半分更新された、壊れた状態」で残る。
3. 不整合の発生: 「削除」と「追加」が混在する場合、ファイルポインタの管理が極めて複雑になり、バグの温床となる。

我々が採用すべき「メモリ・マッピング・パターン」

プロフェッショナルは、以下のライフサイクルで処理を完結させる。
1. Snapshot: ターゲットファイルを完全にバックアップする。
2. Load: ターゲットの内容をすべてメモリ上(`Scripting.Dictionary`)に展開する。
3. Patch: パッチファイルを解析し、メモリ上のオブジェクトを操作する。
4. Flush: メモリ上の状態を「一時ファイル」に一括書き出し、最後にターゲットと入れ替える。

この設計により、処理のアトミック性(全か無か)を担保できる。

2. パッチ定義フォーマットの策定

エンジンの精度は、入力データの規律に依存する。今回は、以下のシンプルな形式を定義する。
`COMMAND|KEY|VALUE`

  • `ADD|Key|Value` : 新規追加(既存なら無視、または上書き)
  • `SET|Key|Value` : 更新
  • `DEL|Key|` : 削除

3. 実装:堅牢なパッチ適用エンジンのプロダクションコード

以下に、実務でそのまま使えるレベルのモジュール構成を示した。単なるコードの羅列ではなく、エラーハンドリングとリソース管理を徹底している。

'**
' PatchEngine.vbs
' 【設計思想】
' 1. Dictionaryオブジェクトによるメモリ内操作(高速・安全)
' 2. 処理前の強制バックアップ(リカバリ性)
' 3. 一時ファイル経由の置換(アトミック性の擬似実現)
'**
Option Explicit

Dim FSO, Dict, PatchEngine
Set FSO = CreateObject("Scripting.FileSystemObject")
Set PatchEngine = CreateObject("Scripting.Dictionary")

' --- メイン実行セクション ---
Dim targetCfg, patchFile, backupFile
targetCfg = "C:\Config\settings.ini"  ' ターゲット設定ファイル
patchFile = "C:\Config\patch.txt"     ' パッチ定義ファイル
backupFile = targetCfg & ".bak"       ' バックアップ先

On Error Resume Next ' エラーハンドリング開始

If Not FSO.FileExists(targetCfg) Then
    WScript.Echo "Error: Target file not found."
    WScript.Quit
End If

' 1. バックアップの作成
If Not CreateBackup(targetCfg, backupFile) Then
    WScript.Echo "Error: Backup failed. Aborting to prevent data loss."
    WScript.Quit
End If

' 2. パッチ適用実行
If ApplyPatch(targetCfg, patchFile) Then
    WScript.Echo "Success: Patch applied successfully."
Else
    WScript.Echo "Error: Patch application failed. Please check the backup."
End If

' --- クラス/関数定義 ---

''' <summary>
''' ファイルのバックアップを作成する
''' </summary>
Function CreateBackup(src, dest)
    On Error Resume Next
    ' 既存のバックアップがあれば削除
    If FSO.FileExists(dest) Then FSO.DeleteFile dest, True
    FSO.CopyFile src, dest, True
    CreateBackup = (Err.Number = 0)
    If Err.Number <> 0 Then Err.Clear
End Function

''' <summary>
''' パッチ適用エンジンのコアロジック
''' </summary>
Function ApplyPatch(targetPath, patchPath)
    Dim ts, line, parts, cmd, k, v, tempPath
    Dim dictTarget, dictPatch
    
    ApplyPatch = False
    tempPath = targetPath & ".tmp"
    Set dictTarget = CreateObject("Scripting.Dictionary")
    
    ' --- STEP 1: ターゲットをメモリにロード ---
    Set ts = FSO.OpenTextFile(targetPath, 1) ' ForReading
    Do Until ts.AtEndOfStream
        line = Trim(ts.ReadLine)
        If line <> "" And InStr(line, "=") > 0 Then
            parts = Split(line, "=")
            If UBound(parts) >= 1 Then
                dictTarget(Trim(parts(0))) = Trim(parts(1))
            End If
        End If
    Loop
    ts.Close

    ' --- STEP 2: パッチを解析してDictionaryを操作 ---
    Set ts = FSO.OpenTextFile(patchPath, 1)
    Do Until ts.AtEndOfStream
        line = Trim(ts.ReadLine)
        If line <> "" Then
            parts = Split(line, "|")
            If UBound(parts) >= 2 Then
                cmd = parts(0)
                k = parts(1)
                v = parts(2)
                
                Select Case cmd
                    Case "ADD", "SET"
                        dictTarget(k) = v
                    Case "DEL"
                        If dictTarget.Exists(k) Then dictTarget.Remove(k)
                End Select
            End If
        End If
    Loop
    ts.Close

    ' --- STEP 3: 一時ファイルへ書き出し ---
    Set ts = FSO.CreateTextFile(tempPath, True) ' ForWriting
    Dim key
    For Each key In dictTarget.Keys
        ts.WriteLine key & "=" & dictTarget(key)
    Next
    ts.Close

    ' --- STEP 4: アトミックな置換 ---
    ' 一時ファイルが正常に作成できていれば、ターゲットを置き換える
    If FSO.FileExists(tempPath) Then
        FSO.DeleteFile targetPath, True
        FSO.MoveFile tempPath, targetPath
        ApplyPatch = True
    Else
        ApplyPatch = False
    End If
End Function

4. 運用における「極限の注意点」

このコードをプロダクション環境に投入するにあたり、アーキテクトとして以下の3点を警告しておく。

① 文字コードの罠 (Encoding)

FSOの`OpenTextFile`は、基本的にANSI (Shift-JIS)を前提としている。もしターゲットファイルやパッチファイルが UTF-8 (BOMなし) で書かれている場合、このスクリプトは文字化けを引き起こす。
UTF-8を扱う必要がある場合は、FSOではなく `ADODB.Stream` オブジェクトを使用するようにロジックを差し替えなければならない。これは「知っているか知らないか」で、ツールがゴミを生成するかどうかの分かれ目だ。

② ファイルロックと権限

自動化ツールが実行される際、対象のファイルが他のプロセス(例えば、設定を読み込んでいるアプリケーション自身)によってロックされている場合、`FSO.DeleteFile` や `MoveFile` は失敗する。
実務では、`On Error Resume Next` で握りつぶすのではなく、リトライ・ロジック(数秒待機して再試行)を組み込むのがプロの作法だ。

③ 冪等性(Idempotency)の確保

「同じパッチを2回適用しても、結果が同じになるか?」
優れた自動化スクリプトは、冪等性を備えている。今回の設計(Dictionaryへのロード)は、`ADD` と `SET` を同一視することで、自然と冪等性を確保している。これにより、スクリプトの再実行による二重登録などのバグを防いでいる。

結びに代えて

VBScriptは古い言語かもしれない。しかし、その低レイヤーな挙動を完全に掌握し、オブジェクトのライフサイクルを制御できれば、これほど強力で軽量な武器はない。

「動けばいい」コードを書く者は、いつかシステムを止める。
「失敗しても安全な設計」を書く者が、真のエンジニアだ。

君のコードに、その哲学が宿っていることを願う。

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