PowerShell でパイプライン処理

今更ながら、PowerShell でパイプライン処理を行う際に混乱してきたのでまとめておく。

# 稼働確認環境
- OS: Windows 10
- PowerShell: V5

# 完成版 (?) テンプレート
最終的に動作確認したのは下記のコード。

```PowerShell:Test-Pipeline.ps1
<#
.Synopsis
   パイプラインのテスト
#>
function Test-Pipeline {
    param (
        # 名前
        [Parameter(Mandatory=$true, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)]
        [ValidateNotNull()]
        [ValidateNotNullOrEmpty()]
        [string[]]$Name,

        # 最終更新日時
        [Parameter(Mandatory=$false, ValueFromPipeline=$false, ValueFromPipelineByPropertyName=$true)]
        [ValidateNotNull()]
        [datetime[]]$LastWriteTime
    )

    begin{
        # LastWriteTime が 1 つしか指定されていない場合、Name と同じ数の配列に初期化する
        if (!$MyInvocation.ExpectingInput) {  # 非パイプライン処理のときのみ実行する
            [int]$count = $Name.Count
            if (($count -ne 0) -and ($LastWriteTime.Count -eq 1)) {
                $LastWriteTime = 1..$count | foreach { $LastWriteTime[0] }
            }
        }
    }

    process {
        [int]$count = $Name.Count
        for ([int]$i=0; $i -lt $count; ++$i) {
            [pscustomobject]@{
                Name = $Name[$i]
                LastWriteTime = $LastWriteTime[$i]
            }
        }
    }
}

# Test1 と Test2 の両方に 2017/6/13 が割り当てられる
Test-Pipeline -Name "Test1","Test2" -LastWriteTime (Get-Date 2017/6/13)
# Test3 に 2017/1/1 が、Test4 に 2017/12/31 が割り当てられる
Test-Pipeline -Name "Test3","Test4" -LastWriteTime (Get-Date 2017/1/1),(Get-Date 2017/12/31)
# Test5 に 2017/1/1 が割り当てられる。2017/12/31 は捨てられる
Test-Pipeline -Name "Test5" -LastWriteTime (Get-Date 2017/1/1),(Get-Date 2017/12/31)

# C ドライブ直下のディレクトリ/ファイルにそれぞれの最終更新日が割り当てられる
ls C:\ | Test-Pipeline

# Test6 と Test7 の両方に 2017/6/13 が割り当てられる
"Test6","Test7" | Test-Pipeline -LastWriteTime (Get-Date 2017/6/13)
```

# 実行結果
```
Name                LastWriteTime      
----                -------------      
Test1               2017/06/13 0:00:00 
Test2               2017/06/13 0:00:00 
Test3               2017/01/01 0:00:00 
Test4               2017/12/31 0:00:00 
Test5               2017/01/01 0:00:00 
Intel               2016/02/04 22:49:36
PerfLogs            2017/03/19 6:03:28 
Program Files       2017/06/19 22:14:14
Program Files (x86) 2017/06/19 22:14:15
Users               2017/06/19 22:14:18
Windows             2017/06/19 22:22:11
Windows.old         2017/06/19 22:09:28
Test6               2017/06/13 0:00:00 
Test7               2017/06/13 0:00:00 
```

# 解説
## パイプラインでの引数の受け取り方
まずは信頼性が高いと思われる [ベンダの資料](https://technet.microsoft.com/ja-jp/library/hh413265.aspx) を確認する。

```PowerShell:Test-Pipeline01.ps1
Function Do-Something {
	[CmdletBinding()]
	param(
		[Parameter(Mandatory=$True,
		           ValueFromPipeline=$True)]
		[string[]]$computername
	)

	begin {
        echo "begin called."
    }

	process {
        echo "proces called."
        foreach ($name in $computername) {
            $name
        }
    }

	end {
        echo "end called."
    }
}
```

> この関数は 2 とおりの方法で呼び出すことができます。
> 1 つ目は、関数に文字列をパイプラインで渡す方法です。
> 2 つ目は、パイプラインをまったく使用せずに、単に 1 つまたは複数のコンピューター名を直接パラメーターに渡す方法です。
> 1 つ目の例では、関数の BEGIN ブロックが最初に実行されます。次に、パイプラインで渡されたコンピューター名ごとに、PROCESS ブロックが 1 回実行されます。\$computername 変数には、一度に 1 つのコンピューター名しか格納されません。すべてのコンピューター名の処理が終了したら、最後に END ブロックが 1 回実行されます。
> 2 つ目の例では、BEGIN ブロックと END ブロックが実行されることはありません。PROCESS ブロックは 1 回しか実行されず、$computername 変数には、パラメーターに渡されたすべてのコンピューター名が格納されます。

ふむふむ。なるほど。早速試してみる。

**1つ目の呼出し方法（パイプライン経由で呼出し）**

```
"foo","bar" | Do-Something
```

**実行結果**

```
begin called.
proces called.
foo
proces called.
bar
end called.
```
想定通りに動作した。

**2つ目の呼出し方法（通常の関数呼出し）**

```
Do-Something -computername "foo","bar"
```

**実行結果**

```
begin called.
proces called.
foo
bar
end called.
```

嘘つけ。 begin も end も実行されるじゃねえか。
バージョンによって挙動が違うんだろうか？
ご存知の方がいれば教えてください。
begin と end 以外は想定通りに動作した。

## パイプラインで受け取ったオブジェクトのプロパティを引数にマッピングする
ValueFromPipelineByPropertyName 属性を true にする。例では 1 つの引数のみにマッピングしているが、同属性を付与すれば複数の引数にマッピングできる。

```PowerShell:Test-Pipeline02.ps1
function Do-Something {
    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipelineByPropertyName=$True)]
        $Name
    )
 
    process{
        echo "process called."
        $name
    }
}

# パイプライン経由で呼び出す
echo "---------- pipeline ----------"
ls | Do-Something

# 非パイプラインで関数呼び出し
echo "---------- function call ----------"
Do-Something -name "foo","bar"
```

**実行結果**

```
---------- pipeline ----------
process called.
Test-Pipeline.ps1
process called.
Test-Pipeline01.ps1
process called.
Test-Pipeline02.ps1
---------- function call ----------
process called.
foo
bar
```

想定通りに動作した。

# 参考文献
- [Windows PowerShell: 高度な関数のライフサイクル](https://technet.microsoft.com/ja-jp/library/hh413265.aspx)
- [PowerShellらしい関数の書き方 [PS Advent Calendar '12]](http://winscript.jp/powershell/253)
- [パラメータとパイプライン両方の入力に対応した関数を作る２](http://mtgpowershell.blogspot.jp/2011/04/blog-post.html)