【引数解析の実践】WScript.Arguments.Named と Unnamed を使い分けたプロ仕様のコマンドラインパーサー構築
レガシーシステムの維持、あるいはWindows環境におけるインフラ自動化の現場において、VBScript(Visual Basic Scripting Edition)いまだにその生命力を失っていない。JScriptやPowerShell全盛の現在であっても、ランタイムの依存性が極めて低く、OS標準で瞬時に起動するWSH(Windows Script Host)の軽快さは、現場のエンジニアにとって強力な武器である。
しかし、業務自動化の規模が拡大するにつれ、避けて通れない課題が浮上する。それがコマンドライン引数の解析(パース処理)だ。
「とりあえず `WScript.Arguments(0)` を参照する」といったナイーブな実装は、引数の順序変更やオプションの追加に対して致命的に脆弱であり、バッチ処理のサイレントエラーや意図せぬファイル上書きといった重大インシデントを引き起こす。
本稿では、`WScript.Arguments.Named`(名前付き引数)と `WScript.Arguments.Unnamed`(位置引数)を厳格に使い分け、エンタープライズ環境に耐えうる「プロ仕様のコマンドラインパーサーモジュール」の構築法を解説する。
—
1. WSHアーキテクチャにおける引数構造の深層
VBScriptの引数処理を司る `WScript.Arguments` コレクションは、内部的に以下の2つの異なるアクセサへ分岐している。
1. `WScript.Arguments.Unnamed`: インデックスベース(0始まり)で順序依存の引数を格納する。主に「処理対象のファイルパス」など、スクリプトの実行に不可欠な位置引数に用いる。
2. `WScript.Arguments.Named`: `/key:value` または `-key:value` の形式で渡されるキーバリュー型の引数を格納する。大文字小文字を区別せず、主に「動作モード」「設定ファイルパス」「フラグ」などのオプションに用いる。
レガシー環境における罠
WSHのパーサーには、独自の癖が存在する。例えば、`/f` と指定した場合、`Named` コレクションにはキー `f` として格納されるが、値が省略された場合の挙動や、スラッシュ (`/`) とハイフン (`-`) の混在によるパース揺れなど、生データのままでは商用利用に耐えない。
メモリ管理の観点からも、COMオブジェクトである `WScript.Arguments` をループ処理する際は、参照のリークを防ぐため、適切なスコープ管理とオブジェクトの明示的な破棄(特にDictionary等と組み合わせる場合)が求められる。
—
2. プロ仕様コマンドラインパーサーの設計要件
エンタープライズ向けのパーサーには、以下の要件が不可欠である。
- 厳格な必須チェック: 必要な位置引数、または名前付き引数が欠けている場合、即座に処理を中断し、使用法(Usage)を表示して終了する。
- デフォルト値のフォールバック: オプションが省略された場合、安全なデフォルト値を適用する。
- 型安全性の担保(擬似): 文字列として渡された引数を、必要に応じて数値や真偽値へ変換・検証する。
- ヘルプ機能: `/h` または `/help` が指定された場合、自動的にドキュメントを出力する。
—
3. 実装コード:汎用引数解析モジュール (`CommandLineParser.vbs`)
以下のコードは、オブジェクト指向的なカプセル化の思想をVBScriptの `Class` 構文で極限まで表現した、再利用可能なコマンドラインパーサーの実装である。
‘ ==============================================================================
‘ 模块名: CommandLineParser.vbs
‘ 概要: Named/Unnamed引数を完全制御するプロ仕様のコマンドラインパーサー
‘ 著作権: Enterprise Automation Architecture
‘ ==============================================================================
Option Explicit
‘ メイン処理のシミュレーション(テスト実行用)
Call Main()
Sub Main()
Dim parser
Set parser = New ArgParser
‘ — 1. スキーマ(仕様)の定義 —
‘ 位置引数 (Unnamed) の定義: インデックス, 必須フラグ, 引数名
Call parser.AddUnnamedSchema(0, True, “InputFile”)
Call parser.AddUnnamedSchema(1, False, “OutputFile”)
‘ 名前付き引数 (Named) の定義: キー名, 必須フラグ, デフォルト値, 説明
Call parser.AddNamedSchema(“config”, False, “C:\Config\default.ini”, “設定ファイルのパス”)
Call parser.AddNamedSchema(“mode”, True, “”, “実行モード (batch/interactive)”)
Call parser.AddNamedSchema(“debug”, False, “false”, “デバッグモード有効化”)
‘ — 2. パース実行 —
If Not parser.Parse() Then
WScript.Echo parser.GetUsage()
WScript.Quit 1
End If
‘ — 3. パース済みデータの取得とビジネスロジックの実行 —
Dim inputFile, outputFile, configPath, mode, isDebug
inputFile = parser.GetUnnamed(0)
outputFile = parser.GetUnnamed(1)
configPath = parser.GetNamed(“config”)
mode = parser.GetNamed(“mode”)
isDebug = CBool(parser.GetNamed(“debug”))
‘ 実処理の開始(ログ出力)
WScript.Echo “[INFO] パース成功:”
WScript.Echo ” – 入力ファイル: ” & inputFile
WScript.Echo ” – 出力ファイル: ” & IIf(outputFile = “”, “(なし)”, outputFile)
WScript.Echo ” – 設定パス : ” & configPath
WScript.Echo ” – モード : ” & mode
WScript.Echo ” – デバッグ : ” & isDebug
‘ オブジェクトの解放
Set parser = Nothing
End Sub
‘ ==============================================================================
‘ クラス定義: ArgParser
‘ ==============================================================================
Class ArgParser
Private m_NamedSchemas
Private m_UnnamedSchemas
Private m_ParsedNamed
Private m_ParsedUnnamed
Private m_ScriptTitle
‘ コンストラクタ
Private Sub Class_Initialize()
Set m_NamedSchemas = CreateObject(“Scripting.Dictionary”)
Set m_UnnamedSchemas = CreateObject(“Scripting.Dictionary”)
Set m_ParsedNamed = CreateObject(“Scripting.Dictionary”)
Set m_ParsedUnnamed = CreateObject(“Scripting.Dictionary”)
m_ScriptTitle = WScript.ScriptName
End Sub
‘ デストラクタ(メモリ最適化のための明示的解放)
Private Sub Class_Terminate()
m_NamedSchemas.RemoveAll
m_UnnamedSchemas.RemoveAll
m_ParsedNamed.RemoveAll
m_ParsedUnnamed.RemoveAll
Set m_NamedSchemas = Nothing
Set m_UnnamedSchemas = Nothing
Set m_ParsedNamed = Nothing
Set m_ParsedUnnamed = Nothing
End Sub
‘ 名前付き引数のスキーマ登録
Public Sub AddNamedSchema(ByVal key, ByVal isRequired, ByVal defaultValue, ByVal description)
Dim schemaObj
Set schemaObj = CreateObject(“Scripting.Dictionary”)
schemaObj.Add “Required”, isRequired
schemaObj.Add “Default”, defaultValue
schemaObj.Add “Desc”, description
m_NamedSchemas.Add LCase(key), schemaObj
Set schemaObj = Nothing
End Sub
‘ 位置引数のスキーマ登録
Public Sub AddUnnamedSchema(ByVal index, ByVal isRequired, ByVal argName)
Dim schemaObj
Set schemaObj = CreateObject(“Scripting.Dictionary”)
schemaObj.Add “Required”, isRequired
schemaObj.Add “Name”, argName
m_UnnamedSchemas.Add index, schemaObj
Set schemaObj = Nothing
End Sub
‘ パース処理の実行
Public Function Parse()
Dim i, key, schema, val
‘ 1. ヘルプ要求のチェック (/h または /help)
If WScript.Arguments.Named.Exists(“h”) Or WScript.Arguments.Named.Exists(“help”) Then
Parse = False
Exit Function
End If
‘ 2. Named引数の解決
Dim keys, item
keys = m_NamedSchemas.Keys()
For Each key In keys
Set schema = m_NamedSchemas(key)
If WScript.Arguments.Named.Exists(key) Then
m_ParsedNamed.Add key, WScript.Arguments.Named(key)
Else
If schema(“Required”) Then
WScript.Echo “[ERROR] 必須の引数 ‘/” & key & “‘ が指定されていません。”
Parse = False
Exit Function
Else
m_ParsedNamed.Add key, schema(“Default”)
End If
End If
Set schema = Nothing
Next
‘ 3. Unnamed引数の解決
Dim unNamedCount
unNamedCount = WScript.Arguments.Unnamed.Count
keys = m_UnnamedSchemas.Keys()
For Each i In keys
Set schema = m_UnnamedSchemas(i)
If i < unNamedCount Then
m_ParsedUnnamed.Add i, WScript.Arguments.Unnamed(i)
Else
If schema("Required") Then
WScript.Echo "[ERROR] 必須の位置引数 '" & schema("Name") & "' (インデックス: " & i & ") が不足しています。"
Parse = False
Exit Function
Else
m_ParsedUnnamed.Add i, "" ' 任意の場合は空文字
End If
End If
Set schema = Nothing
Next
Parse = True
End Function
' パース済みNamed引数の取得
Public Function GetNamed(ByVal key)
Dim lKey: lKey = LCase(key)
If m_ParsedNamed.Exists(lKey) Then
GetNamed = m_ParsedNamed(lKey)
Else
GetNamed = ""
End If
End Function
' パース済みUnnamed引数の取得
Public Function GetUnnamed(ByVal index)
If m_ParsedUnnamed.Exists(index) Then
GetUnnamed = m_ParsedUnnamed(index)
Else
GetUnnamed = ""
End If
End Function
' 使用法(Usage)の自動生成
Public Function GetUsage()
Dim usageStr, key, schema, i
usageStr = vbCrLf & "使用法 (Usage):" & vbCrLf
usageStr = usageStr & " cscript.exe " & m_ScriptTitle
' Unnamedの構文表示
If m_UnnamedSchemas.Count > 0 Then
For i = 0 To m_UnnamedSchemas.Count – 1
Set schema = m_UnnamedSchemas(i)
If schema(“Required”) Then
usageStr = usageStr & ” <" & schema("Name") & ">”
Else
usageStr = usageStr & ” [” & schema(“Name”) & “]”
End If
Set schema = Nothing
Next
End If
‘ Namedの構文表示
If m_NamedSchemas.Count > 0 Then
usageStr = usageStr & ” [オプション…]”
End If
usageStr = usageStr & vbCrLf & vbCrLf & “引数の詳細:” & vbCrLf
‘ Unnamed詳細
If m_UnnamedSchemas.Count > 0 Then
usageStr = usageStr & ” [位置引数 (Unnamed)]” & vbCrLf
For i = 0 To m_UnnamedSchemas.Count – 1
Set schema = m_UnnamedSchemas(i)
usageStr = usageStr & ” ” & i & “: ” & schema(“Name”) & ” (” & IIf(schema(“Required”), “必須”, “任意”) & “)” & vbCrLf
Set schema = Nothing
Next
End If
‘ Named詳細
If m_NamedSchemas.Count > 0 Then
usageStr = usageStr & ” [名前付き引数 (Named)]” & vbCrLf
Dim keys
keys = m_NamedSchemas.Keys()
For Each key In keys
Set schema = m_NamedSchemas(key)
usageStr = usageStr & ” /” & key & “:<値> – ” & schema(“Desc”) & ” (” & IIf(schema(“Required”), “必須”, “デフォルト: ” & schema(“Default”)) & “)” & vbCrLf
Set schema = Nothing
Next
End If
GetUsage = usageStr
End Function
End Class
‘ 簡易三項演算子ヘルパー
Private Function IIf(ByVal expression, ByVal truePart, ByVal falsePart)
If expression Then
IIf = truePart
Else
IIf = falsePart
End If
End Function
—
4. コードの解説とアーキテクチャの極意
オブジェクトのライフサイクルとメモリ管理
VBScriptの `Dictionary` やカスタム `Class` は、スクリプト終了時に自動解放されるが、長期稼働するプロセスや、タスクスケジューラから数千回連続起動されるバッチ基盤においては、ガベージコレクションのタイミングに依存しない明示的な `Set obj = Nothing` の徹底がメモリリークを防ぐ唯一の盾となる。本コードでは `Class_Terminate` イベント内で確実にディクショナリの要素をクリアしている。
大文字小文字の正規化
WScriptの `Named` コレクションはキーの大文字小文字を区別しないが、保守性を高めるため、自作スキーマ側でも `LCase(key)` によってキーを強制的に小文字化し、呼び出し側のタイポや大文字混じりの入力を完全に吸収する堅牢性を実装している。
ヘルプドキュメントの動的生成
「コードがドキュメントである」という思想に基づき、スキーマ定義から自動的に `GetUsage()` が整形されるため、スクリプトの仕様変更とヘルプの乖離(ドキュメント腐敗)が構造的に発生しない設計となっている。
—
5. チーフアーキテクトからの提言
レガシー技術であるVBScriptであっても、設計思想に妥協を許さなければ、モダンな言語に匹敵する堅牢で再利用性の高いモジュールを構築できる。
現場のシステム管理やインフラ自動化において「動けばいい」というコードは、数ヶ月後に必ず負債となり、運用者を苦しめる。本稿で示した `Named` と `Unnamed` の厳格な分離とスキーマ駆動型のパース手法を武器に、あなたの管理するWindows環境の自動化基盤をプロフェッショナルな領域へと引き上げてほしい。
