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 }
How to uninstall Azure PowerShell modules - Microsoft Learn
Az のインストール
代わりにAzモジュールを入れます。
Install-Module -Name Az -AllowClobber -Scope CurrentUser
Azure PowerShell をインストールする方法 - Microsoft Learn
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などいくつか方法がありますが本題ではないので省略します。↩