VBScriptリファレンス補遺

Windows管理の仕事が入ってきそうで今年に入ってからVBScriptのことをいろいろと調べてみた。Microsoftが出しているここ(http://www.microsoft.com/japan/msdn/scripting/)のリファレンスでほとんど事足りるのだけれど、リファレンスの目立たない箇所に記載されていたりあるいは記述がなかったりするマイナーな機能もあるので、補足してみる。

予約語を変数名や関数名として使う

[]で括るとClassやPropertyのような予約語を変数名や関数名として使える。リファレンスには記載なし。もともとVisual Basicの機能らしい。

Classで自分自身を参照する

JavaC++ならthis、Rubyならself、PerlPythonならメソッドの第1引数だが、VBScriptではMeというキーワードを使う。これももともとVisual Basicの機能らしい。割と重要な機能だと思うのだが、なぜかリファレンスには記載なし。

正規表現の複数行マッチ

正規表現のMultilineプロパティをTrueに設定すると、一度に複数行にマッチさせることができる。リファレンスではなぜかJScript正規表現の説明にしか記載されてないが、VBScriptでも使える。

GetRef関数は手続きオブジェクトを返す

リファレンスの説明では「イベントとバインドできるプロシージャに対する参照を返します。」と書かれていてDOMのイベント専用に思えるが、実は汎用的に使える手続きオブジェクトを返す便利な関数。手続きオブジェクトはFunctionプロシージャやSubプロシージャのように実行できるオブジェクト。

VBScript組み込みの関数はGetRefを使えない

なのでGetRefするためにはわざわざ元のVBScript関数を呼ぶだけのプロシージャを定義してやらないといけない。

手続きオブジェクトのクラス定義

パブリックメソッド(Function, Sub)の定義でDefault宣言すると、オブジェクトをプロシージャのように実行できるようになる。当然、プロシージャの呼び出しでDefault宣言したメソッドを実行する。

配列オブジェクトのクラス定義

配列のように添え字でアクセスできるオブジェクトは、引数付きのプロパティをDefault宣言すると良い。Default宣言はGetプロパティのみに付ける。Let,SetプロパティにDefaultを付ける必要はない。

バイナリ用の関数

ほとんどの文字列用のVBScript組み込み関数には、対となるバイナリ用の関数が用意されている。バイナリ用の関数名は文字列用の関数名の最後にBが付いている。

日付(時刻)リテラル

#ではさむと日付や時刻のリテラルになる。VBScriptの定数の説明にひっそりと書かれている。

VBScript感想

ここ1ヶ月半ほどVBScriptを使い込んでみての感想だが、思った以上に使える筋の良い言語だと思う。こんな便利な言語がWindows標準でついてきてたなんて、知らなくて損してたと思ったくらい。

以下、VBScriptのイケてるところ。

  • シンプルな文法。スクリプトという用途に合わせて潔く必要な機能のみに絞ったと思われ、自分はこの選択になかなかセンスを感じる。
  • Windows標準。何もインストールしてない素のWindowsでも問題なく使える。他の言語にない大きな利点。
  • COM・ADSI・WMI等のWindowsの持つ機能に容易にアクセスできる。ドキュメントが少ないのがたまにきずだが。
  • Duck Typing。VBScriptのクラスは継承ができないが、Duck Typingのおかげでほとんど困ることはない。
  • GetRefやクラス定義のDefault宣言で、手続き型のオブジェクトを作れる。
  • Evalが使える。VBScriptは式と文を区別するのでExecuteと使い分けないといけないのがちょっと面倒だけど。上の手続きオブジェクトと合わせて、これがあればクロージャがなくても結構なんとかなる。

以下、VBScriptの微妙なところ。

  • ループ抽象が中途半端。For Each構文で配列やCOMオブジェクトをループでまわせるが、ユーザ定義のクラスは無理。かゆいところに手が届かない。
  • プロシージャの呼び出しがおそい、ような、気が、する。まじめに測ってないからなんとなくだけど。
  • 組み込みで提供されいている機能がプリミティブすぎて、記述が冗長になる。これはライブラリを作りこむことである程度解消できるので、自分はあんまり気にしてない。
  • バックトレースが無い。デバッグで時々てまどることがあってちょっと困る。
  • クロージャが無い。GetRefとEvalの組み合わせでなんとかするしかない。

VBScript小技集

上でも書いたが、VBScriptは組み込みの機能がプリミティブ過ぎて記述が冗長になるきらいがあので、自分はライブラリを作って補っている。ちなみにライブラリはGoogle Codeで公開している。URLは以下参照。

http://code.google.com/p/vbslib/

この中から気が向いたものを徐々に紹介していこうと思う。

変数の代入

VBScriptはなぜか数値・文字列等とオブジェクトとで、変数へ代入する構文が異なるので使い分けが必要だ。オブジェクトを代入する場合だけSetが必要になる。通常にプログラミングする場合はそれぞれ使う場所がおのずと決まるので問題ないが(数値を入れてる場所にいきなりオブジェクトを入れたりとか、その逆とか、普通はないよね、まともにプログラミングしてれば)、ライブラリを作る場合はこれでは困ることがある。コンテナクラスなんかを作る場合は、数値でもオブジェクトでも何でも取り扱えなければ意味がない。そういう場合はIsObject関数で判定して場合わけするのだがいちいち面倒だ。
というわけでライブラリ化したのが以下のコード。VBScriptは引数がデフォルトByRefなのがミソ。

Sub Bind(toStore, value)
  If IsObject(value) Then
    Set toStore = value
  Else
    toStore = value
  End If
End Sub

Sub BindAt(keyValueStore, key, value)
  If IsObject(value) Then
    Set keyValueStore(key) = value
  Else
    keyValueStore(key) = value
  End If
End Sub

Dictionaryのインライン定義

DictionaryとはRubyPerlのハッシュテーブルに相当するオブジェクトで便利なのだが、VBScriptではいちいちCreateObjectで作って変数に代入してからキーと値を入れるのでどうしても記述が冗長で、RubyPerlリテラルで手軽にハッシュを作れるのと比較して今一歩使い勝手が劣る。
そこでインラインでDictionaryを定義する関数を作ってみた。VBScriptではユーザ定義の可変長引数が使えないので、D(Array(k1,v1,k2,v2,...))のように使う。

Function Dictionary(keyValueList)
  Dim dict
  Set dict = CreateObject("Scripting.Dictionary")

  Dim isKey, key, i
  isKey = True

  For Each i In keyValueList
    If isKey Then
      Bind key, i
      dict.Add key, Empty
    Else
      BindAt dict, key, i
    End If
    isKey = Not isKey
  Next

  Set Dictionary = dict
End Function

' shortcut
Function D(keyValueList)
  Set D = Dictionary(keyValueList)
End Function

正規表現のインライン定義

正規表現もNewした後にPatternやオプションを設定しないと使えないので、記述がまわりくどい。VBScriptのオブジェクトは一般に引数付きのコンストラクタが無いので、作った後にセットアップの処理が続くため、記述が冗長になる傾向があるようだ。
正規表現はよく使うのでインライン定義の関数を作った。re("foo", "i")のように使う。

Function re(regexpPattern, regexpOptions)
  Dim regex, reOpts
  Set regex = New RegExp
  regex.Pattern = regexpPattern
  reOpts = LCase(regexpOptions)
  If InStr(reOpts, "i") > 0 Then
    regex.IgnoreCase = True
  End If
  If InStr(reOpts, "g") > 0 Then
    regex.Global = True
  End If
  If InStr(reOpts, "m") > 0 Then
    regex.Multiline = True
  End If
  Set re = regex
End Function

拡張可能な配列

VBScriptの動的配列は一応拡張可能なのだが、いちいちReDim Preserveで配列長を宣言し直す必要があって面倒だ。RubyPerlならpushするだけでいいのに。
ないなら作ってしまえということで作った。可変長配列の実態は横着してDictionaryになっている。だって可変長配列の管理やチューニングってめんどくさいし、VBScriptで速度にこだわっても無意味な気がしたから。
メソッドやプロパティの定義はDictionaryオブジェクトに準拠している。VBScriptの欠点でユーザ定義のクラスではFor Eachでループをまわせないので、For EachするときはlistBuf.ItemsのようにItemsメソッドを呼んで配列に変換する必要がある。

Class ListBuffer
  Private ivar_dict

  Private Sub Class_Initialize
    Set ivar_dict = CreateObject("Scripting.Dictionary")
  End Sub

  Public Property Get Count
    Count = ivar_dict.Count
  End Property

  Public Default Property Get Item(index)
    If ivar_dict.Exists(index) Then
      Bind Item, ivar_dict(index)
    Else
      Err.Raise 9, "stdlib.vbs:ListBuffer.Item(Get)", "out of range."
    End If
  End Property

  Public Property Let Item(index, value)
    If ivar_dict.Exists(index) Then
      ivar_dict(index) = value
    Else
      Err.Raise 9, "stdlib.vbs:ListBuffer.Item(Let)", "out of range."
    End If
  End Property

  Public Property Set Item(index, value)
    If ivar_dict.Exists(index) Then
      Set ivar_dict(index) = value
    Else
      Err.Raise 9, "stdlib.vbs:ListBuffer.Item(Set)", "out of range."
    End If
  End Property

  Public Property Get LastItem
    If ivar_dict.Count > 0 Then
      Bind LastItem, ivar_dict(ivar_dict.Count - 1)
    End If
  End Property

  Public Sub Add(value)
    Dim nextIndex
    nextIndex = ivar_dict.Count
    ivar_dict.Add nextIndex, value
  End Sub

  Public Sub Append(list)
    Dim i
    For Each i In list
      Add i
    Next
  End Sub

  Public Function Exists(index)
    Exists = ivar_dict.Exists(index)
  End Function

  Public Function Items
    ReDim itemList(ivar_dict.Count - 1)
    Dim i
    For i = 0 To ivar_dict.Count - 1
      BindAt itemList, i, ivar_dict(i)
    Next
    Items = itemList
  End Function

  Public Sub RemoveAll
    ivar_dict.RemoveAll
  End Sub

  Public Sub RemoveLastItem
    If ivar_dict.Count > 0 Then
      ivar_dict.Remove ivar_dict.Count - 1
    Else
      Err.Raise 9, "stdlib.vbs:ListBuffer.RemoveLastItem", "no item to remove."
    End If
  End Sub
End Class

VBScriptでinspect

Rubyにはpやinspect、PerlにはData::Dumperのように、数値・文字列やオブジェクトの中身を可読な表示に変換することができる。これがあるのとないのとでは、デバッグの効率が段違いだ。
そこでVBScript用におんなじようなのを作ってみた。ただしVBScriptでは一般にオブジェクトの中身を見れないので、配列やディクショナリのようなオブジェクトだったら中身を表示して、そうでなかったらVarTypeでオブジェクトのクラスを表示することにして妥協した。

Dim ShowString_Quote
Set ShowString_Quote = re("""", "g")

Function ShowString(value)
  ShowString = """" & ShowString_Quote.Replace(value, """""") & """"
End Function

Function ShowArray(value)
  Dim showList, i
  Set showList = New ListBuffer
  For Each i In value
    showList.Add ShowValue(i)
  Next
  ShowArray = "[" & Join(showList.Items, ",") & "]"
End Function

Function ShowDictionary(value)
  Dim showList, k
  Set showList = New ListBuffer
  For Each k In value.Keys
    showList.Add ShowValue(k) & "=>" & ShowValue(value(k))
  Next
  ShowDictionary = "{" & Join(showList.Items, ",") & "}"
End Function

Function ShowObject(value)
  Dim r
  Err.Clear
  On Error Resume Next
  r = ShowDictionary(value)
  If Err.Number <> 0 Then
    Err.Clear
    r = ShowArray(value)
  End If
  If Err.Number <> 0 Then
    Err.Clear
    r = ShowArray(value.Items)
  End If
  If Err.Number <> 0 Then
    Err.Clear
    r = "<" & TypeName(value) & ">"
  End If
  ShowObject = r
End Function

Function ShowOther(value)
  Dim r
  Err.Clear
  On Error Resume Next
  r = CStr(value)
  If Err.Number <> 0 Then
    Err.Clear
    r = ShowArray(value)
  End If
  If Err.Number <> 0 Then
    Err.Clear
    r = ShowDictionary(value)
  End If
  If Err.Number <> 0 Then
    Err.Clear
    r = ShowUnknown(value)
  End If
  ShowOther = r
End Function

Function ShowUnknown(value)
  ShowUnknown = "<unknown:" & VarType(value) & " " & TypeName(value) & ">"
End Function

Function ShowValue(value)
  Dim r
  Err.Clear
  On Error Resume Next

  If VarType(value) = vbString Then
    r = ShowString(value)
  ElseIf IsArray(value) Then
    r = ShowArray(value)
  ElseIf IsObject(value) Then
    r = ShowObject(value)
  ElseIf IsEmpty(value) Then
    r = "<empty>"
  ElseIf IsNull(value) Then
    r = "<null>"
  Else
    r = ShowOther(value)
  End If

  If Err.Number <> 0 Then
    Err.Clear
    r = ShowUnknown(value)
  End If

  ShowValue = r
End Function

以下、実行例。

ShowValue(1)
=> 1
ShowValue("Hello world.")
=> "Hello world."
ShowValue(#2009-03-07#)
=> 2009/03/07
ShowValue(Array("foo", "bar", "baz"))
=> ["foo","bar","baz"]
D(Array("foo", 1, "bar", Nothing, "baz", Empty))
=> {"foo"=>1,"bar"=><Nothing>,"baz"=><empty>}
ShowValue(re("foo", "i"))
=> <IRegExp2>

今日はここまで。