x64には存在しないScriptControlの代わり
x64環境には、ScriptControlが存在しない。
例えば、x64のPowershellでJScriptオブジェクトを受け取ってしまった場合、扱いに困る。
Get-Memberしても、何も返ってこない、、、。
ScriptControlがいれば、AddObjectして解釈できるのだけど。
ということで、方法を考えてみた。
追記 2015/03/05
ieに依存すると、外部プロセスとなるため、htmlfileに依存して内部プロセスにしたバージョンを書いた。
http://d.hatena.ne.jp/junjun777/20150305/x64_scriptcontrol
スクリプトと言えば
ブラウザ、それも通常は存在するIEでしょう、やはり。
IEならCOMオブジェクトで生成できます。
Powershellで書くなら
$js = New-Object PSObject $js | Add-Member NoteProperty "ie" (New-Object -ComObject InternetExplorer.Application) $js.ie.Navigate("about:blank") $js.ie.Document.IHTMLDocument2_write(@" <html><head> <meta http-equiv='X-UA-Compatible' content='IE=edge' /> <title>ie for powershell</title> <script> document.__js = { eval: function(code, p) { var ret = null try { ret = eval(code) } catch(e) { this.lastErrNo = e.number this.lastErrMsg = e.description throw e } return ret; }, get: function(obj, name) { return obj[name] }, set: function(obj, name, val) { obj[name] = val }, getAttrs: function(obj) { var ret = '' for(var a in obj) ret += ',' + a return ret.substr(1) }, parse: function(str) { return JSON.parse(str) }, stringify: function(obj, ind) { return JSON.stringify(obj, null, ind) } } </script> </head><body></body></html> "@) #$js.ie.Visible = $true $js | Add-Member NoteProperty "e" ([System.__ComObject].InvokeMember("__js", [System.Reflection.BindingFlags]::GetProperty, $null, $js.ie.Document, $null)) $js | Add-Member ScriptMethod "Eval" { if($args.length -gt 0) { $para = $null if($args.length -gt 1) { $para = $args[1] } $this.e.eval($args[0], $para) } } $js | Add-Member ScriptMethod "Get" { $ret = $null if($args.length -gt 1) { $ns = $args[1] if($ns -isnot [Array]) { $ns = $ns -split '\.' } $len = $ns.Length $ret = $this.e.get($args[0], $ns[0]) if($args.length -gt 2 -and $args[2] -and $ret -eq $null) { $ret = $this.e.eval("({})", $null) $this.e.set($args[0], $ns[0], $ret) } if($len -gt 1) { $ret = $this.Get($ret, $ns[1..($len-1)]) } } return $ret } $js | Add-Member ScriptMethod "Set" { if($args.length -gt 2) { $ns = $args[1] if($ns -isnot [Array]) { $ns = $ns -split '\.' } $len = $ns.Length if($len -gt 1) { $add = $true if($args.length -gt 3) { $add = $args[3] } $this.e.set($this.Get($args[0], $ns[0..($len-2)], $add), $ns[($len-1)], $args[2]) } else { $this.e.set($args[0], $args[1], $args[2]) } } } $js | Add-Member ScriptMethod "List" { $ret = @() if($args.length -gt 0) { $tar = $args[0] if($args.length -gt 1) { $tar = $this.Get($tar, $args[1]) } $as = $this.e.getAttrs($tar) -split "," foreach($a in $as) { $val = $this.Get($tar, $a) #if($val -is [__ComObject]) { $val = $this.List($val) } $ret += ,@($a, $val) } } return $ret } $js | Add-Member ScriptMethod "Run"{ if($args.length -gt 1) { $obj = $null if($args.length -gt 2) { $obj = $args[2] } $ns = $args[1] if($ns -isnot [Array]) { $ns = $ns -split '\.' } $len = $ns.Length if($len -gt 1) { ([System.__ComObject].InvokeMember($ns[($len-1)], [System.Reflection.BindingFlags]::InvokeMethod, $null, $this.Get($args[0], $ns[0..($len-2)]), $obj)) } else { [System.__ComObject].InvokeMember($args[1], [System.Reflection.BindingFlags]::InvokeMethod, $null, $args[0], $obj) } } } $js | Add-Member ScriptMethod "Dispose" { if($this.ie -ne $null) { $this.ie.Quit() $this.ie = $null } }
使い方
スクリプトを実行すると、$jsオブジェクトができます。
$jsオブジェクトには、二つのプロパティと、6つのScriptMethodがあります。
$js.ie
InternetExplorer.Application COMオブジェクトです。
Scriptの実行環境になります。
$js.e
ie.document.__jsを呼び出すのが面倒なので、エントリーポイントを確保しています。
$js.e自体は、eval/get/set/getAttrs/parse/stringifyという6つの関数を持っており、対応するScriptMethodに機能を提供しています。
IEも8以降なら、ネイティブJSONサポートらしいので、parse/stringify追加しています。(ポイントはmetaタグ)
stringifyの第二引数はインデント。
ちなみに、UWSCでScriptControl+json2.jsとIE8のparse/stringifyを繰り返したら、json2の勝ちだったという、、、。
誰が遅いかは未調査。
$js.Eval($code, $p)
$codeを$js.e.eval()に渡すだけです。
スクリプト実行環境にて、単純にevalされます。
引数を渡したい場合、$js.eに設定すればthisで参照できますが、面倒なので第二引数をpで参照できるようにしています。
$js.Eval("p * 8", 2)
は16を返してきます。
オブジェクトも渡せます。(JScriptオブジェクトなら、可能なことに気づきました)
エラーが発生すると、例外をそのままスローしますが、詳細はGetして確認してください。
$js.Get($js.e, "lastErrMsg")
エラー番号が欲しいなら、「$js.Get($js.e, "lastErrNo")」。
$js.Get($obj, $name)
対象オブジェクトのメンバー値を取得します。
オブジェクトも取得可能です。
$obj.a.bの値は、$js.Get($obj, "a.b")で取得できます。
$js.Set($obj, $name, $val)
対象オブジェクトの指定メンバーに値を設定します。
オブジェクトも設定可能です。
$obj.a.b=値は、$js.Set($obj, "a.b", 値)で設定できます。
この際、$obj.aが存在しなければ、オブジェクトとして追加したいのですが、、、上手く機能してません。
$js.List($obj)
オブジェクトの内容を二次元配列(のようなもの)で返します。
メンバー名と値のタプルイメージ。
$js.e.stringifyが使えるなら、不便なだけに思える、、、。
$js.Run($obj, $name, $paras)
オブジェクトのメソッドを実行します。
第三引数は与えるパラメーター。省略可能です。
基本的には必要のないメソッドかと思っていますが、$objのメソッドが直接呼べない時利用してください。
$js.Dispose()
実行環境を破棄します。
$js.e.parseや$js.Evalでオブジェクトを作成していた場合、使えなくなります。
(実体は実行環境にあるのですから)
IEに依存したくない場合
JScript.NetとInvokeMemberを使う。
ただしこの方法で、JScriptオブジェクトのメンバーを列挙する方法を見つけてない。
メンバーがわかっていれば扱える、というもの。
$source = @' import System; public class JSEnv { public var _ = {}; public function eval(code, p) { return eval(code); } } '@ $jsn = New-Object PSObject $jsn | Add-Member NoteProperty "c" ((Add-Type -Language JScript -TypeDefinition $source -PassThru)[1].GetConstructors()[0]) $jsn | Add-Member NoteProperty "e" ($jsn.c.Invoke($null)) $jsn | Add-Member ScriptMethod "Eval" { if($args.length -gt 0) { $para = $null if($args.length -gt 1) { $para = $args[1] } $this.e.eval($args[0], $para) } } $jsn | Add-Member ScriptMethod "Get" { $ret = $null if($args.length -gt 1) { $ns = $args[1] if($ns -isnot [Array]) { $ns = $ns -split '\.' } $len = $ns.Length $ret = ([System.__ComObject].InvokeMember($ns[0], [System.Reflection.BindingFlags]::GetProperty, $null, $args[0], $null)) if($args.length -gt 2 -and $args[2] -and $ret -eq $null) { $ret = $this.e.eval("p." + $ns[0] + "={}", $args[0]) } if($len -gt 1) { $ret = $this.Get($ret, $ns[1..($len-1)]) } } return $ret } $jsn | Add-Member ScriptMethod "Set" { if($args.length -gt 2) { $ns = $args[1] if($ns -isnot [Array]) { $ns = $ns -split '\.' } $len = $ns.Length if($len -gt 1) { $add = $true if($args.length -gt 3) { $add = $args[3] } ([System.__ComObject].InvokeMember($ns[($len-1)], [System.Reflection.BindingFlags]::SetProperty, $null, $this.Get($args[0], $ns[0..($len-2)], $add), $args[2])) } else { ([System.__ComObject].InvokeMember($args[1], [System.Reflection.BindingFlags]::SetProperty, $null, $args[0], $args[2])) } } } $jsn | Add-Member ScriptMethod "Run"{ if($args.length -gt 1) { $obj = $null if($args.length -gt 2) { $obj = $args[2] } $ns = $args[1] if($ns -isnot [Array]) { $ns = $ns -split '\.' } $len = $ns.Length if($len -gt 1) { ([System.__ComObject].InvokeMember($ns[($len-1)], [System.Reflection.BindingFlags]::InvokeMethod, $null, $this.Get($args[0], $ns[0..($len-2)]), $obj)) } else { [System.__ComObject].InvokeMember($args[1], [System.Reflection.BindingFlags]::InvokeMethod, $null, $args[0], $obj) } } }
IEを使うパターンとほぼ同等だけど、EvalはJScript.Net環境で実行される。
このため、IEやWSHのJScriptオブジェクトが上手く扱えない。
JSONの機能を入れたいなら、.Net 3.5以降になりますが、以下を追加すれば簡単です。
Add-Type -AssemblyName System.Web.Extensions $jsn | Add-Member NoteProperty "o" (New-Object System.Web.Script.Serialization.JavaScriptSerializer)
$jsn.o.DeserializeObjectでパースができて、$jsn.o.Serialize()でStringify(インデントは不可)
ただし、IEやWSHのJScriptオブジェクトはSerializeできません。
なお、JScript.Netのオブジェクトは何ら問題なくPowershellで扱えるので、場合によってはこっちが便利。
(シリアライズにはいろいろ問題がありますが)
JScriptオブジェクトを相手にしないなら、Get/Set/Runは不要。
Evalは同様に、第二引数をpとして参照できます。
$jsn.e内に持たせてしまいたい場合、「_」が自由に使えるオブジェクトとなります。
雑感
PowershellはJScriptオブジェクトを上手く扱えない、と書いたので解決してみた。
UWSCのpowershell関数をスピードアップする - じゅんじゅんのきまぐれ
わかってしまえば、JScriptオブジェクトを扱うのも、そんなに大変ではないです。
でも、JScriptオブジェクトを貰うより、JSON形式で貰ってJScript.Netオブジェクトにした方が、扱いが楽ですね。
JScriptオブジェクトを貰ってしまった場合、$jsn.o.DeserializeObject($js.e.stringify($obj,0))とJSON形式経由で、JScript.Netに輸入してしまえば、楽かもしれません。