使える時に使いたい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はますます本腰を入れて覚える言語ではなくなりました。
便利に使える間だけ使っていきましょう。
基本テンプレ
基本的なとこを詰め込んだ起動から終了までのテンプレートです。
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起動ならカレントディレクトリはそちらで用意するのが普通なので外してしまっても良いとは思います。
デバッグログ出力
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はまったく意味のない関数ですが何となく対にしたかったので記載。
現在日時と時刻の取得
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ですね。もちろん、文字列結合で作っても良いです。
ファイル名の操作
Function getOutputFilename(filename)
Dim sfile : sfile = FSO.getAbsolutePathName(filename)
getOutputFilename = FSO.getParentFolderName(sfile) & "\" _
& FSO.GetBaseName(sfile) _
& "." & FSO.GetExtensionName(sfile)
End Function
上から順に、フルパス化、フォルダ名、拡張子無しファイル名、拡張子。
このままだと元のファイル名を復元するだけなので、必要な接頭語・接尾語を付けてください。
FSOは上のテンプレートでグローバル宣言済。
テキストファイルの入出力
Scripting.FileSystemObject
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
'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
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
がString1つを受け取るものです。使うのはこれ一つで十分でしょう。
なお、System.Text以外の.Net Frameworkの機能も呼ぼうと思えば呼べます。しかし、言語も用途も違うもので実際に有意に使えるものはごくわずかなようです。
JOIN関数
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, "")
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
を使います。
上の例は結合数が分からないケース。下の例は結合数が事前に分かっているケースです。分かっている事の方が多いでしょう。
下の方がRedim1回で済むので見た目もスッキリしますね
なお、上の例では1回結合するたびにRedimしていますが、例えば1000個ずつ配列を拡げるような書き方もできます。
もっとも、それで脅威的に速くなることも無かったというか‥‥まぁ元から速かったです。
テキストデータを1行ずつ処理する
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でもこの処理には1秒かからないくらいなので、たいていのファイルは処理ができる(待てる)と思います。むしろ、前述のとおり、読み込みの方に時間が掛かるのでそちらの方が懸念事項。
また、行を削除する、抽出するなどの場合はlinesの行数が変わりますから、そういった状況では結合の方はもう少し書き方を変える必要があると思います。出力用の文字列配列を作るとか‥。
ウェイトを入れる
WScript.Sleep 100
単位はミリ秒!
DOSコマンドを実行する
WScript.Shellを使えばDOSコマンドを実行することができます。
DOSバッチでいいじゃん、なのですが、VBSはいくぶん文字列操作が上手いので
パラメータをゴリゴリに自動生成したい場合とかはこういうのも有りな気がします。
'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 なんとか >> かんとか
で済むことがあります。
まぁ、汎用性を持たせるとエスケープが面倒ですが。
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の仕様からです。
あと必要なときに加筆予定
独学のため正確でない可能性があります。
(っ・x・)っ きゅ