読者です 読者をやめる 読者になる 読者になる

Code Aquarium

minazoko's blog -*- 水底のブログ -*-

(PowerShell)ScriptBlockで文脈を作る。あと年賀状も。

PowerShell

この記事は,PowerShell Advent Calendar 2013の7日目の記事です。
なお、私のPowerShell環境は V3 です。以下すべて検証は V3でのみ行っています。

ScriptBlockで文脈を作る

ScriptBlockで高階関数を作るとコンテキストをカスタマイズしているかのようなコードが書けます。
PowerShellの冗長性をなんとか回避できないかと奮戦している最中なのですが、その中途半端な成果物をお見せします。

first 関数

function first ([ScriptBlock]$Body) {
    $input | select -First 1 | % { & $Body }
}

Select -First 1 と書くのが面倒なので関数にしたものです。
一要素しかないオブジェクトをパイプに流して処理する場合にもよく使っています。

new Uri "http://www.google.com" | first { $_.Host } | ...

ForEach-Object でもいいんですが、一要素だけを期待していることを明示できるのがメリットです。

this 関数(フィルタ)

ScriptBlock $Body を実行してその戻り値は捨てて、パイプライン引数をそのまま返します。

filter this ([ScriptBlock]$Body){
    [void](& $Body)
    $_
}

これを使うと、プロパティの多いオブジェクトの初期化コードをthis関数を通して設定することができるようになります。

$form = new System.Windows.Forms.Form `
| this {
    $_.Text = "sample"
    $_.Size = new System.Drawing.Size 100,100
    $_.Controls.Add((new System.Windows.Forms.Label `
    | this {
        $_.Text = "Hoge"
        $_.Dock = [System.Windows.Forms.DockStyle]::Fill
        $_.BackColor = [System.Drawing.Color]::LightBlue
    }))
}
$form.ShowDialog()

this の戻り値は $_ なので $form 変数に受けずに、そのまま this の戻り値に対してメソッドコールすることもできます。
一時変数を使うことなくオブジェクトの引き回しがしやすくなります。
スクリプトブロックの返却値はすべて破棄されるので、> $null や [void]で値を返さないよう気を使う必要もありません。
一応 $_ は型を保ってくれる*1ことが多く、PowerShell_ISEでインテリセンスも結構効いてくれます。上の様にネストすることもできます。
ただ、$_ スコープの問題なのか、モジュールファイルにこの関数を移動するとうまく機能ないみたいです(ダメじゃん)。

ちなみに

Set-Alias new New-Object

は私の中ではデフォルトです。

aps (New-ApplyContext) 関数

関数を適用する関数です。
パイプラインから Invoke 可能なオブジェクトを受け取り、$Bodyの返却値で Invoke します。$Bodyの返却値が複数の場合ひとつずつ順番に適用します。

filter New-ApplyContext([ScriptBlock] $Body, [Switch] $PassResult){
    $private:It = $_
    if($PassResult){
        $Body.Invoke() | % { $It.Invoke($_) }
    }else{
        $Body.Invoke() | % { $It.Invoke($_) } > $null
        $It
    }
}

Set-Alias aps New-ApplyContext

こんな風に使います。

[System.Reflection.Assembly]::LoadFile | aps {
    "AAA.dll"
    "BBB.dll"
    "CCC.dll"
    "DDD.dll"
}

PowerShellでは、ScriptBlock内のすべての文に、C#で言うところの yield return がついてるような感じになっています。
上のように書くと各行の評価値を要素にした配列がScriptBlockの返却値になります。それをまとめて、LoadFileしています。
aps はパイプライン引数をそのまま返すので、こうやってチェーンすることもできます。

[System.Reflection.Assembly]::LoadFile | aps {
    "AAA.dll"
    "BBB.dll"
} | aps {
    "CCC.dll"
    "DDD.dll"
}

また オプションスイッチ -PassResult を付けると、パイプライン引数ではなく、$Body の評価値をパイプにPassします。

new System.Random `
| first {$_.Next} `
| aps {
    10
    10
    100
    100
} -PassResult
7
0
95
64

単純な適用の場合でも、これを使うと括弧の数を減らすことができたります。

$list = new System.Collections.ArrayList

$list.Add((new System.Windows.Point 10,20))

$list.Add | aps {new System.Windows.Point 10,20}

ただ、対象メソッドがオーバーロードされていたり、スクリプトブロックを絡めたりするとうまくいかないことも多いです。限定的には結構便利なんだけどやっぱり中途半端。

use ( New-DisposeOnLeaveContext) 関数

これは説明不要。C# で言うところの using ブロックみたいなものが欲しかった。

function New-OnLeaveContext ([ScriptBlock] $Body, [ScriptBlock] $OnLeave){
    try {
        & $Body
    }finally{
        & $OnLeave
    }
}

function New-DisposeOnLeaveContext ([System.IDisposable] $it, [ScriptBlock] $Body){
    New-OnLeaveContext $Body {
        if( $it ){ $it.Dispose() }
    }
}

Set-Alias use New-DisposeOnLeaveContext
use ($ins = new System.IO.StreamReader "infile.txt") {
use ($outs = new System.IO.StreamWriter "outfile.txt") {
    ...
}}

このようにネストもできますが、スクリプトブロックの { } は省略できないので、最後にまとめて use の数だけ閉じましょう。
他にも、 同じ仕組みでFinally時に Close, Quit , [System.Runtime.InteropServices.Marshal]::FinalReleaseComObject などを実行する関数も作れます。
一応ちゃんと機能するようですが、エラーが起きたときに二次災害で Disposeが例外を投げたりして、よくわからない状況になりがちです。まぁそれは、PowerShellに限った話ではありませんが。
あと、 trap とかと併用した時にどうなるか、あまり自信はありません。

使ってみた。

以上のいろいろ怪しい関数群を使うとどうなるのか実演と実益を兼ねて、年賀状を作りました。
iTextSharpというPDFライブライを使い年賀状の宛名印刷用PDF作成スクリプトです。
こんな感じのPDFが作れました。
f:id:minazoko:20131207211725p:plain:w500

スクリプトコード

コード全体はこちらにあります。
最後の郵便番号のあたりはかなりやっつけです。われながら酷い。

iTextSharp については C# のライブラリを試行錯誤しながら使うだけでした。当初情報が少なくて苦労しましたが、オリジナルの iText公式サイトにJavaのサンプルが豊富で、そちらが参考になりました。

iTextでは座標が左下を原点とします。しかも縦書きするには右から座標決定する必要があり混乱することが多かったです。
ややこしいので、左上を原点とする馴れ親しんだ座標系で System.Drawing.RectangleF を作って、そこから iTextの座標系へ変換する関数(ConvertTo-iRectangle)を作りました。これでiTextの座標系は意識しないで座標設定できるようになりました。
また、iTextのスケールはインチとポイントが混在していてここも混乱の元でした。
基本的にはポイントで位置管理をして、必要な時にインチに変換するような方針で収集を付けました。

(訂正 2013/12/8 : 勘違いでした。インチ指定は必要ありませんでした。リンク先gistのコードも訂正します。)

終わりに

皆さんもスクリプトブロックで、自分だけの文脈(パーソナルコンテキスト)を手に入れましょう。
あと、年賀状は早めに出しましょう。

*1:おかしくなることもある