デリゲートっぽいこと

ある処理に、外から計算ロジックなどをインジェクションしたい!

そんなときPowerShellではスクリプトブロックを処理や関数に引数で渡すことができます。インジェクションすることのメリットは割愛しますが、とにかくすごい効率的にプログラムを書くことができます。

いきなり使用例(基本)

例えば、時間が朝で、男の子へのあいさつは「おはよう、○○君」で、時間が夜で、女の子へのあいさつは「★○○★さん、こんばんは」にしたい場合

# 時間は朝、男の子
$jikan = "asa"
$seibetsu = "otoko"

# あいさつ
if ($jikan -eq "asa") {
    [ScriptBlock]$aisatsu = {
        param($name)
        "おはよう、$name"
    }
}
else {
    [ScriptBlock]$aisatsu = {
        param($name)
        "$name、こんばんは"
    }
}

# 敬称
if ($seibetsu -eq "otoko") {
    [ScriptBlock]$keisyo = {
        param($name)
        "$($name)君"
    }
}
else {
    [ScriptBlock]$keisyo = {
        param($name)
        "★$($name)★さん"
    }
}

# あいさつ文の表示
&$aisatsu (&$keisyo "翼")
結果
$jikan = "asa" ; $seibetsu = "otoko"
おはよう、翼君

$jikan = "asa" ; $seibetsu = "onna"
おはよう、★翼★さん

$jikan = "yoru" ; $seibetsu = "onna"
★翼★さん、こんばんは

サンプルなので効率が良いかと言われたら微妙ですが、あいさつ文の表示処理では、朝とか夜とか、男の子とか女の子とか気にしないでメッセージを出力できます。

あいさつ文表示処理は、$aisatsuと$keisyoのスクリプトブロックに処理を委譲しているため、メッセージを出力することだけに集中できます。

少し解説

スクリプトブロックは$abc = {}の形式で定義します。

$abcの状態ではスクリプトブロックが格納された変数を表します。そのため、関数に引数として渡すことができます。

スクリプトブロックを実行するには、&$abcのように先頭に&を付与します。&は関数と同じ(スコープの)扱いでスクリプトブロックを実行するという意味になります。

[ScriptBlock]は省略可能です。上記のサンプルの場合は明らかにスクリプトブロックとわかるため、省略してしまってよいでしょう。ただし、関数などでスクリプトブロックを受け取るときは[ScriptBlock]を記載しておいたほうがよいでしょう。

上記のサンプルの「&$aisatsu (&$keisyo $name)」を、下記のように関数で記述した場合は、関数内で$aisatsuがスクリプトブロックだとわかるように型を書いたほうがよいでしょう。

※サンプルのために変な場所に関数を記述していますが、別ファイルにするか、ファイルの先頭に記述しましょう。

# あいさつ文の表示

function DispAisatsu {
    param([ScriptBlock]$aisatsu, [ScriptBlock]$keisyo, $name)

    &$aisatsu (&$keisyo $name)
}

DispAisatsu $aisatsu $keisyo "翼"

パイプ処理でスクリプトブロックを使う

例えば、上下と左右の2つの変数があり、上下が「上」の場合はファイルの先頭2行を取得し、上下が「下」の場合はファイルの最終2行を取得します。

左右が「左」の場合は取得したレコードの先頭文字を取得し、左右が「右」の場合は取得したレコードの最終文字を取得します。

$joge = "ue"
$sayu = "hidari"

# スクリプトブロックはデフォルトendブロックとして動作する。

# 上下
if ($joge -eq "ue") {
    $joge_proc = {end{$input | Select-Object -First 2}}
}
else {
    $joge_proc = {end{$input | Select-Object -Last 2}}
}

# 左右
if ($sayu -eq "hidari") {
    $sayu_proc = {process{$_.substring(0,1)}}
}
else {
    $sayu_proc = {process{$_.substring($_.length-1)}}
}

Get-Content "C:\aaa\test.txt" | &$sayu_proc | &$joge_proc   
c:\aaa\test.txt の内容
ABCD
BCDE
CDEF
DEFG

結果
$joge = "ue" ; $sayu = "hidari"
A
B

$joge = "shita" ; $sayu = "hidari"
C
D

$joge = "shita" ; $sayu = "migi"
F
G

少し解説

スクリプトブロックにはbegin process endがあって、processはパイプ処理でファイルのレコード1件1件に対して処理を行いたい場合はprocessを使う。1つのスクリプトブロックでprocessとendの両方を使うことも可能。

ファイルの先頭行など特定の行だけ抜き出したい場合は、ファイル全体に対して処理を行わないといけないため、endを使う。endはパイプ処理の最後に1回呼び出される。

$_や$inputは自動変数で、$_はprocessで使用でき、ファイルなどのカレントのレコード1件などが設定される。

$inputはprocessやendで使用でき、イテレータ的なもの(ArrayListEnumeratorSimple)が渡される。

processで使用した場合は$_と変わらないが、$inputは一度参照するとクリアされるため、次のレコードを読み込むなどして再度セットされないと参照できない。processでは$_を使用したほうがよさそうです。

なお、endで使用する場合も$inputは参照するとクリアされます。

Get-Content "c:\aaa\test.txt" | &{$input | Select-Object -First 1 ; $input | Select-Object -Last 1}
結果
ABCD
DEFG

-Firstで先頭行を参照し、先頭行はクリアされますが、最終行はそのままなので参照できます。

Get-Content "c:\aaa\test.txt" | &{$input | Select-Object -Last 1 ; $input | Select-Object -First 1}
結果
DEFG

-Lastで最後まで参照しているため、先頭行はクリアされて参照できません(たぶん)。

Get-Content "c:\aaa\test.txt" | &{$input | Select-Object -First 1 ; $input | Select-Object -First 1}
結果
ABCD
BCDE

-Firstで1行目がクリアされ、もう一度-Firstを行った際はもともとの2行目が1行目になり取得されたと思われます。

パイプ処理は、processとendどちらがよいか

processでendのようなことはできませんが、endでprocessのようなことはできます。

Get-Content "c:\aaa\test.txt" | &{end{$input.substring(0,1)}}
結果
A
B
C
D

と出力され、processと同等のことができます。

しかし、processでできるときはprocessでおこなったほうがよいです。

実験したところ、endは、パイプからの入力をすべて受け取ってから処理を開始するため、メモリを大量に使います。

Get-Content "c:\aaa\test.txt" | &{end{$input | Select -Last 1}} | &{process{$_.substring(0,1)}}

上記は、c:\aaa\test.txtの内容をすべてメモリに格納し、そこから最終レコードを取得し、最終レコードを次の処理に渡し、次の処理は最終レコードの先頭文字を取得して出力します。

Get-Content "c:\aaa\test.txt" | &{process{$_.substring(0,1)}} | &{end{$input | Select -Last 1}}

上記は、c:\aaa\test.txtの内容を1件ずつ読み込み、レコードの先頭文字を取得して次のパイプ処理に渡します。
次のパイプ処理はendなので、すべてのレコードを受け取ってから最終レコードを出力します。

上の2つは結果は同じになりますが、上のほうがメモリを消費します(c:\aaa\test.txtに30MByteのファイルを指定した場合、上は100MByte程度メモリを消費し、下は30MByte程度メモリを消費しました。タスクマネージャでの確認のため精度は悪いです)