以下は訳あって削除済みの過去記事の転載です(published at 2023-08-25 23:33)。
現在はより汎用的な方法を思いつき、こちらを使用しています。
はじめに
WScript.Shell
オブジェクトのExec
メソッドは、類似するRun
メソッドと異なり、戻り値のWshShellExec
オブジェクトを通じて立ち上げた子プロセスの標準入力/出力/エラー出力にアクセスできます。その代わりにRun
メソッドに備わっているウィンドウ非表示のオプションがなく、wscript.exeやVBAでCLIアプリケーションを実行するとコンソールウィンドウが出てきてしまうのが悩ましいところです。無駄なコンソールウィンドウが出なければ、見た目がよくなるだけでなく、現在のウィンドウからフォーカスが奪われないなどそこそこ実用性があります。
こちらの記事では、cscript.exeでExec
メソッドを実行した際にはコンソールウィンドウが出現しないことを利用して、Run
メソッドでウィンドウを非表示にして「cscript.exeでExec
メソッドを実行するスクリプト」を実行するアイディアが紹介されていました。
今回はこれをさらに押し進めた手法を紹介します。Exec
相当の関数実行時にcscript.exeで実行させるスクリプトを動的に生成し、さらにShell.Application
オブジェクトのWindows().Item().PutProperty
/GetProperty
メソッドを介して、cscript.exe側から呼び出し元にWshScriptExec
オブジェクトを公開させるというのが基本的な発想です。
比較用(VBScript)
Dim pwsh
Set pwsh = CreateObject("WScript.Shell").Exec("powershell.exe")
WScript.Echo(pwsh.StdOut.ReadLine())
VBScript
Class WshScriptExecProvider
Private objWshShell 'As Object
Private objShell 'As Object
Private arrKeys() 'As String
Private Sub Class_Initialize()
Set objWshShell = CreateObject("WScript.Shell")
Set objShell = CreateObject("Shell.Application")
ReDim arrKeys(-1)
End Sub
Private Sub Class_Terminate()
Dim i 'As Long
For i = LBound(arrKeys) To UBound(arrKeys)
objShell.Windows().Item().PutProperty arrKeys(i) & "_Quit", True
Next 'i
Set objWshShell = Nothing
Set objShell = Nothing
End Sub
Private Function GenerateRandomString(lLength) 'As String
Dim i 'As Long
Dim ret 'As String
ret = ""
Dim strChars 'As String
strChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
Randomize
For i = 1 To lLength
ret = ret & Mid(strChars, Int(Len(strChars) * Rnd() + 1), 1)
Next 'i
GenerateRandomString = ret
End Function
Private Function GenerateVBScriptContent(strCommand, strKey) 'As String
GenerateVBScriptContent = "" & _
"Set sh = CreateObject(""Shell.Application""): " & _
"Set item = sh.Windows().Item(): " & _
_
"item.PutProperty """ & strKey & """, CreateObject(""WScript.Shell"").Exec(""" & strCommand & """): " & _
"item.PutProperty """ & strKey & "_Quit"", False: " & _
_
"Do While Not item.GetProperty(""" & strKey & "_Quit""): " & _
" WScript.Sleep 100: " & _
"Loop: " & _
_
"Set sh = Nothing"
End Function
Private Function GenerateRunCommand(strScript, strKey) 'As String
GenerateRunCommand = "" & _
"cmd.exe /c echo " & strScript & " > %TEMP%\" & strKey & ".vbs &&" & _
"cscript %TEMP%\" & strKey & ".vbs &&" & _
"del %TEMP%\" & strKey & ".vbs"
End Function
Private Function GetObjectFromShell(strKey) 'As Object
Dim ret 'As Object
Dim startTime 'As Double
startTime = Timer
On Error Resume Next
Do While True
Set ret = objShell.Windows().Item().GetProperty(strKey)
If Err.Number = 0 Then
Exit Do
End If
Err.Clear
WScript.Sleep(1)
If 1 < Timer - startTime Then
On Error GoTo 0
End If
Loop
On Error GoTo 0
Set GetObjectFromShell = ret
End Function
Public Function Exec(strCommand) 'As Object
Dim strKey 'As String
strKey = GenerateRandomString(16)
Dim strScript 'As String
strScript = GenerateVBScriptContent(strCommand, strKey)
Dim strRunCommand 'As String
strRunCommand = GenerateRunCommand(strScript, strKey)
objWshShell.Run strRunCommand, 0, False
Dim ret 'As Object
Set ret = GetObjectFromShell(strKey)
ReDim Preserve arrKeys(UBound(arrKeys) + 1)
arrKeys(UBound(arrKeys)) = strKey
Set Exec = ret
End Function
End Class
Dim provider
Set provider = New WshScriptExecProvider
Dim pwsh
Set pwsh = provider.Exec("powershell.exe")
WScript.Echo(pwsh.StdOut.ReadLine())
JScript
function WshScriptExecProvider() {
var objWshShell = new ActiveXObject("WScript.Shell");
var objShell = new ActiveXObject("Shell.Application");
var keys = [];
this.Dispose = function () {
for (var i = 0; i < keys.length; i++) {
objShell.Windows().Item().PutProperty(keys[i] + "_Quit", true);
}
};
function _generateRandomString(length) {
var ret = "";
var strChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
for (var i = 1; i <= length; i++) {
ret += strChars.charAt(Math.floor(Math.random() * strChars.length));
}
return ret;
}
function _generateVBScriptContent(command, key) {
return '' +
'Set sh = CreateObject("Shell.Application"): ' +
'Set item = sh.Windows().Item(): ' +
'item.PutProperty "' + key + '", CreateObject("WScript.Shell").Exec("' + command + '"): ' +
'item.PutProperty "' + key + '_Quit", False: ' +
'Do While Not item.GetProperty("' + key + '_Quit"): ' +
' WScript.Sleep 100: ' +
'Loop: ' +
'Set sh = Nothing';
}
function _generateRunCommand(script, key) {
return '' +
'cmd.exe /c echo ' + script + ' > %TEMP%\\' + key + '.vbs && ' +
'cscript %TEMP%\\' + key + '.vbs && ' +
'del %TEMP%\\' + key + '.vbs';
}
function _getObjectFromShell(key) {
var ret;
var startTime = new Date().getTime();
while (true) {
ret = objShell.Windows().Item().GetProperty(key);
if (ret != null) {
break;
}
WScript.Sleep(1);
if (new Date().getTime() - startTime > 1000) {
throw new Error();
}
}
return ret;
}
this.Exec = function (command) {
var key = _generateRandomString(16);
var script = _generateVBScriptContent(command, key);
var runCommand = _generateRunCommand(script, key);
objWshShell.Run(runCommand, 0, false);
var ret = _getObjectFromShell(key);
keys.push(key);
return ret;
};
}
var provider = new WshScriptExecProvider();
try {
var pwsh = provider.Exec("powershell.exe");
WScript.Echo(pwsh.StdOut.ReadLine());
} finally {
provider.Dispose();
}
スクリプトの動的生成とオブジェクトの共有
まずcscript.exeで実行するスクリプトの動的生成部に注目します。以下のスクリプトを1行につなげたものをシェルワンライナーで保存しています。埋め込まれる文字列を便宜上{{(変数名)}}
と表現します。
Set sh = CreateObject("Shell.Application")
Set item = sh.Windows().Item()
item.PutProperty "{{strKey}}", CreateObject("WScript.Shell").Exec("{{strCommand}}")
item.PutProperty "{{strKey}}_Quit", False
Do While Not item.GetProperty("{{strKey}}_Quit")
WScript.Sleep 100
Loop
Set sh = Nothing
Shell.Application
オブジェクトのWindows().Item().PutProperty
/GetProperty
メソッドでは、文字列をキーに値やオブジェクトを登録できます。登録された内容は別プロセスからもアクセスできるため、事実上のプロセス間通信が可能になります。
キーとなる文字列は16文字のランダムな英数字を埋め込んでいます。このスクリプトが終了すると登録した参照も切れてしまうため、_Quit
をサフィックスとして停止用のフラグも併せて登録します。
内部で作成したファイルを実行するようなスクリプトは往々にしてウイルス判定されやすいですが、シェルワンライナーにしてしまえばお目こぼしをもらえることが多い気がします。キーをファイル名にも使用し、環境変数TEMP
が示すディレクトリに保存します。スクリプトの終了後には自動的に削除されるようにします。
cmd.exe /c echo (中略) > %TEMP%\{{strKey}}.vbs
cscript %TEMP%\{{strKey}}.vbs
del %TEMP%\{{strKey}}.vbs
衝突判定はしていないのでお祈りをお願いします。
非同期で共有されるオブジェクトの取得
続いて共有されたオブジェクトの取得部分です。上記の通りGetProperty
メソッドで取得しますが、Run
メソッドによるcscript.exeの実行を非同期にしているため、共有される前に取得を試みてしまう可能性があります。
JScriptではnull
が返るので比較で容易に対処できますが、VBScriptではこの箇所でエラーが発生します。On Error Resume Next
でエラーを処理して、間隔をおいてリトライします。1秒間オブジェクトを取得できなかった場合エラー処理をデフォルトの即時送出に戻す(On Error GoTo 0
)ことで、次回試行時のGetProperty
のエラーはそのまま送出されます。次の部分を比較してみてください。
VBScript
Private Function GetObjectFromShell(strKey) 'As Object
Dim ret 'As Object
Dim startTime 'As Double
startTime = Timer
On Error Resume Next
Do While True
Set ret = objShell.Windows().Item().GetProperty(strKey)
If Err.Number = 0 Then
Exit Do
End If
Err.Clear
WScript.Sleep(1)
If 1 < Timer - startTime Then
On Error GoTo 0
End If
Loop
On Error GoTo 0
Set GetObjectFromShell = ret
End Function
JScript
function _getObjectFromShell(key) {
var ret;
var startTime = new Date().getTime();
while (true) {
ret = objShell.Windows().Item().GetProperty(key);
if (ret != null) {
break;
}
WScript.Sleep(1);
if (new Date().getTime() - startTime > 1000) {
throw new Error();
}
}
return ret;
}
cscript.exe用プロセスの終了
キーは配列に貯めておきます。Execを実行してオブジェクトを公開している子プロセスは明示的に終了する必要があることに注意してください。VBScriptではデストラクタ内で、貯めたキーをもとに先ほど設定した終了用のフラグをセットします。
Dim i 'As Long
For i = LBound(arrKeys) To UBound(arrKeys)
objShell.Windows().Item().PutProperty arrKeys(i) & "_Quit", True
Next 'i
JScriptではデストラクタを利用できないため、try/finallyで処理してください。
var provider = new WshScriptExecProvider();
try {
var pwsh = provider.Exec("powershell.exe");
WScript.Echo(pwsh.StdOut.ReadLine());
} finally {
provider.Dispose();
}
まとめ
標準出力を一斉に取得するだけなら一時ファイルへのリダイレクトなど他の方法も考えられますが、標準入力も扱えるのがExec
メソッドの特徴です。いちいちコンソールウィンドウが出ないことで、少し便利に使えるようになります。いろいろ試してみてください。
Execで対話型CLIアプリケーションを利用したこちらの記事もぜひどうぞ。