WSHでES2015を利用するベストプラクティス

Qiita初投稿です。

# 概要

Windows Script Host(WSH)はWindowsに標準搭載されたスクリプト実行環境です。
心臓部にEdge(Legacy)に搭載されたJavaScriptエンジンである**Chakra**を利用することで、ES2015相当の言語機能を利用することができます。

Chakraを常用するベストプラクティスを探っています。

- CLSIDは覚えられない
- 職場など管理者権限のない環境でも設定できる
- 他人の環境でも動く
- ダブルクリックで実行できるWSHの利点を損ないたくない
- 将来的にTypeScriptで書きたい

など...

# 環境

- Windows10


# 私の環境で試していること

- コンソールから実行する用に、[バッチにPATHを通す](#バッチにPATHを通す)
- ダブルクリックで実行できる機構は「[おまじない](#冒頭に「おまじない」を書く)」に任せる
- [レジストリ編集](#レジストリを編集する)は補助的に利用

この記事のコードは、[GitHub](https://github.com/mukai1011/chakra-environment/)に置いています。

## バッチにPATHを通す

以下3つのバッチファイルを作成し、PATHを通します。
`WChakra.bat`, `CChakra.bat`はそれぞれwscript, cscriptを司り、`Chakra.bat`はデフォルトがどちらか調査して適当に振り分けます。
コマンドラインから`chakra hoge.js`　`wchakra fuga.js`などで利用できるほか、既定のアプリケーションに登録することもできます。

### 良いところ

- 特にコンソールから実行する場合にわかりやすい
- 拡張子に関連付けして、ダブルクリックで実行できる

### 微妙なところ

- バッチは普段書かないので改善点があるかも(逐次修正)
- `Chakra.bat`はほんの少し遅いので、大量にキックする用途だと厳しそう
- `WChakra.bat`をダブルクリックで実行中にはDOS窓が表示される

```batchfile:Chakra.bat
@echo off
setlocal
set fn=%~dp0\_.vbs

REM this one-liner returns 0 only if hosted by "wscript.exe"
echo If(LCase(WScript.CreateObject("Scripting.FileSystemObject").GetFileName(WScript.FullName))="wscript.exe")Then:ret=0:Else:ret=1:WScript.Quit(ret) > %fn%
%fn% > nul 2>&1

if %errorlevel% equ 0 (
    del %fn%
    WChakra %*
) else (
    del %fn%
    CChakra %*
)
exit /b %errorlevel%
```
```batchfile:WChakra.bat
@echo off
if not "%~1"=="" (
    wscript //E:{1B7CD997-E5FF-4932-A7A6-2A9E636DA385} %*
) else (
    wscript
)
exit /b %errorlevel%
```
```batchfile:CChakra.bat
@echo off
if not "%~1"=="" (
    cscript //E:{1B7CD997-E5FF-4932-A7A6-2A9E636DA385} %*
) else (
    cscript
)
exit /b %errorlevel%
```

## 冒頭に「おまじない」を書く

https://qiita.com/riafeed/items/fcedc5943e6a041c90d7

こちらを参考にさせていただきました。
ダブルクリックまたはドラッグアンドドロップで利用できます。

### 変更点

- 文字数を絞る
- 生成される一時ファイルを隠しファイルに変更
- コマンドライン引数対応
- WScriptとCScriptを判別してキック

### 良いところ

- 冒頭(と末尾)に記載するだけ、手軽
- 他人の環境でも動く

### 微妙なところ

- 現状CScriptの標準入力は利用できない

```javascript:args.js
/*@cc_on
w=WScript;
st=w.CreateObject("ADODB.Stream");
fs=w.CreateObject("Scripting.FileSystemObject");
sh=w.CreateObject("WScript.Shell");
fn=w.ScriptFullName;

// 自身の文字コードをShift-JISに変換した一時ファイルを作成する
st.Charset="UTF-8";
st.Open();
st.LoadFromFile(fn);
t=st.ReadText();
st.Close();
fn=fs.GetFile(fn).ParentFolder.Path+"\\."+w.ScriptName;
if(fs.FileExists(fn))fs.DeleteFile(fn,true);
st=fs.CreateTextFile(fn);
f=fs.GetFile(fn);
f.Attributes=2;
st.Write(t);
st.Close();

// コマンドライン引数を引き継ぐ
arg=w.Arguments;
opt="";
for(i=0;i<arg.Count();i++){
    opt+=" \""+arg.Item(i)+"\"";
}
opt=opt.slice(1);

// キック
host=w.FullName;
cmd=host+" //Nologo //E:{1B7CD997-E5FF-4932-A7A6-2A9E636DA385} \""+fn+"\" "+opt;
if(fs.GetFileName(host).toLowerCase()=="wscript.exe"){
    ret=sh.Run(cmd,1,true);
}else{
    ex=sh.Exec(cmd);
    while(ex.Status==0){
        try{
            w.StdOut.Write(ex.StdOut.Read(1));
        }catch(e){}
    }
    ret=ex.ExitCode;
    w.StdOut.Write(ex.StdOut.ReadAll());
}
f.Delete(true);
w.Quit(ret);
@if(0)*/

// 本体ここから
const args = WScript.Arguments;
let msg = "";
for(let i = 0; i < args.Count(); i++){
    msg += `${args.Item(i)}\n`;
}
WScript.Echo(msg);

//@end
```
```javascript:omajinai.js
/*@cc_on;w=WScript;st=w.CreateObject("ADODB.Stream");fs=w.CreateObject("Scripting.FileSystemObject");sh=w.CreateObject("WScript.Shell");fn=w.ScriptFullName;st.Charset="UTF-8";st.Open();st.LoadFromFile(fn);t=st.ReadText();st.Close();fn=fs.GetFile(fn).ParentFolder.Path+"\\."+w.ScriptName;if(fs.FileExists(fn))fs.DeleteFile(fn,true);st=fs.CreateTextFile(fn);f=fs.GetFile(fn);f.Attributes=2;st.Write(t);st.Close();arg=w.Arguments;opt="";for(i=0;i<arg.Count();i++){opt+=" \""+arg.Item(i)+"\"";}opt=opt.slice(1);host=w.FullName;cmd=host+" //Nologo //E:{1B7CD997-E5FF-4932-A7A6-2A9E636DA385} \""+fn+"\" "+opt;if(fs.GetFileName(host).toLowerCase()=="wscript.exe"){ret=sh.Run(cmd,1,true);}else{ex=sh.Exec(cmd);while(ex.Status==0){try{w.StdOut.Write(ex.StdOut.Read(1));}catch(e){}}ret=ex.ExitCode;w.StdOut.Write(ex.StdOut.ReadAll());}f.Delete(true);w.Quit(ret);@if(0)*/
```

## レジストリを編集する

https://stuncloud.wordpress.com/2021/02/06/scriptcontrol-chakra/

こちらを参考にさせていただきました。
`wscript //E:Chakra hoge.js`で利用できるようになります。

### 良いところ

- wsfでも利用できる(はず、確認出来たら追記)
    - CLSID直書きでもできるかも

### 微妙なところ

- 他所の環境では動かない
- レジストリいじるときは慎重に

```HKCR.reg
Windows Registry Editor Version 5.00

[HKEY_CLASSES_ROOT\Chakra]
@="Chakra Language"

[HKEY_CLASSES_ROOT\Chakra\CLSID]
@="{1b7cd997-e5ff-4932-a7a6-2a9e636da385}"


```
```HKCU.reg
Windows Registry Editor Version 5.00

[HKEY_CURRENT_USER\SOFTWARE\Classes\Chakra]
@="Chakra Language"

[HKEY_CURRENT_USER\SOFTWARE\Classes\Chakra\CLSID]
@="{1b7cd997-e5ff-4932-a7a6-2a9e636da385}"


```

# 機能制限について補足

Chakra利用時に`WScript.Quit()`が利用できないため、終了コードを返せないとのことなので実験しました。
1~6の整数を終了コードにセットするJavaScriptを用意して、10回実行します。
PowerShellでは、`$LastExitCode`に直前の終了コードが代入されます。

エラーが発生するため、握り潰します。

```javascript:dice.js
try {
    WScript.Quit(parseInt(6 * Math.random() + 1));
} catch (e) {
    // JavaScript runtime error: Object doesn't support this action
}
```
```terminal:PowerShell
> for($i=0; $i -lt 10; $i++){chakra dice.js; echo $LastExitCode;}
1
4
5
6
5
2
4
1
3
2
```

きちんとランダムです。
エラーが発生し停止もしませんが、なぜか終了コードは返せているようです。

# 参考
- [WSH JScript Chakra を使用した ES2015(ES6) 対応 （ スクリプトエンジン まとめ ）](https://inemaru.hatenablog.com/entry/2018/04/05/055557)
- [WSH JScript Chakra エンジンの共存による機能制限の回避（WScript Quit 代替）](https://inemaru.hatenablog.com/entry/2020/08/11/020312)
- [Chakraエンジン対応のWSHスクリプトをダブルクリックで実行できるようにする](https://qiita.com/riafeed/items/fcedc5943e6a041c90d7)
- [ScriptControlでChakra使う](https://stuncloud.wordpress.com/2021/02/06/scriptcontrol-chakra/)

随時更新...