JScriptからVBScriptの関数を使う

# 概要

VBScriptの組み込み関数はJScriptから使用することができません。

数値処理やキャストはJScript側でどうにかなりますし、有名なところでは`MsgBox`が`Popup`で代用できますが、`InputBox`や`RGB`関数などは代替手段がなく、利用したいシーンもそこそこ考えられます。

従来、JScriptとVBScriptの相互運用には、ScriptControlかWSFファイルが用いられていました。前者は64bit環境では動作しませんし、後者は...なんだか...個人的にスマートな感じがせず好きではないです。

今回紹介する内容では、`*.js`ファイル1つで完結するようにまとめています。

# 解説

VBScriptとJScriptから同時に触れるオブジェクトに対して、VBScript側でプロパティを生やして、JScriptから叩くことを考えます。

## スクリプトを2つ使う

まずは2つのファイルに分けて、わかりやすい状態で説明します。

VBScript側のコードがこちらです。JScript側で関数を使う前に実行します。

```vb
With CreateObject("Shell.Application").Windows().Item()
    .PutProperty "Server", Me
End With

Dim called: called = False
Do While Not called
    WScript.Sleep 1
Loop

' 組み込まれているInputBox関数はオプション引数を省略できるが、
' VBScriptで定義した関数については省略できないという仕様につき、このようになっています。
' （InputBoxを直接GetRefにかけることはできません。）
Function InputBox_1(prompt)
    InputBox_1 = InputBox(prompt)
    called = True
End Function
Function InputBox_2(prompt, title)
    InputBox_2 = InputBox(prompt, title)
    called = True
End Function
Function InputBox_3(prompt, title, default)
    InputBox_3 = InputBox(prompt, title, default)
    called = True
End Function
Function InputBox_4(prompt, title, default, xpos)
    InputBox_4 = InputBox(prompt, title, default, xpos)
    called = True
End Function
Function InputBox_5(prompt, title, default, xpos, ypos)
    InputBox_5 = InputBox(prompt, title, default, xpos, ypos)
    called = True
End Function
Function InputBox_7(prompt, title, default, xpos, ypos, helpfile, context)
    InputBox_7 = InputBox(prompt, title, default, xpos, ypos, helpfile, context)
    called = True
End Function
```

ShellWindowsオブジェクトのインデックス0に、"Server"という名前のプロパティを生やします。キーを列挙すると、こんな感じで公開されていることがわかります。

```
WScript
WSH
InputBox_1
InputBox_2
InputBox_3
InputBox_4
InputBox_5
InputBox_7
called
```

VBScriptの実行が終了すると参照は無効になるので、呼び出されるまで待機させます。

JScript側のコードはこちらです。上記のVBScriptを実行して待機させた後、下記のスクリプトを実行します。

```javascript
var server = WScript.CreateObject('Shell.Application').Windows().Item().GetProperty('Server');
server.InputBox_1("hello");
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1550728/f0a8ed95-1906-55ca-663c-7bb1d7752ef5.png)


VBScriptによって変更された`WScript.CreateObject("Shell.Application").Windows().Item()`にアクセスし、`"Server"`に属する関数を取得しているだけです。

## JScriptからVBScriptも呼び出す

2つスクリプトファイルを用意しておいてJScriptからVBScriptをキックしてもいいのですが、移動させる際に不意にどちらかを置き忘れたり、どちらを実行したらいいのか分からなくなることもあり得ます。

JScriptにVBScriptを埋め込んでしまいましょう。実行するたびに一時ファイルに保存し、`cscript.exe`で実行して、終了時に削除させます。

ついでにオーバーロードにも対応させます。省略された引数は`undefined`になるため、その個数で振り分ければ実現できそうです。それから補完が効くようにJSDoc書いていい感じに包んで ...

```javascript
var VBScript = new (function VBScript() {

    /**
     * @private 
     * 
     * `ShellWindows`経由で組み込み関数を公開するVBScriptを起動する。
     * 
     * 一度公開された関数が呼び出されると削除されるため、使う度に用意します。
     * 
     * @param {number} [timeout = Infinity] 省略した場合はタイムアウトしない
     * @throws {Error} timeoutミリ秒以上経過した場合
     */
    this._startService = function (timeout) {
        
        if (typeof timeout === 'undefined') {
            timeout = Infinity;
        }

        executeScript(saveEmbeddedScript());
        waitForReady(timeout);

        /**
         * 埋め込まれているVBScriptを一時ファイルに書き込みます。
         * 
         * 文字コードはASCIIで、万が一同名のファイルが存在する場合は上書きされます。
         * 
         * @returns {string}
         */
        function saveEmbeddedScript() {
            var script = 'With CreateObject("Shell.Application").Windows().Item(): .PutProperty "Server", Me: End With: Dim called: called = False: Do While Not called: WScript.Sleep 1: Loop: Function InputBox_1(prompt): InputBox_1 = InputBox(prompt): called = True: End Function: Function InputBox_2(prompt, title): InputBox_2 = InputBox(prompt, title): called = True: End Function: Function InputBox_3(prompt, title, default): InputBox_3 = InputBox(prompt, title, default): called = True: End Function: Function InputBox_4(prompt, title, default, xpos): InputBox_4 = InputBox(prompt, title, default, xpos): called = True: End Function: Function InputBox_5(prompt, title, default, xpos, ypos): InputBox_5 = InputBox(prompt, title, default, xpos, ypos): called = True: End Function: Function InputBox_7(prompt, title, default, xpos, ypos, helpfile, context): InputBox_7 = InputBox(prompt, title, default, xpos, ypos, helpfile, context): called = True: End Function';
            var fs = WScript.CreateObject('Scripting.FileSystemObject');
            var scriptFileName = fs.GetSpecialFolder(2) + '\\' + fs.GetTempName().replace(/\..+$/g, '.vbs');
            var stream = fs.CreateTextFile(scriptFileName, true, false);
            stream.Write(script);
            stream.Close();
            return scriptFileName;
        }

        /**
         * 与えられたスクリプトを非表示・非同期で実行します。ファイルは終了後に削除されます。
         * 
         * @param {string} scriptFileName 
         */
        function executeScript(scriptFileName) {
            WScript.CreateObject('WScript.Shell').Run('%COMSPEC% /C cscript.exe "' + scriptFileName + '" && DEL /Q "' + scriptFileName + '"', 0, false);
        }

        /**
         * calledを読んでみてエラーが発生するかどうかで公開されたかを判定して、スクリプトの起動を待機します。
         * 
         * timeoutミリ秒以上かかるとErrorをthrowします。
         * 
         * @param {number} timeout 
         */
        function waitForReady(timeout) {
            var ready = false;
            var pivot = new Date().getTime();
            while (true) {
                try {
                    WScript.CreateObject('Shell.Application').Windows().Item().GetProperty('Server').called;
                    ready = true;
                } catch (error) {
                    if (new Date().getTime() - pivot > timeout) {
                        throw new Error('VBScript._startService() timed out.');
                    }
                }
                if (ready) break;
            }
        }
    }

    /**
     * @private 
     * 
     * `undefined`ではない要素の数を返します。
     * 
     * @param {any[]} args 
     * @returns {number}
     */
    this._countValidArguments = function (args) {
        var count = 0;
        for (var i = 0; i < args.length; i++) {
            if (typeof args[i] !== 'undefined') {
                count++;
            }
        }
        return count;
    }

    /**
     * Displays a prompt in a dialog box, waits for the user to input text or click a button, and returns the contents of the text box.
     * 
     * @param {string} prompt String expression displayed as the message in the dialog box. The maximum length of prompt is approximately 1024 characters, depending on the width of the characters used. If prompt consists of more than one line, you can separate the lines using a carriage return character (Chr(13)), a linefeed character (Chr(10)), or carriage return–linefeed character combination (Chr(13) & Chr(10)) between each line.
     * @param {string} [title] String expression displayed in the title bar of the dialog box. If you omit title, the application name is placed in the title bar.
     * @param {string} [default_] String expression displayed in the text box as the default response if no other input is provided. If you omit default, the text box is displayed empty.
     * @param {number} [xpos] Numeric expression that specifies, in twips, the horizontal distance of the left edge of the dialog box from the left edge of the screen. If xpos is omitted, the dialog box is horizontally centered.
     * @param {number} [ypos] Numeric expression that specifies, in twips, the vertical distance of the upper edge of the dialog box from the top of the screen. If ypos is omitted, the dialog box is vertically positioned approximately one-third of the way down the screen.
     * @param {string} [helpfile] String expression that identifies the Help file to use to provide context-sensitive Help for the dialog box. If helpfile is provided, context must also be provided.
     * @param {number} [context] Numeric expression that identifies the Help context number assigned by the Help author to the appropriate Help topic. If context is provided, helpfile must also be provided.
     * @returns {string}
     */
    this.InputBox = function (prompt, title, default_, xpos, ypos, helpfile, context) {
        
        // 組み込み関数を公開するVBScriptを起動し、JScriptからの呼び出しを待ち受けます。
        this._startService(5000);
        
        // JScriptでは引数の省略が許可されるので、オーバーロードはJScript側で振り分けます。
        var count = this._countValidArguments([prompt, title, default_, xpos, ypos, helpfile, context]);
        var server = WScript.CreateObject('Shell.Application').Windows().Item().GetProperty('Server');
        switch (count) {
            case 1:
                return server.InputBox_1(prompt);
            case 2:
                return server.InputBox_2(prompt, title);
            case 3:
                return server.InputBox_3(prompt, title, default_);
            case 4:
                return server.InputBox_4(prompt, title, default_, xpos);
            case 5:
                return server.InputBox_5(prompt, title, default_, xpos, ypos);
            case 7:
                return server.InputBox_7(prompt, title, default_, xpos, ypos, helpfile, context);
            default:
                throw new Error('Invalid number of arguments: VBScript.InputBox()');
        }
    }
})();

var ret = VBScript.InputBox('with 1 arg');
WScript.Echo(ret);

ret = VBScript.InputBox('with 2 args', 'title');
WScript.Echo(ret);
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1550728/c84f2110-6386-3380-dac3-c7461dc5700a.png)

いいかんじ。

VBScriptの組み込み関数を、JScript自身の機能のように利用することができるようになりました。

# 参考

WSHから他のWSHを操作する。
http://scripting.cocolog-nifty.com/blog/2009/03/wshwsh-d9a5.html

VBScriptリファレンス補遺
https://y10k.hatenadiary.org/entries/2009/03/07

VBScript Language Reference
https://docs.microsoft.com/en-us/previous-versions/d1wf56tt%28v%3dvs.80%29

VBScriptの組み込み関数一覧
https://docs.microsoft.com/en-us/previous-versions/3ca8tfek(v=vs.80)

WshShell Object
https://docs.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/scripting-articles/aew9yb99(v=vs.84)