今更ながら、PowerShell でパイプライン処理を行う際に混乱してきたのでまとめておく。
稼働確認環境
- OS: Windows 10
- PowerShell: V5
完成版 (?) テンプレート
最終的に動作確認したのは下記のコード。
<#
.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
解説
パイプラインでの引数の受け取り方
まずは信頼性が高いと思われる ベンダの資料 を確認する。
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 つの引数のみにマッピングしているが、同属性を付与すれば複数の引数にマッピングできる。
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
想定通りに動作した。