PowershellとUWSCの連携(その2)

PowershellからUWSCにコマンドを実行させる。
前回はIE経由な上に、PowershellUWSCの二つのスクリプトが必要でした。
PowershellとUWSCの連携 - じゅんじゅんのきまぐれ
今回は、名前付きパイプを使用してIEを排除した上に、メンテするスクリプトを一つにしました。
、、、ファイルは二つ必要だけど、、、。(二つ目は勝手に作る)



スクリプト

.ps1ファイルとして保存してください。
基本、Powershellスクリプトです。

#=0 // PowershellとUWSCスクリプトを同居させるおまじない
# = EVAL("UwscMain()")
function TextBlock {}
TextBlock _pre_psh
function EndTextBlock {}
function Fend {}
function Result {}
EndTextBlock
Result = 0
Fend

#=0 // UwscMainを書く
TextBlock _uwsc_main_pre
$uwsc = @'
EndTextBlock

// ここはUWSCの王国

FUNCTION UwscMain()
	DEF_DLL CloseHandle(dword): bool: kernel32.dll
	CONST INVALID_HANDLE_VALUE = $FFFFFFFF
	DEF_DLL WaitNamedPipeW(wstring,dword): bool: kernel32.dll
	DEF_DLL CreateFileW(wstring,dword,dword,dword,dword,dword,dword): dword: kernel32.dll
	CONST GENERIC_ALL = $10000000
	CONST OPEN_EXISTING = 3
	DEF_DLL ReadFile(dword,var wstring,dword,var dword,dword): bool: kernel32.dll
	DEF_DLL WriteFile(dword,wstring,dword,var dword,dword): bool: kernel32.dll

	CONST __pipe_name = "JunePshUwscPipe"
	CONST __pipe_full_name = "\\.\pipe\" + __pipe_name
	CONST __size = 1024

	// パイプに接続する
	DIM __count = 0, __hPipe = INVALID_HANDLE_VALUE, __cmd = 30
	WHILE !WaitNamedPipeW(__pipe_full_name, 0) AND __count < __cmd
		SLEEP(0.1)
		__count = __count + 1
	WEND
	IFB __count < __cmd THEN
		__hPipe = CreateFileW(__pipe_full_name, GENERIC_ALL, 0, NULL, OPEN_EXISTING, 0, NULL)
		IFB __hPipe = INVALID_HANDLE_VALUE THEN
			PRINT "Err: CreateFile."
		ENDIF
	ELSE
		PRINT "Err: WaitNamedPipe."
	ENDIF

	RESULT = TRUE
	IFB __hPipe <> INVALID_HANDLE_VALUE  THEN
		// パイプから読み込む
		WHILE TRUE
			__cmd = FORMAT(CHR(0), __size / 2)
			IF !ReadFile(__hPipe, __cmd, __size, __count, 0) THEN BREAK
			__cmd = COPY(__cmd, 1, __count / 2)
			IF __cmd = "exit" OR __cmd = "quit" THEN BREAK
			TRY
				__cmd = "" + EVAL(__cmd)
			EXCEPT
				__cmd = TRY_ERRMSG + "<#CR>" + __cmd
			ENDTRY
			IF LENGTH(__cmd) = 0 THEN __cmd = CHR(0)
			IF !WriteFile(__hPipe, __cmd, LENGTH(__cmd) * 2, __count, 0) THEN BREAK
		WEND

		// パイプを閉じる
		CloseHandle(__hPipe)
	ENDIF

FEND


// 適当なTextBlock内はPowershellの王国
TextBlock _uwsc_main_past
'@

$uwsc = New-Object PSObject
$uwsc | Add-Member NoteProperty 'path' 'D:\yama\tools\cmd\bin\uwsc\uwsc.exe'

# 自スクリプト名+'.uws'ファイルの同一性チェック。だめなら再生成する
$uwsc | Add-Member NoteProperty 'file' ($MyInvocation.MyCommand.Path + '.uws')
if(!(Test-Path -Path $uwsc.file) -or $(Compare-Object -ReferenceObject $(Get-Content $MyInvocation.MyCommand.Path) -DifferenceObject $(Get-Content $uwsc.file) | Measure-Object).Count -ne 0) {
	if(Test-Path -Path $uwsc.file) {
		Remove-Item $uwsc.file
	}
	Copy-Item -Path $MyInvocation.MyCommand.Path -Destination $uwsc.file
}

$uwsc | Add-Member ScriptMethod 'Create' {
	# サーバーパイプを作成し、uwscを起動する
	$this | Add-Member NoteProperty -Force 'pipe' (New-Object System.IO.Pipes.NamedPipeServerStream 'JunePshUwscPipe', InOut)
	$this | Add-Member NoteProperty -Force 'buf' (New-Object byte[] 1024)
	$this | Add-Member NoteProperty -Force 'bin' (Start-Process -PassThru $uwsc.path $uwsc.file)
	$this.pipe.WaitForConnection()
}
$uwsc.Create()
$uwsc | Add-Member ScriptMethod 'Dispose' {
	# サーバーパイプを終了する
	$this.pipe.Close()
	$this.bin.Kill()
}
$uwsc | Add-Member ScriptMethod 'In' {
	while($true) {
		Write-Host -NoNewline 'UWSC> '
		$cmd = [Console]::in.ReadLine()
		if($cmd -match '^(exit|quit)') {
			break;
		}
		$cmd = $this.Eval($cmd)
		Write-Host $cmd
	}
}
$uwsc | Add-Member ScriptMethod 'Eval' {
	if($args.length -gt 0) {
		$wb = [System.Text.Encoding]::Unicode.GetBytes($args[0])
		if($wb.Length) {
			$this.pipe.Write($wb, 0, $wb.Length)
			$len = $this.pipe.Read($this.buf, 0, $this.buf.Length)
			[System.Text.Encoding]::Unicode.GetString($this.buf, 0, $len)
		}
	}
}

EndTextBlock

使い方

上記スクリプトPowershellから「. (スクリプトファイル)」として実行します。
UWSCのプロセスが起動し、名前付きパイプでPowershellと接続、$uwscオブジェクトが生成されます。
$uwsc.EvalでEVALを実行するか、$uwsc.In()でUWSCモードに入り、インタラクティブに使用します。
終了は、$uwsc.Dispose()。
Dispose後再開したい場合は、$uwsc.Create()で再度使用可能です。

動作

  1. 実行すると、「(スクリプトファイル).uws」ファイルの中身を確認し、自身と同じでない場合はコピーします
  2. $uwscオブジェクトに、名前付きパイプやuwscプロセスを作成します
  3. 「$uwsc.In()」すると、プロンプトが「UWSC> 」となり、UWSCでEVALする文字列を受け付けます
  4. UWSCモードは「exit」か「quit」で抜けます
  5. $uwsc.Evalは引数一つをUWSCでEVALし、結果を返します

UWSC主導と違って、Powershellが終わると、Pipeが閉じてUWSCは終了します。

同一内容のトリック

残念ながら、Powershellは「.ps1」、UWSCは「.uws」と拡張子が固定のため、同一ファイルにはできませんでした。
使いどころはないけど、同一内容で成立させているおまじないを解説します。


ポイントは、最初の部分。

#=0 // PowershellとUWSCスクリプトを同居させるおまじない
# = EVAL("UwscMain()")
function TextBlock {}
TextBlock _pre_psh
function EndTextBlock {}
function Fend {}
function Result {}
EndTextBlock
Result = 0
Fend
最初の二行

Powershellからすると、「#」で始まるのはコメントです。
UWSCは「#」という変数が取れるため、どちらも代入文です。
一行目はどちらにしてもコメントです。
二行目は、UWSCはUwscMain関数を実行します。

三行目「function TextBlock {}」

Powershellは、TextBlockという関数の宣言です。(中身空なので実行しても何もしない)
UWSCは、TextBlockという関数の開始行です。

四行目「TextBlock _pre_psh」

Powershellは、「_pre_psh」を引数として先ほど定義したTextBlock関数を実行します。(動作なし)
UWSCは、_pre_pshという名前のテキストブロックの開始行です。

五〜七行目

Powershellは、EndTextBlock/Fend/Resultという関数の宣言です。
UWSCは、テキストブロックの中。

八行目「EndTextBlock」

Powershellは、EndTextBlock関数の実行。
UWSCは、テキストブロックの終わり

九行目「Result = 0」

Powershellは、「=」と「0」の二つの引数でResult関数を実行。
UWSCは、戻り値の定義

十行目「Fend」

Powershellは、Fend関数の実行。
UWSCは、TextBlock関数の終了行。


といった感じで、お互いがそれぞれに解釈して処理します。
UWSCが「#」という変数、「function TextBlock {}」という関数開始行を許容することが大きいです。
ここまでくると、「TextBlock」は、Powershellには動作なしの関数、UWSCでは複数行文字列定数になるため、一方には文字列定数で、もう一方には意味あるスクリプト、という状況が作れるようになります。
具体的には、

TextBlock _uwsc_main_pre
$uwsc = @'
EndTextBlock

// UWSCスクリプトはここ

TextBlock _uwsc_main_past
'@

# Powershellスクリプトはここ

EndTextBlock

上のスクリプトは、Powershellは、TextBlock関数の実行と$uwsc変数の定義とEndTextBlock関数の実行。
($uwscに「EndTextBlock〜TextBlock _uwsc_main_past」という文字列が入る)
UWSCは、二つのテキストブロックとその間のスクリプト、ということになります。


どっちかの拡張子が固定でなければ、ファイル一つでできたのだけど、、、残念!

雑感

私がスクリプト開発時に欲しいのは、コード補完と即時実行(イミディエイトウインドウ)。
コードスニペットの実行では、環境が保全されないので、ちょっと物足りないのです。
スクリプトを書くときは、その場で、試行錯誤をしたいのです。