PowerShell Advent Calendar 2013 に参加させていただいています。これは17日目の記事となります。*1
昨日は、@84zumeさんによるPowerShellとNLog でした。PowerShell の処理をログ出力するにあたって、NLog は有力な手段ですね。私も今後移行を検討しています。
さて、今回は、一歩進んだファンクション構築に関して触れようかと思います。
目次
paramキーワードによるパラーメーター定義
PowerShell で ファンクション を書いていく中で、param
キーワードを利用したパラメーター定義を行うことが多くあります。
例えばこのような例です。
function Write-ParamTest { [CmdletBinding()] Param ( # パラメーター 1 のヘルプの説明 [string] $parameter1 ) Process { Write-Output $parameter1 } }
これで、Write-ParamTest と入力した際に、 -parameter1 というパラメーターで [string]型の受入れをIntellisenseに表示、利用できるようになります。 もし paramキーワードを使わない場合、Args[x] に格納されてしまうため、パラメータの型指定やヴァリデーションも含めてparamを利用する機会は多いかと思います。
ValidateSetによる入力補完
paramキーワードでは、複数の[Validateほげもげ] を利用可能です。その中の一つが、[ValidateSet()]です。
[ValidateSet()]は、パラメータ利用時にIntelllisense に指定した文字列で候補を表示、それ以外の入力を制限できます。
function Write-ParamTest { [CmdletBinding()] Param ( # Param1 help description [ValidateSet("cat", "dog", "fox")] [string] $parameter1 ) Process { $parameter1 } }
これで、次のように parameter1 というパラメータを入力した段階でcat
かdog
かfox
の3つがIntellisenseに候補として表示されます。
[f:id:guitarrapc_tech:20190125044211p:plain]
Intellisense がはかどってとても便利です。また、ValidateSetは指定した入力(この場合は、cat, dog, fox) 以外は弾くように制限されます。
Write-ParamTest : Cannot validate argument on parameter 'parameter1'. The argument "hogemoge" does not belong to the set "cat,dog,fox" specified by the ValidateSet attribute. Supply an argument that is in the set and th en try the command again. At line:18 char:29 + Write-ParamTest -parameter1 hogemoge + ~~~~~~~~ + CategoryInfo : InvalidData: (:) [Write-ParamTest], ParameterBindingValidationException + FullyQualifiedErrorId : ParameterArgumentValidationError,Write-ParamTest
ValidateSetでは利用しにくい場合がある
ValidateSetは一件便利ですが、その内容は静的に[string], [int]な値を指定する必要があり、[ScriptBlock]やコマンドレットを指定できません。
たとえば、特定フォルダのファイル名のみに入力を制限したくとも、そのファイル名を直接記述するしかなくファンクション実行時に存在するファイル名を走査し候補にあげるような利用はできません。
リスト指定は可能
function Get-FileTest { [CmdletBinding()] Param ( # Param1 help description [ValidateSet("file1.txt", "file2.log", "file3.ps1")] [string] $fileName ) Process { Get-Item $fileName } } Get-FileTest -fileName file1.txt
動的にフォルダ走査して指定はできない
function Get-FileTest { [CmdletBinding()] Param ( # Param1 help description [ValidateSet(&{Get-ChildItem})] [string] $fileName ) Process { Get-Item $fileName } }
このようなエラーがでます。
Parameter declarations are a comma-separated list of variable names with optional initializer expressions.
これでは、はかどりませんね?そこで Dynamic Paramの出番となります。
Dynamic Param
そもそも Dynamic Param とは何でしょうか?
PowerShell で利用頻度Topであろう、Get-Help
で適当なfunctionを見てみると 各パラメーターに Dynamic? という表示があります。
Get-Help Get-FileTest -Parameter filename
-fileName <string> Required? false Position? 0 Accept pipeline input? false Parameter set name (All) Aliases None Dynamic? false
これが、Dynamic paramの利用であるかの判断となります。
Dynamic Param を構築する
DynamicParam{} セクションを指定して、結果に [System.Management.Automation.RuntimeDefinedParameterDictionary] を返すことで、その内容がDynamicParamとして利用できます。
この[System.Management.Automation.RuntimeDefinedParameterDictionary]は、パラメーターの構築にそのものです。
先ほどの、現在のフォルダのファイルを動的に候補にあげるように作ってみましょう。
function Get-FileTest { [CmdletBinding()] Param ( ) DynamicParam { $attributes = New-Object System.Management.Automation.ParameterAttribute $attributesCollection = New-Object 'Collections.ObjectModel.Collection[System.Attribute]' $attributesCollection.Add($attributes) $validateSetAttributes = New-Object System.Management.Automation.ValidateSetAttribute (Get-ChildItem) $attributesCollection.Add($validateSetAttributes) $runtimeDefinedParameter = New-Object -TypeName System.Management.Automation.RuntimeDefinedParameter @("filename", [System.String], $attributesCollection) $dictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary $dictionary.Add("filename", $runtimeDefinedParameter) return $dictionary } Process { Get-Item $fileName } }
これで、filnameパラメーターを指定した際に現在のフォルダに存在するファイルが候補に表示されます。
Get-FileTest -filename file1.txt
[f:id:guitarrapc_tech:20190125044226p:plain]
Get-Helpでも Dynamic? がtrueになっていることがわかります。
PARAMETERS -filename <string> Required? false Position? Named Accept pipeline input? false Parameter set name (All) Aliases None Dynamic? true
Dynamic Param をもっと楽に構築する
先ほどは最低限の要素で構築しましたが、DynamicParam ではMandatory など各属性も指定できます。 ただ、構築にいちいち先ほどのようなことはして入れないので、ヘルパーを作ってしまうのがいいかと思います。
ただ、複数のDynamicParamを指定するには、[System.Management.Automation.RuntimeDefinedParameterDictionary[]]を返す必要があるので、ひと手間加えます。
今回は、valentiaに組み込む予定で作ったサンプルを詳解しましょう。
GitHub - PowerShellUtil / DynamicParamTest / New-ValentiaDynamicParamMulti.ps1
function New-ValentiaDynamicParamList { param ( [parameter( mandatory = 1, position = 0, valueFromPipeline = 1, valueFromPipelineByPropertyName = 1)] [hashtable[]] $dynamicParams ) begin { # create generic list $list = New-Object System.Collections.Generic.List[HashTable] # create key check array [string[]]$keyCheckInputItems = "name", "options", "position", "valueFromPipelineByPropertyName", "helpMessage", "validateSet" $keyCheckList = New-Object System.Collections.Generic.List[String] $keyCheckList.AddRange($keyCheckInputItems) } process { foreach ($dynamicParam in $dynamicParams) { $invalidParamter = $dynamicParam.Keys | Where {$_ -notin $keyCheckList} if ($($invalidParamter).count -ne 0) { throw ("Invalid parameter '{0}' found. Please use parameter from '{1}'" -f $invalidParamter, ("$keyCheckInputItems" -replace " "," ,")) } else { if (-not $dynamicParam.Keys.contains("name")) { throw ("You must specify mandatory parameter '{0}' to hashtable key." -f "name") } elseif (-not $dynamicParam.Keys.contains("options")) { throw ("You must specify mandatory parameter '{0}' to hashtable key." -f "options") } else { $list.Add($dynamicParam) } } } } end { return $list } } function New-ValentiaDynamicParamMulti { param ( [parameter( mandatory = 1, position = 0)] [System.Collections.Generic.List[HashTable]] $dynamicParamLists ) $dictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary foreach ($dynamicParamList in $dynamicParamLists) { # create attributes $attributes = New-Object System.Management.Automation.ParameterAttribute $attributes.ParameterSetName = "__AllParameterSets" if($dynamicParamList.mandatory){$attributes.Mandatory = $dynamicParamList.mandatory} # mandatory if($dynamicParamList.position -ne $null){$attributes.Position=$dynamicParamList.position} # position if($dynamicParamList.valueFromPipelineByPropertyName){$attributes.ValueFromPipelineByPropertyName = $dynamicParamList.valueFromPipelineByProperyName} # valueFromPipelineByPropertyName if($dynamicParamList.helpMessage){$attributes.HelpMessage = $dynamicParamList.helpMessage} # helpMessage # create attributes Collection $attributesCollection = New-Object 'Collections.ObjectModel.Collection[System.Attribute]' $attributesCollection.Add($attributes) # create validation set if ($dynamicParamList.validateSet) { $validateSetAttributes = New-Object System.Management.Automation.ValidateSetAttribute $dynamicParamList.options $attributesCollection.Add($validateSetAttributes) } # create RuntimeDefinedParameter $runtimeDefinedParameter = New-Object -TypeName System.Management.Automation.RuntimeDefinedParameter @($dynamicParamList.name, [System.String], $attributesCollection) # create Dictionary $dictionary.Add($dynamicParamList.name, $runtimeDefinedParameter) } # return result $dictionary }
利用する際は、このようにします。
function Show-DynamicParamMulti { [CmdletBinding()] param() dynamicParam { $parameters = ( @{name = "hoge" options = "fuga" validateSet = $true position = 0}, @{name = "foo" options = "bar" position = 1}) $dynamicParamLists = New-ValentiaDynamicParamList -dynamicParams $parameters New-ValentiaDynamicParamMulti -dynamicParamLists $dynamicParamLists } begin { } process { $PSBoundParameters.hoge $PSBoundParameters.foo } }
これで、候補が動的に利用可能です。
Show-DynamicParamMulti -hoge fuga -foo bar
Dynamic Param利用時の注意点
Dynamic Param を利用する際は、Begin{}Process{}End{} などのブロック構文を必ず利用する必要があります。
もちろん、process{}だけで Begin{}End{}を利用しないことも可能ですが、ご注意ください。
まとめ
割と世界でも詳解例がない内容ですが、開発者としては需要が高い機能を紹介しました。
諸事情により、12/17 35時の投稿となりましたが、どうかご容赦のほどを..... 申し訳ありませんでした。
明日(?)は、@gab_km せんせーです。お楽しみに!
*1:2週目という噂もある