PowerShell v5 で追加された PipelineVariable に関して、本では説明していたのですがブログに書いていませんでした。 いい感じの例があったので、説明しておこうと思います
目次
TL;DR
- PipelineVariable を使うと、パイプラインの中で一度
$x = $_
と書いて変数を保持していた処理が不要になります Sort-Object
のような集計系のCmdlet を使うとパイプラインの後続にわたる値の挙動が変わるので注意しましょう- Az モジュール使いにくい
対象のPowerShellコード
いい感じの Pipeline Variable の例があります。
List all #azure vnets and subnets #powershell one-liner: Get-AzVirtualNetwork | % -pv vnet { $_ } | % { $_.subnets } | select @{l="vnet";e={$vnet.name}},@{l="snet";e={$_.name}},addressprefix | sort vnet, snet
— Oisín Grehan (x0n) (@oising) January 8, 2020
この例を通して PipelineVariable を見てみましょう。
PipelineVariable とは
このコードの | % -pv vnet { $_ }
は Pipeline Variable を使っています。
Pipeline Variable というのは、パイプラインからオブジェクトを次のパイプラインに送出するときに、そのオブジェクトを指定した変数に保持する機能です。
公式ではこう言っています。
新しい共通パラメーター PipelineVariable が追加されました。 PipelineVariable を使用すると、パイプされたコマンド (またはパイプされたコマンドの一部) の結果を変数として保存し、パイプラインの残りの部分に引き渡すことができます。
そのパイプライン時点のオブジェクトを変数に保持して何が嬉しいかというと、パイプラインがつながっている限り、直後ではない後続のパイプラインでもそのオブジェクトを変数経由で利用できます。 どういうことでしょうか?
PowerShellでは、通常パイプライン直後のスクリプトブロックでは流れてきたオブジェクトを 自動変数$_
経由で読めます。
PS > ps | %{$_} | Get-Member TypeName: System.Diagnostics.Process
しかし、その次のパイプラインのスクリプトブロックでは、$_
の中身は前のパイプラインの中身に変わります。
例えば次のように、%{$_.Name}
とProcess型のName プロパティ出力すると、後続のパイプライン | %{$_}
では Process型ではなく String型に 変わります。
PS> ps | %{$_.Name} | %{$_} | Get-Member TypeName: System.String
PipelineVariable を使わないと、一連のPipelineの中であるパイプラインにおける変数を保持して後続に渡すときにハッシュテーブルや適当な入れ物に入れて渡すなど手間がかかります。
# こんなことはしたくない! PS> ps | %{@{ps=$_;name=$_.Name}} | %{$_.ps} | Get-Member
PipelineVariables を使う例
↑の例以外にも、パイプラインの中でさらにパイプラインを書く時に、一番初めにパイプラインに入った$_
を思うように取れなくて一瞬引っ掛かったという人は多いのではないでしょうか?
何もしない無駄にそれっぽいのを例にします。 末尾の %{$_} では Process型が欲しいのですが、当然 String が来ます。
PS> ps | %{$_ | Where Name -eq "pwsh" | %{$_.Name.Replace("sh","hs")} | %{$_}} pwhs pwhs
この時にPipelineVariable を使わない場合、一時変数に入れてからやることが多いと思います。
PS> ps | %{$ps=$_; $_ | Where Name -eq "pwsh" | %{$_.Name.Replace("sh","hs")} | %{$ps}} NPM(K) PM(M) WS(M) CPU(s) Id SI ProcessName ------ ----- ----- ------ -- -- ----------- 149 404.51 462.63 54.20 6820 1 pwsh 71 55.12 96.09 1.95 13816 1 pwsh
PipelineVarible はこういった「パイプラインの後続で今のコンテキスト ($_) を使いたい」というシーンで機能します。 PipelineVarible に置き換えてみましょう。
PS> ps -pv ps | %{$_ | Where Name -eq "pwsh" | %{$_.Name.Replace("sh","hs")} | %{$ps}} NPM(K) PM(M) WS(M) CPU(s) Id SI ProcessName ------ ----- ----- ------ -- -- ----------- 149 404.51 462.63 54.20 6820 1 pwsh 71 55.12 96.09 1.95 13816 1 pwsh
%{$ps=$_;}
と書いていた処理を、パイプラインを開始する前の ps コマンド時点に -pv ps
と持ってきました。
このように、「パイプラインの中で $_
をいちいち変数に受け取っていた」という人は結構楽に書けるようになるはずです。
Aggregateする処理ではPipelineVariables の利用を気を付ける
Sort-Object
のようにパイプラインをせき止める Aggregation 系の処理では、後段のパイプラインの結果は前段と変わります。
例えば先ほどのコードに意図的に Sort-Object を入れるとわかります。
PS> ps -pv ps | sort | %{$_ | Where Name -eq "pwsh" | %{$_.Name.Replace("sh","hs")} | %{$ps}} NPM(K) PM(M) WS(M) CPU(s) Id SI ProcessName ------ ----- ----- ------ -- -- ----------- 31 29.86 9.57 0.42 9476 1 YourPhone 31 29.86 9.57 0.42 9476 1 YourPhone
pwsh を拾っているはずなのに、YourPhone というプロセスに変わってしまいました。
YourPhone Sort-Object
から渡った最後のオブジェクトに相当します。
PS> ps | sort | select -Last 1 NPM(K) PM(M) WS(M) CPU(s) Id SI ProcessName ------ ----- ----- ------ -- -- ----------- 31 29.86 9.57 0.42 9476 1 YourPhone
では、Sort-Object
のようなパイプラインを一度せき止めるCmdletを挟みたい場合、どうすればいいのでしょうか?
簡単です、Sort-Object
で PipelineVariable に割り当ててください。
PS> ps | sort -pv ps | %{$_ | Where Name -eq "pwsh" | %{$_.Name.Replace("sh","hs")} | %{$ps}} NPM(K) PM(M) WS(M) CPU(s) Id SI ProcessName ------ ----- ----- ------ -- -- ----------- 148 403.27 461.28 59.45 6820 1 pwsh 71 55.39 96.36 2.00 13816 1 pwsh
元コードからみる PipelineVariable
さぁ、PipelineVariable については概ね理解できたと思います。 元のコードを再度提示してみてみましょう。
AzVirtualNetwork | % -pv vnet { $_ } | % { $_.subnets } | select @{l="vnet";e={$vnet.name}},@{l="snet";e={$_.name}},addressprefix | sort vnet, snet
このコードの PipelineVariable は、先ほどの ps
と違って一度 %{$_}
を介しているように見えます。
どういうことか見てみましょう。
-pv vnet は早められる
もし、PipelineVariables を使うときに自分が % -pv vnet {$_}
のように、ただ後続に $_
を流すだけの処理で PipelineVariablesを使うように書いていたら、その-pv、前段のコマンド時点に持っていくことができます。
AzVirtualNetwork -pv vnet | % { $_.subnets } | select @{l="vnet";e={$vnet.name}},@{l="snet";e={$_.name}},addressprefix | sort vnet, snet
元コードでは %{$_}
でパイプラインを通るたびに新しく $vnet
に $_
を割り当てているのですが、それは AzVirtualNetwork |
時点でやっています。
ps
の例で見せたように一番初めの Cmdletを実行した結果はパイプラインを通るのですが、その時点で PipelineVariable としてキャプチャできます。
単純な % {$_}
をやるようなパイプラインが減るのは可読性、速度面の両面から嬉しいので検討するといいでしょう。
Select @{l={};v={}} をやめる
PipelineVariable とは関係ありませんが、Select @{l={};v={}}
を使って PSObjectを生成している部分があります。
PSObject の生成方法としては、Select @{l={};v={}}
以外にも [PSCustomObject]@{}
があります。1
Select-Object
を使ったPSObject生成のメリットは、「プロパティの合成ができる」ことです。プロパティの合成は、ハッシュテーブルの後に書かれている addressprefix
がそれにあたります。
select @{l="vnet";e={$vnet.name}},@{l="snet";e={$_.name}},addressprefix
一方で、[PSCustomObject]@{}
を使うとハッシュテーブルから直接PSObjectに型変換します。
このやり方ではプロパティの合成はできず、自分で全プロパティに関してハッシュテーブルを定義しないといけません。
とはいえ、l
や e
のようなマジックキーに比べるとシンプルで速度も速く、可読性は高いでしょう。
% {[PSCustomObject]@{vnet=$vnet.name;snet=$_.name;addressprefix=$_.addressprefix}}
初めのコードをPSCustomObject に切り替えるとこうなります。
AzVirtualNetwork | % -pv vnet { $_ } | % { $_.subnets } | % {[PSCustomObject]@{vnet=$vnet.name;snet=$_.name;addr=$_.addressprefix}} | sort vnet, snet
まとめる
「-pv を早める」、「PSCustomObject に切り替える」の2つを組み合わせてみましょう。
AzVirtualNetwork -pv vnet | % { $_.subnets } | % {[PSCustomObject]@{vnet=$vnet.name;snet=$_.name;addr=$_.addressprefix}} | sort vnet, snet
幾分ワンライナー長い!読みたくない!感は減りました。
Azure環境の事前準備
もしコードを試す場合、Azureに VNet と Subnet が必要です。 せっかくなので、Az モジュールでサクッと組んでみましょう。
VNet作るだけなら料金かからないですしね。
AzureRm をアンインストールする
おわこん!それなのに Visual Studio で勝手に入る。
foreach ($module in (Get-Module -ListAvailable AzureRM*).Name |Get-Unique) { write-host "Removing Module $module" Uninstall-module $module }
Az のインストール
代わりに Az モジュールを入れます。
Install-Module -Name Az -AllowClobber -Scope CurrentUser
vnet とかの準備
あとは Azure環境に ReousrceGroup、VirtualNetwork、Subnetを作ります。
$location = 'Japan East' $rg = New-AzResourceGroup -Name test -Location 'Japan East' $vnet = New-AzVirtualNetwork -Name test -ResourceGroupName $rg.ResourceGroupName -Location 'Japan East' -AddressPrefix 10.0.0.0/16 $subnet = Add-AzVirtualNetworkSubnetConfig -Name a -VirtualNetwork $vnet -AddressPrefix 10.0.0.0/24 $vnet | Set-AzVirtualNetwork
これでコードを試せます。
AzVirtualNetwork | % -pv vnet { $_ } | % { $_.subnets } | select @{l="vnet";e={$vnet.name}},@{l="snet";e={$_.name}},addressprefix | sort vnet, snet
後片付け
リソースグループごとさくっと消せば全部消えます。
Remove-AzVirtualNetwork -Name $rg.Name
蛇足
Add-AzVirtualNetworkSubnetConfig
で$vnet のプロパティを変更して (out相当の処理!?)、$vnet | Set-AzVirtualNetwork
でVirtualNetwork に変更を適用しているの、とても書きにくいやり方ですね。
PowerShell っぽくないというか Azure に見られる特有に感じるのですが... 気のせいでしょうか。 Az モジュール、コマンドも探しにくく、Cmdletから使い方が予想できない使い方になってて、PowerShell の書く経験としては最悪に感じます。すごい、悲しい。
PowerShell 的には Addを使うと対象のオブジェクトに追加されることが多いので、それを期待している人は多いでしょう。Add-Content
とか。
今回の場合、Add-AzVirtualNetworkSubnetConfig
というCmdlet実行時点で $vnet に割り当てがされるのを期待するような気がほんわりします。(しない感じもある)
Add-AzVirtualNetworkSubnetConfig -Name a -VirtualNetwork $vnet -AddressPrefix 10.0.0.0/24
あるいは、VirtualNetworkSubnetConfig を作って、VirtualNetworkにAddするとか。(こっちのほうが納得感とCmdletからの予測ができそう)
$subnet = New-AzVirtualNetworkSubnetConfig -Name a -VirtualNetwork $vnet -AddressPrefix 10.0.0.0/24 #実際には-VirttualNetwork パラメーターはない Set-AzVirutalNetwork -SubnetConfig $subnet # こんなCmdlet もない
リソースを逐次分離したくてこうなったと思うのですが使い勝手が悪いのは Az Module でも改善されてないのでした。 az cli のほうが使いやすいので、私はもっぱら az cli です。
なお、AWS の Cmdlet 設計は秀逸で、どのCmdlet も aws cli と比較してもわかりやすい印象があります。 ただ、やはり型の扱いは若干めんどうさが表に出ていますが。
- ほかにもAdd-Member などいくつか方法がありますが本題ではないので省略します。↩