イネマルのプログラミング備忘録

趣味プログラマのメモ

WSH WSF JScript・VBScript で、YAML をパースする方法(js-yaml)

JScriptVBScriptyaml ファイルをパースしたいとき、
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オブジェクトを扱うだけです。

github.com

その他

オフライン環境で使いたい場合など、
ライブラリの参照に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 { (ここに計測するコマンド) })

おわり。 developer.mozilla.org

WSH JScript で、JSONファイルをパースする(Chakra を利用する場合)

メモ

既定のJScriptJSONオブジェクトを利用する方法の別解。
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

以上、おわり。

関連

developer.mozilla.org inemaru.hatenablog.com

WSH JScript CScript プログレスバー

はじめに

軽く検索しても手軽なサンプルが見つからなかったので、
CScript と JScript を使ったコマンドライン環境向けに、プログレスバーを用意します。

↓ のように動きます。
f:id:inemaru:20200321072912g:plain

実装

最低限の機能があれば良いので、設定できるオプションは下記だけ。

画面更新の仕組みとしては、C++等と同様で、
制御文字CR(\r)を使っています。(行頭に戻して書き直しするだけ。)

var ProgressBar = (function(){
    function ProgressBar() {
        this.MaxValue = 100;
        this.BarWidth = 30;
        this.Label = "progress";
    };
    ProgressBar.prototype.Update = function(value){
        var rate = value/this.MaxValue;
        var val = Math.ceil(Math.min(Math.max(1,rate*this.BarWidth),this.BarWidth));
        var barComplete = Array(val+1).join("=");
        var barIncomplete = Array(this.BarWidth-val+1).join("-");
        var isProcessing = 1>rate;
        WSH.StdOut.Write("\r"+this.Label+" ["+barComplete+barIncomplete+"] "
            +(isProcessing?(rate*100).toFixed(2)+"%":"complete\n"));
        return isProcessing;
    };
    ProgressBar.prototype.Complete = function(){this.Update(this.MaxValue)};
    return ProgressBar;
}());

使い方

進捗状態を更新するときは、Update
強制的に完了にしたければ、Completeメソッドを呼びます。

WSH.Quit(function()
{
    try{
        var prog = new ProgressBar();
        {
            prog.Label = "サンプルA:整数\t\t";
            for (i=0; prog.Update(i); i+=2) {
                WSH.Sleep(100);
            }
        }
        {
            prog.Label = "サンプルB:浮動小数\t";
            prog.MaxValue = Math.PI;
            for (i=0; prog.Update(i); i+=0.1) {
                WSH.Sleep(100);
            }
        }
        {
            prog.Label = "サンプルC:バーの幅変更\t";
            prog.BarWidth = 50;
            for (i=0; i < prog.MaxValue; i+=0.1) {
                prog.Update(i);
                WSH.Sleep(100);
            }
            // 強制する場合
            prog.Complete();
        }
    }
    catch(e){
        WSH.Echo(e.name + ":" + e.message);
        return -1;
    }
    finally{
        // 3秒待機
        WSH.Sleep(3000);
    }
    return 0;
}());

おわりに

時間のかかる処理ではプログレスバーを出す方が親切ですが、
コンソール画面(CUI, CLI)では、ログを出していればとりあえず処理が進んでいるのが目視できるのでプログレスバーは必須では無いと思います。

WSH wsf の resource を外部ファイルに切り出す(WSC を使った実装の分割)

はじめに

WSH(.wsf) では、<resource>タグを使って任意のテキストデータをスクリプトに埋め込みできます。
しかし、設定ファイル用途で使用したり、ある程度容量が増えてきたりすると、外部ファイルに分割したくなります。

こういった場合ファイルの分割は、Windows Script Component (WSC) の仕組みを使って実現できます。

WSC は、WSF形式のスクリプトをCOMコンポーネントとして扱えるようにするテクノロジーですが、
モニカという機能を使って、レジストリに登録すること無くコンポーネントを利用できます。

つまり、<resource>タグごとコンポーネント化することで、外部ファイルに切り出せるということです。

やり方

モニカを使ったコンポーネントの取得には、
WSHJScriptVBScript)が公開している GetObject 関数の引数に script:<wscの絶対パス> を指定します。
wscのパスは、相対パスだとエラーになります。
その他、拡張子は wsc である必要は無く xml 形式として誤りが無ければ任意の拡張子が指定可能です。(サンプルでは、拡張子を xml とします。)

sample.bat(スクリプト側) resource.xml(リソース側) は、同じディレクトリに配置して動かしています。

Workspace
├─sample.bat        
└─resource.xml      

スクリプト

簡素化のために、bat ファイルに埋め込んでいますが、処理内容はシンプルです。
バッチでプロセス環境変数に、wsc(resource.xml) のパスをsetしておいて、
JScriptで、setした環境変数を読み取っています。

<!-- :
@echo off&title %~n0&setlocal&pushd %~dp0

rem リソース(wsc)のパス
set resource_name=resource.xml
set resource_path=%~dp0%resource_name%
set resource_moniker=script:%resource_path%

rem job実行
cscript //nologo "%~f0?.wsf" %* || pause

popd&endlocal&exit /b
-->
<job>
<object id="shell" progid="WScript.Shell"/>
<script language="JScript">
WSH.Quit(function()
{
    try{
        // リソース読み込み
        var procEnv = shell.Environment("Process");
        var rc = GetObject(procEnv("resource_moniker"));
        
        // とりあえず、表示
        var result = rc.Get("sln");
        WSH.Echo(result);

        // (ここで、リソースを使ってやりたい処理など)
    }
    catch(e){
        WSH.Echo(e.name + ":" + e.message);
        return -1;
    }
    finally{
        // 表示確認のため、3秒待機
        WSH.Sleep(3000);
    }
    return 0;
}());
</script>
</job>

リソース側

<resource id="識別子"><![CDATA[]]></resource> の間が、埋め込むテキストです。
拡張子を .wscでなく、.xmlにしているのは、ダブルクリックでレジストリ登録が走らないようにするためのフェイルセーフ。

<?xml version="1.0" encoding="utf-16" ?>
<component>
<!-- 下記2行で、リソース取得関数を公開 -->
<public><method name="Get"><PARAMETER name="id"/></method></public>
<script language="JScript">function Get(id){return getResource(id)}</script>

<!-- 適当なリソース -->
<resource id="sln"><![CDATA[
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.28010.2016
MinimumVisualStudioVersion = 10.0.40219.1
Global
    GlobalSection(SolutionProperties) = preSolution
        HideSolutionNode = FALSE
    EndGlobalSection
    GlobalSection(ExtensibilityGlobals) = postSolution
        SolutionGuid = {753BEE69-EAF9-44D4-86E7-78DDC490BA05}
    EndGlobalSection
EndGlobal
]]></resource>
</component>

あとがき

今回は、任意のディレクトリ構成でVisualStudio用のプロジェクトを作成するスクリプトで使用した方法の備忘録です。
設定ファイル程度であれば htmlfile オブジェクトから、JSONオブジェクトが取れるので.jsonで取り扱うことも出来ますが、
複数の既成フォーマットをひとまとめにしておくケースでは、テキストリソースの方が都合が良い訳です。

一応別解として、ES3程度の機能しかないJScriptでも正規表現を駆使したテクニックで、
ヒアドキュメントのような事ができるので、
<script>タグのsrc属性でリソース分割は可能ではありますが、これはランタイムで本来不要なテキストの加工が走るので好みません。

そういうわけで、WSCを使った方がパフォーマンス的にもコードの読みやすさとしても、スマートになるかと思います。

終わり。

C++ CLI Processクラスから HINSTANCE を取得する

メモ

C++/CLI 環境で、Process クラスで起動したプログラムのインスタンスハンドルを取得する方法。
Handleプロパティが、そのまま HINSTANCE として扱える。

実装

// 「ペイント」を起動
auto proc = gcnew System::Diagnostics::Process();
proc->StartInfo->FileName = "mspaint.exe";
auto result = proc->Start() && proc->WaitForInputIdle();
if (!result)
{   // 起動に失敗
    return;
}

// インスタンスハンドルを取得
auto hInst = static_cast<HINSTANCE>(proc->Handle.ToPointer());

JScript.NET をバッチに埋め込む方法

はじめに

Microsoft製のドマイナー言語、JScript .NET をバッチファイルに埋め込んで実行する方法です。

JScript .NET は、スクリプト言語である WSHJScript の後継的な位置づけでありながら、コンパイル必須という扱いづらい言語なので、
コンパイル処理と実行を一度に処理できるように、バッチファイルに埋め込んでしまおうというお話。

実装

おまじない部分

バッチに埋め込んで実行するためのコード。
コンパイルに成功したら実行するだけ。

@if(0)==(0) for /f "delims=" %%i in ('where /r %windir%\Microsoft.NET jsc.exe^|sort') do @set jsc=%%i
@"%jsc%" /nologo /out:"%~dp0%~n0.exe" "%~f0"&&"%~dp0%~n0.exe" %*
@exit /b&@end

バッチをUTF-8で保存する場合

コンパイラ文字コードを知らせるため、/codepage オプションを指定します。
UTF-8のコードページは、65001です。

@"%jsc%" /nologo /codepage:65001 /out:"%~dp0%~n0.exe" "%~f0"&&"%~dp0%~n0.exe" %*

多重コンパイルしたくない場合

実行毎にコンパイルが走ると、その分起動に時間がかかってしまうので、
実行ファイル名にハッシュ値を入れて、コンパイルが必要かどうか判定します。

@if(0)==(0) echo off&for /f "delims=" %%i in ('certutil -hashfile "%~f0"^|find /v ":"') do set app_path=%~dp0%~n0[%%i].exe
if not exist "%app_path%" del "%~dp0%~n0[*].exe">nul 2>&1&for /f "delims=" %%i in ('where /r %windir%\Microsoft.NET jsc.exe^|sort /r') do "%%i" /nologo /out:"%app_path%" "%~f0"&&goto:run||pause&goto:EOF
:run
"%app_path%" %*&exit /b &@end

// 以下に実装を書く
import System;

/**
 * エントリーポイント用
 */
Environment.Exit(function():int{
    var exitCode:int = 123;
    try{
        Console.WriteLine("Hello JScript.NET!!");
        Console.ReadKey();
    }catch(e){
        exitCode = int.MaxValue;
        print(e), Console.ReadKey();
    }finally{
    }
    return exitCode;
}());

その他

別解として、
実行ファイルを作らず powershell 経由でインメモリ実行する例。 qiita.com