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

Code Aquarium

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

(PowerShell) $inputはコンテキストが違うと別物である

PowerShell

$inputはコンテキストが違うと別物である

勘違いしていました。
下記の4つのパイプライン変数 $input は同じものなんだろうなと思い込んでいましたが、違うものでした。

function my1 {
    $input
}
function my2 {
    begin{ $input }
    process{ $input }
    end{ $input }
}

「違うもの」というのには2つの意味が含まれています。

非パイプライン関数とパイプライン関数の$input

上は私の造語ですが、パイプラインを意識しprocessを定義している関数を「パイプライン関数」そうでない関数を「非パイプライン関数」と思ってください。

次のようなコードで検証してみます。

function my1 {
    Write-Host "my1 `$input = $input"
}
function my2 {
    process{
        Write-Host "my2 Proc `$input = $input"
        $_
    }
}
PS C:\> "A","B","C" | my1 > $null
my1 $input = A B C
PS C:\> "A","B","C" | my2 > $null
my2 Proc $input = A
my2 Proc $input = B
my2 Proc $input = C

my1 と my2 では明らかに $input の挙動が違います。
my1 では、入力がすべて一度に渡されているのですが、my2 では $input に1つずつしか値が入っていません。
まるで $_ と同じように見えますが、型が違います $_ は パイプへ入力された「コレクションの要素」そのものですが、$inputは $_ と同じ値を返す Enumerator になっています。1要素だけの Enumeratorです。*1
このように、非パイプライン関数である my1 と パイプライン用に作った my2 では $input の内容がまるで違います。これが一つ目の「違い」。

begin, process, end の$input

パイプライン関数の同じスコープにいる3つのブロック。これらのブロック間でも $input は別物です。

function my3 {
    begin{
        Write-Host "my3 Begin `$input = $input"
    }
    process{
        Write-Host "my3 Proc `$input = $input"
        $_
    }
    end {
        Write-Host "my3 End `$input = $input"
    }
}
PS C:\> "A","B","C" | my3 > $null
my3 Begin $input = 
my3 Proc $input = A
my3 Proc $input = B
my3 Proc $input = C
my3 End $input = 

既にmy2で半分種明かしをしていますので不思議に思わないかもしれませんが、begin, process, end 内の $input はそれぞれ異なるインスタンスのオブジェクトです。
$inputはEnumeratorです。ならば処理を通して同じ $inputが MoveNextで要素を進めながら値を渡してくれているのかと思ったのですが、そうではなく、呼ばれるたびに別の $inputがわたってきていました。一要素のEnumeratorを渡す意味って何なんでしょうね。謎です。
同じ関数内でも呼び出し毎にインスタンスが違う。これが二つ目の「違い」です。

パイプを使わずにパイプを表現する

この話パイプの動作を理解するためにパイプの動作と同等のコードを書いてみよう。
と考えたのがきっかけでした。以前から色々想像してはいたのですが確認してみるとどうもおかしい。
こんな感じだと思っていました。

まず普通のパイプライン

#関数定義
function my ([string] $Tag) {
    begin {
        Write-Host "$Tag begin"
    }
    process {
        Write-Host "$Tag proc $_ + $_"
        $_ + $_
    }
    end {
        Write-Host "$Tag end"
    }
}
#パイプライン実行
"A","B","C" | my "<1st>" | my "<2nd>" | my "<3rd>"

そして、次のようにしたら上のコードと同等なんじゃないかと...
$my_ が $_、$my_input が $input にあたるものと思ってください。

#関数定義
function my_begin ([string] $Tag){
    Write-Host "$Tag begin"
}
function my_process ([string] $Tag){
    Write-Host "$Tag proc $my_ + $my_"
    $my_ + $my_
}
function my_end ([string] $Tag){
    Write-Host "$Tag end"
}
#パイプライン実行
$a = New-Object System.Collections.ArrayList
$a.AddRange(@("A","B","C"))
$my_input = $a.GetEnumerator()

my_begin "<1st>"
my_begin "<2nd>"
my_begin "<3rd>"

foreach($my_ in $my_input){
    $my_ = my_process "<1st>"
    $my_ = my_process "<2nd>"
    $my_ = my_process "<3rd>"
    $my_
}

my_end "<1st>"
my_end "<2nd>"
my_end "<3rd>"

出力は省きますが、同じ結果になります。
begin,process,endのスコープがバラバラなのは実際と違ってしまいますが、そこはPsCustomObjectとかでオブジェクト指向して貰えばそれっぽくなるでしょう。同じスコープを持った3つのハンドラで1セットのスコープが作れます。

が...
これだと、$my_inputはすべてのコンテキストで同じインスタンスのオブジェクトを参照していることになるはずです。
同じインスタンスであること、それを検証しようとしたのですが冒頭で書いたように結果はまったく違っていました。

じゃあどう書くのが正解なのかと言われると、分かりません。
無理やり辻褄を合わせるならこうでしょうかね。効率が悪くなっているだけですけど。

#パイプライン実行
$a = New-Object System.Collections.ArrayList
$a.AddRange(@("A","B","C"))
$a0 = $a.GetRange(0,0)

$my_input = $a0.GetEnumerator()
my_begin "<1st>"
$my_input = $a0.GetEnumerator()
my_begin "<2nd>"
$my_input = $a0.GetEnumerator()
my_begin "<3rd>"

$my_input = $e = $a.GetEnumerator()
foreach($my_ in $e){
    $my_input = @($my_).GetEnumerator()
    $my_ = my_process "<1st>"
    $my_input = @($my_).GetEnumerator()
    $my_ = my_process "<2nd>"
    $my_input = @($my_).GetEnumerator()
    $my_ = my_process "<3rd>"
    $my_
}
$my_input = $a0.GetEnumerator()
my_end "<1st>"
$my_input = $a0.GetEnumerator()
my_end "<2nd>"
$my_input = $a0.GetEnumerator()
my_end "<3rd>"

まとめ

PowerShellのパイプラインよくわからん。

*1:PowerShellでは一要素のコレクションは自動的に展開されます。このため 表示上「$input = A」となっていますが、AはEnumeratorだったものが展開された結果です