【VBScript】使える時に使いたいVBScript(WSH)のコード

# 使える時に使いたいVBScript(WSH)のコード

令和の時代、次から次へと新しい言語やフレームワークが出てきます。ひとつの言語を抱きつづける時代ではないからこそ、サブの言語として、SDKや開発ツールのセットアップが要らずに大抵のWindows環境で動作しテキストエディタ一本で開発できるVBScriptは如何でしょうか。

とは言え、今さら勉強するのも、覚えるのも、また思い出すのも面倒くさいのは事実。
予め自分がよく使う機能については、テンプレートは用意しておいた方がいいでしょう。
というわけで、ここではまさに私が良く使う機能群を備忘として残しています。

基本構文が分かる人レベルに向いて書いてあります。初級者の人は
https://garafu.blogspot.com/2018/09/vbscript-basic.html
https://qiita.com/mitsuru793/items/319409059d31aba70158
https://www.kanaya440.com/contents/script/
こういったところが助けになると思います。

[2023/11/05 追記]
2023年10月、MicrosoftからWindows上で動くVBScriptが非推奨になると発表されました。
ということで、VBScriptはますます本腰を入れて覚える言語ではなくなりました。
便利に使える間だけ使っていきましょう。

## 基本テンプレ

基本的なとこを詰め込んだ起動から終了までのテンプレートです。

~~~VB
Option Explicit
Dim WSO : Set WSO = WScript.CreateObject("WScript.Shell")
Dim FSO : Set FSO = WScript.CreateObject("Scripting.FileSystemObject")
WSO.CurrentDirectory = FSO.getParentFolderName(WScript.ScriptFullName)

Dim arg1, arg2, arg3
If (WScript.Arguments.Count() > 0) Then arg1 = WScript.Arguments(0)
If (WScript.Arguments.Count() > 1) Then arg2 = WScript.Arguments(1)
If (WScript.Arguments.Count() > 2) Then arg3 = WScript.Arguments(2)

Call main(arg1, arg2, arg3)

Set FSO = Nothing
Set WSO = Nothing
WScript.Quit
'---------------------------------------------------------------
Sub main (arg1, arg2, arg3)
    WScript.Echo(" Hello World ! " &  arg1 & "!")
End Sub
'---------------------------------------------------------------
~~~
* エラー除けのため変数宣言を必須化
* よく使う`WScript.Shell`と`Scripting.FileSystemObject`のグローバルオブジェクトの作成廃棄
* 引数は、とりあえず3引数に対応させました。(当然、毎回、修正する前提です)
* カレントディレクトリをスクリプトファイルの位置へ移動するように

GUIでダブルクリック起動すると、カレントディレクトリがSystem32とかになるのでたいがい書き込めませんし、書きこむべきではありません。逆にCUI起動ならカレントディレクトリはそちらで用意するのが普通なので外してしまっても良いとは思います。

## デバッグログ出力

~~~VB
Sub printDebug (msg)
    If LCASE(RIGHT(WScript.FullName, 11) ) = "cscript.exe" Then WScript.Echo(TIME & " " & msg)
End Sub

Sub printInfo (msg)
    WScript.Echo(msg)
End Sub
~~~
と言いますか、GUI・CUIの判定する構文。
vbsファイルはCUI上では`cscript.exe`で起動させるのが一般的ですが、GUI上でダブルクリック・ファイルドロップによって起動するアプリは`wscript.exe`です。
これらはほぼ同じように動きますが、違いもあり、`WScript.Echo`についてはCUIだと標準出力して処理が続きますが、GUIではメッセージダイアログによる表示を行い、OKを押すまで処理を止めます。

CUIで動作させる前提で多数のEchoを用いていると、そのファイルをうっかりダブルクリック起動してしまうとダイアログ地獄が始まります。そのため、些細な情報はCUI起動の時のみ出力したいところ。
PrintInfoはまったく意味のない関数ですが何となく対にしたかったので記載。

## 現在日時と時刻の取得

~~~VB
Function getDateStr
    getDateStr = CSTR(YEAR(DATE)*10000 + MONTH(DATE)*100 + DAY(DATE))
End Function

Function getTimeStr
    getTimeStr = RIGHT("0" & CSTR(HOUR(TIME)*10000 + MINUTE(TIME)*100 + SECOND(TIME)),6)
End Function
~~~
YYYYMMDDと、hhmmssですね。もちろん、文字列結合で作っても良いです。

## ファイル名の操作

~~~vb
Function getOutputFilename(filename)
    Dim sfile : sfile = FSO.getAbsolutePathName(filename)
    getOutputFilename = FSO.getParentFolderName(sfile) & "\" _
                      & FSO.GetBaseName(sfile) _
                      & "." & FSO.GetExtensionName(sfile)
End Function
~~~
上から順に、フルパス化、フォルダ名、拡張子無しファイル名、拡張子。
このままだと元のファイル名を復元するだけなので、必要な接頭語・接尾語を付けてください。
FSOは上のテンプレートでグローバル宣言済。

## テキストファイルの入出力

### Scripting.FileSystemObject
~~~vb
Function loadText(filename)
    Dim file : Set file = FSO.OpenTextFile(filename, 1 )
    Dim text : text = file.ReadAll
    file.Close
    loadText = text
	Set file = Nothing
End Function

Function saveText(filename, text)
    saveText = false
    Dim file : Set file = FSO.OpenTextFile(filename, 2, true)
    file.Write(text)
    file.Close
    Set file = Nothing
    saveText = true
End Function
~~~

`saveText "copy.txt",loadText("base.txt")`
何処にでもみつかるFileSystemObjectでのサンプル。FSOは上のテンプレートでグローバル宣言済。
saveの方のOpenTextFileの2を8にすると追記モードになります。それ以外はマニュアルを。

### ADODB.Stream
~~~vb
'charCode : "Shift_JIS","UTF-8","unicode","euc-jp"

Function loadTextWithEncoding(filename, charCode)
    Dim stream : Set stream = CreateObject("ADODB.Stream")
    Dim lines()
    stream.Type = 2 ' adTypeText
    stream.Charset = charCode 
    stream.Open
    stream.LoadFromFile filename
    Dim iCount : iCount = 0
    Do Until stream.EOS
        ReDim Preserve lines(iCount)
        lines(iCount) = stream.ReadText(-2)
        iCount = iCount + 1
    Loop
    stream.Close
    Set stream = Nothing
    loadTextWithEncoding = JOIN(lines, vbCrLf)
End Function

Function saveTextWithEncoding(filename, text, charCode)
    saveTextWithEncoding = false
    Dim stream : Set stream = CreateObject("ADODB.Stream")
    stream.Type = 2 ' adTypeText
    stream.Charset = charCode
    stream.Open
    stream.WriteText text, 0
    stream.SaveToFile filename, 2 ' adSaveCreateOverWrite
    stream.Close
    Set stream = Nothing
    saveTextWithEncoding = true
End Function
~~~

`saveTextWithEncoding "utf8.txt",loadTextWithEncoding("sjis.txt","Shift_JIS"),"UTF-8"`

FileSystemObjectの場合、扱える文字コードが限られるので、UTF-8を使いたい場合はADODBを使います。
ADODBでもShift JISが行けるので、こちらに統一してもいいのかもしれません。
ただし、Shift JISの読み込みについてはFileSystemObjectの方が速い事を付記しておきます。

### 読み込み速度について

大きいテキストファイルとして、日本郵便から12Mの12万件の郵便番号CSVをダウンロードして処理してみたところ、FileSystemObjectのReadAllが一瞬で終わるのに対して、ADODBでのReadAllには20秒くらいかかりました。これは少し掛かり過ぎだと思ったので、こちらの`loadTextWithEncoding`では、1行ずつReadLineにして結合する高速化(後述)を実施済みです。これなら2秒くらいで読めました。
（これで早くなるところから考えてADODBのReadAllが遅いというより文字列への格納が遅いのでしょうか？）
ちなみに、書き込みの方はFSO、ADODBともに一瞬です。

## 高速文字列結合

### System.Text.StringBuilder
~~~vb
    Dim builder : Set builder = CreateObject("System.Text.StringBuilder")
    builder.Append_3 "ほげ"
    builder.Append_3 "はげ"
    WScript.Echo builder.ToString
    Set builder = nothing
~~~
単純な文字列結合`s = s & "ほげ"`の構文は、1回や10回なら良いんですが、サイズと回数が大きくなると目に見えて処理が重くなるのは周知のとおり。しかし、VBSには`StringBuilder`が無いので‥‥どうするかというところですが、
実は、.Net Frameworkのものを借りて使用することができます。
ただし、VBSは引数違いの同名メソッドが定義できない関係か、引数違いには番号が付いています。`Append_3`がString１つを受け取るものです。使うのはこれ一つで十分でしょう。

なお、System.Text以外の.Net Frameworkの機能も呼ぼうと思えば呼べます。しかし、言語も用途も違うもので実際に有意に使えるものはごくわずかなようです。

### JOIN関数
~~~vb
    Dim i
    Dim buf(), iBuf : iBuf = 0
    For i=0 To 100
        Redim Preserve buf(iBuf)
        buf(iBuf) = CSTR(i) & ","
        iBuf=iBuf+1
    Next
    WScript.Echo JOIN(buf, "")
~~~

~~~vb
    Dim i,size : size = 100
    Dim buf(): Redim buf(size-1)
    For i=0 To size-1
        buf(i) = CSTR(i) & ","
    Next
    WScript.Echo JOIN(buf, "")
~~~
紹介しておいてなんですが、借り物の`StringBuilder`より速いのがJOIN関数です。
わざわざ結合用に新たな文字列配列を作ってさえ`StringBuilder`より早い位で、もちろん単純な結合とは比較になりません。

配列の長さを変えるには`Redim`。中身を捨てずに拡張するのは`Redim Preserve`を使います。
上の例は結合数が分からないケース。下の例は結合数が事前に分かっているケースです。分かっている事の方が多いでしょう。
下の方がRedim１回で済むので見た目もスッキリしますね

なお、上の例では1回結合するたびにRedimしていますが、例えば1000個ずつ配列を拡げるような書き方もできます。
もっとも、それで脅威的に速くなることも無かったというか‥‥まぁ元から速かったです。

## テキストデータを1行ずつ処理する

~~~vb
Function convertText(inputText)
    Dim SEP  : SEP = vbCrLf
    Dim lines : lines = SPLIT(inputText, SEP)
    Dim i
    For i=0 To UBOUND(lines)
        lines(i) = translate(lines(i))
    Next
    convertText = JOIN(lines, SEP)
End Function

Function translate(line)
    translate = REPLACE(line, "hoge", "hage")
End Function
~~~
お作法としては、入力元と出力先を両方オープンして1行ずつ読込・1行ずつ処理・1行ずつ書込するのがエコです。
ただ、小さいテキストファイルであれば、全行取得して、`SPLIT`で配列化して・それぞれに処理・`JOIN`して文字列に戻す、ほうがソースはすっきりします。
速度が気になる所ですが、12Mの12万件の郵便番号CSVでもこの処理には１秒かからないくらいなので、たいていのファイルは処理ができる(待てる)と思います。むしろ、前述のとおり、読み込みの方に時間が掛かるのでそちらの方が懸念事項。

また、行を削除する、抽出するなどの場合はlinesの行数が変わりますから、そういった状況では結合の方はもう少し書き方を変える必要があると思います。出力用の文字列配列を作るとか‥。

## ウェイトを入れる

~~~vb
WScript.Sleep 100
~~~

単位はミリ秒！

## DOSコマンドを実行する

WScript.Shellを使えばDOSコマンドを実行することができます。
DOSバッチでいいじゃん、なのですが、VBSはいくぶん文字列操作が上手いので
パラメータをゴリゴリに自動生成したい場合とかはこういうのも有りな気がします。

~~~vb
    'Dim WSO : Set WSO = WScript.CreateObject("WScript.Shell")
    Dim cmd
    cmd = "echo てすと >> text.txt"
    WSO.Run "cmd /c " & cmd , 0, true
    'Set WSO = Nothing
~~~

WSOオブジェクトは冒頭の起動ロジックでグローバルで宣言しています。1個あれば十分かと。
`Run`コマンドの2番目の引数はDOSのあの黒い画面を出すか出さないか。0なら非表示です。
3番目のフラグは同期／非同期で、実行が終わるまで待つならtrue、待たないならfalseです。
ちなみに`Run`じゃなくて`Exec`を使うと標準出力が取れたりしますけど、扱いにくいので、いったんファイルに吐いてからそのファイルを取込む形の方が楽な気がします。証跡にもなりますし。

### echoコマンドを実行する

逆に、DOSの方が楽な機能もあって、例えば、上例のように
DOSコマンドで1行のログを出すなら`cmd /c echo なんとか >> かんとか`で済むことがあります。
まぁ、汎用性を持たせるとエスケープが面倒ですが。

~~~vb
Sub printLog (msg, logfile)
    msg = REPLACE(msg, vbCrLf, " ")
    msg = REPLACE(msg, vbLf, " ")
    msg = REPLACE(msg, vbCr, " ")
    msg = REPLACE(msg, "^", "^^")
    msg = REPLACE(msg, "<", "^<")
    msg = REPLACE(msg, ">", "^>")
    msg = REPLACE(msg, "&", "^&")
    msg = REPLACE(msg, "%", "^%")
    msg = "[" & DATE & " " & TIME & "] " & msg
    WSO.Run "cmd /c echo " & msg & " >> """& logfile &"""", 0, true
End Sub
~~~

エスケープはechoの仕様からです。

----

あと必要なときに加筆予定

----
独学のため正確でない可能性があります。
（っ・ｘ・）っ きゅ