WSH JScript Chakra エンジンの共存による機能制限の回避(WScript Quit 代替)
はじめに
WSH JScript に Chakra を指定すると WScript Object の機能が制限される為、
WScript.Quit
関数が利用できず、戻り値を返す術が失われます。
今回は、この問題の対処例を挙げます。
前提
Windows 環境には、Windows Script Host(WSH)と呼ばれる
Node.js の様な JavaScript が動作するスクリプトの実行環境が標準搭載されています。
技術としては古いものなので、デフォルトでは ES3 程度のサポートですが、
スクリプトエンジンを Chakra に変更することでES6の機能が利用可能
です。
WSH JScript Chakra を使用した ES2015(ES6) 対応 ( スクリプトエンジン まとめ )
JScript と Chakra を共存させる
既定のJScript で、Chakra 用の処理をラップする形の対処法です。
WScript.Quit は、CScript でないと戻り値が機能しないので、バッチに埋め込みます。
この方法は、JScript と Chakra で各エンジンが共存するので、
機能制限の掛かる箇所は、JScript側で関数化する等の対処で回避可能になります。
※ 例えば、GetObject等のChakraでは未定義となる関数は、JScript 経由で利用できます。
0</* :: @cscript /nologo /E:JScript "%~f0" %* @rem 戻り値の確認用 @echo.ExitCode:%errorlevel%&pause @exit /b %errorlevel%&*/0;//@cc_on @if(0) // // Chakra // // エントリーポイント用の関数(プロセスの戻り値を返す) function main() { // コンソールに表示 const msg = `${WSH} for Chakra`; WSH.Echo(msg); // JScript側の関数を呼ぶ const r = JS.hoge(); // 強制終了するなら、例外を投げる //throw "quit"; // もしくは、早期リターン //return; // ポップアップを表示 // ActiveXObject は未定義なので、CreateObject を使う const shell = WSH.CreateObject("WScript.Shell"); shell.popup(msg); // 戻り値 return r; } function fuga() { WSH.Echo("fuga"); } var quit=!1;let item,ie,w=WSH.CreateObject("Shell.Application").Windows(),i=0; for(;i<w.Count;i++)if((item=w(i))&&item.hWnd==WSH.Arguments(0)){ie=item;break} for(ie.PutProperty("@",this);!quit;)WSH.Sleep(1e3);/*@end @cc_on for(var ckr,ie=WSH.CreateObject("InternetExplorer.Application"),sh=WSH.CreateObject("WScript.Shell"), p=sh.Exec('cscript /E:{1B7CD997-E5FF-4932-A7A6-2A9E636DA385} "'+WSH.ScriptFullName+'" '+ie.hWnd);!(ckr=ie.GetProperty("@"));) {if(0!=p.Status){for(;!p.StdErr.AtEndOfStream;)WSH.StdErr.Write(p.StdErr.Read(256));ie.Quit(),WSH.Quit(p.ExitCode)}WSH.Sleep(1e3)} (this.CKR=ckr).JS=this,ckr.WScript=WSH,ckr.WSH=WSH,ie.Quit(),r=CKR.main(),ckr.quit=!0,WSH.Quit(r); // // JScript // function hoge() { // Chakra側の関数を呼ぶ CKR.fuga(); return 123; } @*/
仕組み
条件付きコンパイル
機能を使って、1つのスクリプト内に
エンジン毎(JScript, Chakra)で実行されるコードを共存させています。
また、プロセス間でオブジェクトを共有するために、
InternetExplorer
Object の PutProperty / GetProperty
を利用しています。
該当部分は、コーディングに直接関係無いのでMinify済みですが、
大体下記のように処理しています。
追記(おそらく最適解):例外を握りつぶすと戻り値が使えるらしい
2022/05/28:
例外をキャッチすれば正しく戻り値が扱えるという話を見つけました。
実際試してみると、普通に扱えているようなので、これが最適解でしょう。
WSHでES2015を利用するベストプラクティス - Qiita
戻り値を確認するため、exit /b
で戻り値を設定するバッチに、スクリプトを埋め込みます。
0</* :: @%windir%\System32\cscript.exe /nologo /E:{1B7CD997-E5FF-4932-A7A6-2A9E636DA385} "%~f0" %* @echo 戻り値は、%errorlevel%です。&pause @exit /b %errorlevel%&*/0; // エントリ let global_result=(()=>{ // ポップアップを表示 // ActiveXObject は未定義なので、CreateObject を使う const shell = WSH.CreateObject("WScript.Shell"); shell.popup(`${WSH} for Chakra`); return 123; })(); try {WScript.Quit(global_result); }catch(e){// JavaScript runtime error: Object doesn't support this action }
おわりに
一般的に、外部から呼び出されることを想定するプログラムは、
戻り値から、エラー原因を特定できるようにしています。
Chakra を指定したスクリプトでは、スクリプトが失敗しても
戻り値から判断できない状況でしたが、これで一件落着?でしょうか。
元々 Chakra を指定する事自体が、仕様の穴をつくような方法なので、
回りくどい対応を取らざる得なくなる事もありますが、
それを差し引いても ES6 が利用できる点は大きいかと思います。
参考
JScript Chakra wscriptで、ECMAScript 2015 (ES6) を利用する
はじめに
wscript を利用する際に、Chakra エンジンを指定する方法です。
cscript で JScript を利用する場合、Chakra エンジンを指定するために
バッチファイルと組み合わせる方法がありますが、
wscript ではコマンドプロンプトの画面が不要なので、非表示で起動する方法です。
方法
必要なコードを先に載せます。
.js
もしくは .jse
形式で保存して、そのまま実行可能です。
//@cc_on for(s='"'+WSH.ScriptFullName+'"',i=0;i<WSH.Arguments.length;i++)s+=' "'+WSH.Arguments(i)+'"';WSH.CreateObject("WScript.Shell").Run("wscript /E:{1B7CD997-E5FF-4932-A7A6-2A9E636DA385} "+s)@if(0) // 引数を表示 let args=[]; for(let a=WSH.Arguments,c=0;c<a.length;c++)args.push(a(c)); WSH.Echo(`実行引数の数:${WSH.Arguments.length}\n${args.join("\n")}`); //@end
もしくは、コマンドライン引数を利用しない場合、もう少し短くなります。
//@cc_on WSH.CreateObject("WScript.Shell").Run('wscript /E:{1B7CD997-E5FF-4932-A7A6-2A9E636DA385} "'+WSH.ScriptFullName)@if(0) // 適当にコードを書く const echo = ()=>WSH.Echo("Hello WScript Chakra"); echo(); //@end
JScript エンジンで実行された後、Chakraエンジンで自身を起動し直す仕組みで、
実行すると、メッセージボックスが表示されます。
JScript に Chakra のコードを埋め込む
JScript には、IEのスクリプトエンジンとして利用されている経緯から
Chakra には無い、ブラウザの判別用の「条件付きコンパイル」機能が搭載されています。
これは、コメント行にコードを利用するかどうかの判定を埋め込む代物なので、
JScriptコンパイラが読み飛ばす部分(@if(0)~@end)は、処理されません。
つまり、ES6の機能を利用しても、コンパイルエラーしないという事です。
/*@cc_on WSH.Echo("JScript で実行される範囲"); @if(0) @*/ WSH.Echo(`Chakra で実行される範囲`); //@end
結果、ES6ベースなコードの埋め込みが実現し、
「JScript で実行される範囲」から、Chakraエンジンを指定して自身を起動し直すことで、
Chakraエンジンからは、コメントアウトされていない部分だけが実行されます。
無事コマンドプロンプトを表示せずに、Chakraエンジンでスクリプトが実行されました。
おわりに
JScript に Chakra のコードを埋め込む方法は、
こちらを参考にしました。
qiita.com
Chakraで再起動する処理は、コーディングに直接関係ないので
Minifierを使って1行に収めてしまいましたが、
コマンドライン引数を利用する為の処理が、そこそこ長くなっています。
WSH.Arguments は、配列型ではないので無難にforを使いましたが、
もしかすると、より短く書けるかもしれません。
おわり。
WSH WSF JScript・VBScript で、YAML をパースする方法(js-yaml)
JScript や VBScript で yaml ファイルをパースしたいとき、
wsf形式と外部パッケージのjs-yaml
で、簡単に実現できます。
github.com
実装
yamlファイル。(sample.yaml)
文字コードは、SJISを使用。
# 設定ファイル config: title: "AppName" width: 1280 height: 720
wsf形式が関連付けされていない環境があるので、
サンプルは適当なバッチに埋め込んで使います。
<!-- : @setlocal&pushd %~dp0 @cscript //nologo "%~f0?.wsf" %* || pause @popd&endlocal&exit /b --> <job> <script src="https://cdnjs.cloudflare.com/ajax/libs/es5-shim/4.5.14/es5-shim.min.js" /> <script src="https://cdnjs.cloudflare.com/ajax/libs/es5-shim/4.5.14/es5-sham.min.js" /> <script src="https://cdnjs.cloudflare.com/ajax/libs/js-yaml/3.13.1/js-yaml.js" /> <object id="shell" progid="WScript.Shell" /> <object id="fso" progid="Scripting.FileSystemObject" /> <script language="JScript"> WSH.Quit(function() { try{ // 読み込み var yamlFile = fso.OpenTextFile("sample.yaml", 1, true); var yamlText = yamlFile.ReadAll(); yamlFile.Close(); var doc = jsyaml.load(yamlText); // 表示 WSH.Echo(doc.config.title); WSH.Echo(doc.config.width); WSH.Echo(doc.config.height); // Enter入力待ち WSH.StdIn.ReadLine(); } catch(e){ WSH.Echo(e.name + ":" + e.message); return -1; } return 0; }()); </script> </job>
VBScript 版
<script language="VBScript"> ' 読み込み Set yamlFile = fso.OpenTextFile("sample.yaml", 1, True) yamlText = yamlFile.ReadAll yamlFile.Close Set doc = jsyaml.load(yamlText) ' 表示 WSH.Echo(doc.config.title) WSH.Echo(doc.config.width) WSH.Echo(doc.config.height) ' Enter入力待ち WSH.StdIn.ReadLine ' 解放 Set yamlFile = Nothing Set doc = Nothing </script>
仕組み
js-yaml
では、ES5に対応した環境が求められますが、
wsf 形式は、ES3程度の機能しか使えないのでes5-shim
ライブラリを使って
擬似的にES5環境を扱えるようにします。
読み込んだ後は、jsyaml
オブジェクトを扱うだけです。
その他
オフライン環境で使いたい場合など、
ライブラリの参照にCDNを使いたくなければ、ローカルに保存して使いましょう。
<script src="./es5-shim.min.js" /> <script src="./es5-sham.min.js" /> <script src="./js-yaml.js" />
以上、おわり。
WSH JScript で、高精度に処理時間を計測する(Performance API)
はじめに
JScriptで、スクリプトの処理時間を計測するとき、
Date オブジェクトを使って計測することがありますが、これはミリ秒単位の精度です。
厳密な速度計測が不要であれば、ミリ秒もあれば事足りますが、
より精度の高いマイクロ秒単位が必要であれば、
Web API (Performance API) の Performance インターフェースを使います。
実装
計測対象の関数は、ここから拝借。
フィボナッチ数を出力する関数を作る - Qiita
function do_something() { var fib = function (n) { return n > 2 ? fib(n - 1) + fib(n - 2) : 1; }; fib(33); }
Performance インターフェースによる計測
JScript環境の、Performance インターフェースは、
JSON オブジェクト等と同様で、htmlfile オブジェクトから取得できます。
WSH.Quit(function() { try{ // Performance を取得 var htmlfile = WSH.CreateObject("htmlfile"); htmlfile.write('<meta http-equiv="x-ua-compatible" content="IE=Edge"/>'); var performance = htmlfile.parentWindow.performance; htmlfile.close(); // 計測 var startTime = performance.now(); do_something(); var endTime = performance.now(); // 結果 // 浮動小数が得られる。小数点以下が、マイクロ秒。 // (小数第1位までしか得られない?) WSH.Echo((endTime - startTime) + " ms"); } catch(e){ WSH.Echo(e.name + ":" + e.message); return -1; } return 0; }());
過去の手法
Date オブジェクトによる計測
WSH.Quit(function()
{
try{
// 計測
var startTime = new Date();
do_something();
var endTime = new Date();
// 結果(※ これでは、ミリ秒に丸められた結果しか得られない)
WSH.Echo((endTime.getTime() - startTime.getTime()) + " ms");
}
catch(e){
WSH.Echo(e.name + ":" + e.message);
return -1;
}
return 0;
}());
その他メモ
バッチコマンドの計測であれば、powershell の Measure-Command が手軽です。
powershell -c (Measure-Command { (ここに計測するコマンド) })
WSH JScript で、JSONファイルをパースする(Chakra を利用する場合)
メモ
既定のJScript でJSONオブジェクトを利用する方法の別解。
Chakra エンジンを使うとJSONオブジェクトが定義済み
で、そのまま使える模様。
実装
エンジンを指定するため、適当な .bat
に埋め込んで使います。
0</*! :: @%windir%\System32\cscript.exe //nologo //E:{1B7CD997-E5FF-4932-A7A6-2A9E636DA385} "%~f0" %*||pause @pause rem 入力待機 @exit /b */0; (()=>{ try { // JSONをパース const jsonTxt = `{"Application": {"title": "SampleApp", "width": 1280, "height": 720}}`; let rootObj = JSON.parse(jsonTxt); // 結果を表示 let app = rootObj.Application; WSH.Echo(`${app.title}\n${app.width}\n${app.height}`); } catch(e){ WSH.Echo(e.name + " : " + e.message); } finally{} })();
結果
SampleApp 1280 720
以上、おわり。
関連
WSH JScript Chakra を使用した ES2015(ES6) 対応 ( スクリプトエンジン まとめ )
はじめに
WSH JScript (wscript cscript) で使用できる スクリプトエンジン まとめです。
JavaScript限定、プリインストールされている物だけなので注意。
ツールとして独立しているChakraCoreや、その他サードパーティーは含みません。
各 JavaScript エンジン について
JScript (JavaScript) だけでも、4種類ほどスクリプトエンジンがあるようです。
以下、それぞれバッチファイルから cscript で実行する際のサンプルコマンドです。
JScript
JScript を使用する際、一般的に指定されるスクリプトエンジンです。
バージョンは、環境によって異なりますが、
現時点のOSサポートを考慮すれば、5.7 か 5.8 でしょう。
rem jscript.dll %windir%\System32\cscript.exe /nologo /E:JScript "%~f0" %*
JScript.Compact
JScript Compact Profile (ECMA 327) で実行する、スクリプトエンジンです。
MSの情報が少ないので、詳細は未確認ですが
簡単に説明すれば、「いくつかの機能を制限して、パフォーマンス向上を図る設定を使用する」と言った感じでしょうか。
機能の制限について、少なくとも with ステートは、サポートされていないようです。
rem JScript Compact Profile (ECMA 327) %windir%\System32\cscript.exe /nologo /E:JScript.Compact "%~f0" %*
JScript9
IE9 から使用されている、JScript9.dll をスクリプトエンジンとして指定できます。
ProgID が公開されていないので、CLSID を指定する必要があります。
ちなみに、JScript9 の 9 は、エンジンバージョンと言うわけでは無い様で、
Windows10 では、バージョン 11.0 として扱われていました。(中身は、Chakra っぽい?)
JScript と同程度の機能しか使えませんが、JScript9.dll の方が高速に動作します。
ただし、JScript 互換は完全ではなく WScript.Quit が使用できません。
rem jscript9.dll %windir%\System32\cscript.exe /nologo /E:{16d51579-a30b-4c8b-a276-0ff4dc41e755} "%~f0" %*
Chakra
Microsoft Edge の JavaScriptエンジン です。
こちらも、ProgID が公開されていないので、CLSID を指定する必要があります。
Chakra を使用する場合は、ES2015(ES6) に対応
している為、クラスなどが使用できます。
ただし、JScript9 と同様に、WScript.Quit に加え GetObject、VBArray 等が、使用できません。
rem chakra.dll %windir%\System32\cscript.exe /nologo /E:{1B7CD997-E5FF-4932-A7A6-2A9E636DA385} "%~f0" %*
Chakraを使用したスクリプト を バッチに埋め込む場合
JScript9までは、@if を使用してバッチファイルにJScriptを埋め込むことが出来ましたが、
Chakra は、IE由来の条件付きコンパイルをサポートしていないため、@if を解釈できません。
JScript でハマる日々 - m2
@if(0==0) /* @rem 指定できるエンジンは、JScript9 まで。Chakra 不可 @%windir%\System32\cscript.exe /nologo /E:JScript "%~f0" %*||pause @exit /b %errorlevel%&*/@end
JScript共通の埋め込み
Chakra 以外のエンジンを共存させる場合は、
@if を使用しないshebangモドキがあるので、そちらを使います。
ildar-shaimordanov/hello.jscript.bat
0</* :: @%windir%\System32\cscript.exe /nologo /E:{1B7CD997-E5FF-4932-A7A6-2A9E636DA385} "%~f0" %*||pause @exit /b %errorlevel%&*/0;
Chakra限定の埋め込み
Chakra のみ使う場合は、
Chakra が「<!--」をコメント扱いすることを利用した方法があります。
https://qiita.com/snipsnipsnip/items/50e4ca88e3ce3f8cffda
<!-- : ^ /* @%windir%\System32\cscript.exe /nologo /E:{1B7CD997-E5FF-4932-A7A6-2A9E636DA385} "%~f0" %*||pause @exit /b %errorlevel%&*/
JScript9 や Chakra で、戻り値を返したい場合(2022/05/28 更新)
WScript.Quit は、例外を握りつぶすと使えます。
.bat
にコピペで、動作確認できます。
0</* :: @%windir%\System32\cscript.exe /nologo /E:{1B7CD997-E5FF-4932-A7A6-2A9E636DA385} "%~f0" %* @echo 戻り値は、%errorlevel%です。&pause @exit /b %errorlevel%&*/0; // エントリ let global_result=(()=>{ // ポップアップを表示 // ActiveXObject は未定義なので、CreateObject を使う const shell = WSH.CreateObject("WScript.Shell"); shell.popup(`${WSH} for Chakra`); return 123; })(); try {WScript.Quit(global_result); }catch(e){// JavaScript runtime error: Object doesn't support this action }
補足
一応触れておくとChakraCore
を利用する場合、
WScript.Quit は定義済みで普通に呼び出せるので、このような悩みとは無縁です。
https://github.com/microsoft/ChakraCore/blob/bd91335361ddd215622d810cc7341a180abd2db4/bin/ch/WScriptJsrt.cpp#L1090
おわりに
WSH ってだけで、今更感満載のレガシーなのですが、
初期状態のWindowsで使用できる手軽さで、根強く?使われている様子。
今回のまとめは、WSH JScript で、クライアントの環境に依存せずに
ES2015 ベースのコードを使えないか調査したときの副産物です。
結果的に、ES2015 対応は行っていませんが、
TypeScript 経由で、対応できそうかなと言ったところでオチ。
参考リンク
- リファレンス
- JScript.Compact
- ECMAScript - Wikipedia
ECMA 327 について簡単な説明 - 'with' not available in the ECMA 327 Compact Profile Microsoft Docs
with が使用できない点について、MSのリファレンス
- ECMAScript - Wikipedia
- Chakra
- Chakra - Wikipedia
JScript9.dll を含めた簡単な概要
- Chakra - Wikipedia