COM時代のWindows Script Host(WSH)に対し、 .NET Framework時代にはPowerShell(PSH)がCLI環境として提供されており、 WSH同様PSHもVisual Studio .NETからの制御(Automation)が可能になっています。

ここでいうAutomationとは、VBやC#などEXE形式のプログラム内でWSHやPSHをCOM/.NETのオブジェクトとして扱い使用することを指し、 EXEは処理の一部をWSH/PSHに肩代わりさせることができます。 これの利点としては、

が挙げられ、高度で柔軟性の高い処理を実現できます。

PSHはWSH同様スクリプトによる処理に加えパイプラインという処理が用意されていますが、 複雑な処理を1行のパイプラインで記述するというのはそれなりの習熟が必要で、 関数(Function)も併用してAutomationしたいこともあるでしょう。 ここではPSH Automationで関数を定義・呼び出す方法を書いておきます。 なおVB.NETで説明してますが、C#でも同じはずです。読み替えてくださいませ。

準備

System.Management.Automation.dllを入手して、Visual Studio .NETのプロジェクトに参照登録します。 DLLの入手方法については、2つ挙げておきます。

参考

というか、そのまんま^^; 向こうのページがいつまであるか分からないので、保険のために。

コード

二つの方法がありますが、一長一短ですので好きなほうを使用すればいいでしょう。 パイプラインの方が機能的には制限が無い(オブジェクトを渡せる)ので慣れればこちら一本でこなせますが、 どうも私は全引数がしかも名無しで一個の$input変数に入っているというのは、気持ち悪くてねぇ(だからPerlの関数も好きではない^^;)

RunspaceIvokeを使う

引数が数字とか文字列とかコマンドラインに記述し易い場合は、こちらの方が値の渡しが楽です。

  1. RunspaceConfigurationのインスタンスAを作成する
  2. A.Scripts.Append()で関数を定義する
  3. Aを引数にRunspaceInvokeインスタンスBを作成する
  4. B.Invokeメソッドで関数を呼ぶ
  5. 返値をFor Eachなどで取り出す

関数を呼ぶ際はRunspaceInvokeインスタンスを作成後、Invokeメソッドに"関数名 引数1 引数2 ..."と引数込みの文字列を付けて呼び出します。 よってこの方法を使う場合、引数を文字列内に記述できないといけません。 Visual Studio側で作成したオブジェクトを渡したい場合は、次のパイプラインを使用することになります。

サンプルコード
Dim runspaceConfig As RunspaceConfiguration = RunspaceConfiguration.Create()

Dim scripts As RunspaceConfigurationEntryCollection(Of ScriptConfigurationEntry) = runspaceConfig.Scripts
scripts.Append(New ScriptConfigurationEntry("add", "param([double]$a,[double]$b);$a+$b"))

Dim runspaceInvoke As New RunspaceInvoke(runspaceConfig)
Dim results As ObjectModel.Collection(Of PSObject) = runspaceInvoke.Invoke("add 1 8")

For Each result As PSObject In results
  Console.WriteLine(result.ImmediateBaseObject.ToString)
Next

パイプラインを使う

引数の引渡しのために、呼び出し側・関数側双方で手数が増えますが、文字列では表現できないオブジェクトを渡せるという代え難い機能が使えます。

  1. RunspaceConfigurationのインスタンスAを作成する
  2. A.Scripts.Appendで関数を定義する
  3. Aを引数にRunspaceFactory.CreateRunspace()でRunspaceインスタンスBを作成する
  4. B.Open()を呼ぶ
  5. B.CreatePipeline()でPipelineインスタンスCを作成する
  6. C.Commands.Add()で呼び出す関数を記述する
  7. C.Input.Write()で引き渡したいオブジェクトを登録する
  8. C.Invoke()を呼ぶ
  9. 返値をFor Eachなどで取り出す
  10. B.Close()を呼ぶ

引数は特殊変数$inputの中に収められるので関数はここから値を得ます。中身はEnumlatorなのでforeachなどで取り出します。 引数を全部足すとかいう関数ならいいのですが、そうでなければforeachでどういう順番に値が取り出せるのか気になるところ。 C.Input.Write()でオブジェクトを入れた順番だと思います・・・多分^^; 万全を期すなら、

になるでしょうか。何でもかんでもクラスとして定義する.NET Frameworkのやり方や、他のCmdletとの連携を考えるとクラスを定義したほうがよさそうです。

ここではCommands.Addでひとつの関数を呼び出してますが、もちろん|で複数のCmdletなどを繋いでもかまいません。これはRunspaceInvokeを使う場合でも同じです。

最後の結果を取り出すところは、RunspaceInvokeを使う場合と同じです。

サンプルコード
Dim runspaceConfig As RunspaceConfiguration = RunspaceConfiguration.Create()

Dim scripts As RunspaceConfigurationEntryCollection(Of ScriptConfigurationEntry) = runspaceConfig.Scripts
scripts.Append(New ScriptConfigurationEntry("add", "$a=0;foreach($b in $input){$a=$a+$b};$a"))

Dim runspace As Runspace = RunspaceFactory.CreateRunspace(runspaceConfig)
runspace.Open()

Dim rsPipeline As Pipeline = runspace.CreatePipeline()
rsPipeline.Commands.Add("add")
rsPipeline.Input.Write(1.0)
rsPipeline.Input.Write(8.0)

Dim results As ObjectModel.Collection(Of PSObject) = rsPipeline.Invoke

For Each result As PSObject In results
  Console.WriteLine(result.ImmediateBaseObject.ToString)
Next

関数の引数定義方法

PowerShellでは関数の宣言の書式として、
  1. func1($a,$b){$a+$b}
  2. func1{param($a,$b);$a+$b}
  3. func1{$args[0]+$args[1]}
の3つがありますが、RunspaceConfiguration.Scripts.Append()では、最初の方法は使えないようです。 PowerShell上で最初の方法で定義したとしても、内部では2番目の方法で記録されており、 PowerShellを起動して下記のコマンドを打ち込んで確認できます。
PS> function add([double]$a, [double]$b){$a+$b}
PS> type function:add
param([double]$a, [double]$b) $a+$b
ここで出てくる内容がPowerShellの正規の関数表現だということなのでしょう。 普段最初の方法で関数を定義している場合、一度PowerShell上で定義したりdir function:で出てくる関数を適当に選んで、 type function:関数名で確認するといいでしょう。

引数はRunspaceInvoke、パイプラインどちらの方法でも指定可能です。 パイプラインの場合には、パイプラインから入れる全オブジェクトに共通の指定を引数で渡すと良いでしょう。 ただし文字列で表現できないような指定ならば、パイプラインからオブジェクトを入れる必要があります。

参考

2007/9/19
公開
2007/10/2
関数の定義方法を別セクションに分離 & 加筆
2008/12/28
冒頭の説明を加筆