VBScriptリファレンス補遺
Windows管理の仕事が入ってきそうで今年に入ってからVBScriptのことをいろいろと調べてみた。Microsoftが出しているここ(http://www.microsoft.com/japan/msdn/scripting/)のリファレンスでほとんど事足りるのだけれど、リファレンスの目立たない箇所に記載されていたりあるいは記述がなかったりするマイナーな機能もあるので、補足してみる。
予約語を変数名や関数名として使う
[]で括るとClassやPropertyのような予約語を変数名や関数名として使える。リファレンスには記載なし。もともとVisual Basicの機能らしい。
Classで自分自身を参照する
JavaやC++ならthis、Rubyならself、PerlやPythonならメソッドの第1引数だが、VBScriptではMeというキーワードを使う。これももともとVisual Basicの機能らしい。割と重要な機能だと思うのだが、なぜかリファレンスには記載なし。
正規表現の複数行マッチ
正規表現のMultilineプロパティをTrueに設定すると、一度に複数行にマッチさせることができる。リファレンスではなぜかJScriptの正規表現の説明にしか記載されてないが、VBScriptでも使える。
GetRef関数は手続きオブジェクトを返す
リファレンスの説明では「イベントとバインドできるプロシージャに対する参照を返します。」と書かれていてDOMのイベント専用に思えるが、実は汎用的に使える手続きオブジェクトを返す便利な関数。手続きオブジェクトはFunctionプロシージャやSubプロシージャのように実行できるオブジェクト。
手続きオブジェクトのクラス定義
パブリックメソッド(Function, Sub)の定義でDefault宣言すると、オブジェクトをプロシージャのように実行できるようになる。当然、プロシージャの呼び出しでDefault宣言したメソッドを実行する。
配列オブジェクトのクラス定義
配列のように添え字でアクセスできるオブジェクトは、引数付きのプロパティをDefault宣言すると良い。Default宣言はGetプロパティのみに付ける。Let,SetプロパティにDefaultを付ける必要はない。
バイナリ用の関数
ほとんどの文字列用のVBScript組み込み関数には、対となるバイナリ用の関数が用意されている。バイナリ用の関数名は文字列用の関数名の最後にBが付いている。
VBScript感想
ここ1ヶ月半ほどVBScriptを使い込んでみての感想だが、思った以上に使える筋の良い言語だと思う。こんな便利な言語がWindows標準でついてきてたなんて、知らなくて損してたと思ったくらい。
以下、VBScriptのイケてるところ。
- シンプルな文法。スクリプトという用途に合わせて潔く必要な機能のみに絞ったと思われ、自分はこの選択になかなかセンスを感じる。
- Windows標準。何もインストールしてない素のWindowsでも問題なく使える。他の言語にない大きな利点。
- COM・ADSI・WMI等のWindowsの持つ機能に容易にアクセスできる。ドキュメントが少ないのがたまにきずだが。
- Duck Typing。VBScriptのクラスは継承ができないが、Duck Typingのおかげでほとんど困ることはない。
- GetRefやクラス定義のDefault宣言で、手続き型のオブジェクトを作れる。
- Evalが使える。VBScriptは式と文を区別するのでExecuteと使い分けないといけないのがちょっと面倒だけど。上の手続きオブジェクトと合わせて、これがあればクロージャがなくても結構なんとかなる。
以下、VBScriptの微妙なところ。
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とはRubyやPerlのハッシュテーブルに相当するオブジェクトで便利なのだが、VBScriptではいちいちCreateObjectで作って変数に代入してからキーと値を入れるのでどうしても記述が冗長で、RubyやPerlがリテラルで手軽にハッシュを作れるのと比較して今一歩使い勝手が劣る。
そこでインラインで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で配列長を宣言し直す必要があって面倒だ。RubyやPerlなら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>
今日はここまで。