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ほげもげ] を利用可能です。その中の1つが、[ValidateSet()]です。
[ValidateSet()]は、パラメータ利用時に指定した文字列に入力制限、およびIDEで候補表示できます。
function Write-ParamTest { [CmdletBinding()] Param ( # Param1 help description [ValidateSet("cat", "dog", "fox")] [string] $parameter1 ) Process { $parameter1 } }
これで、parameter1 というパラメータを入力した段階でcat
かdog
かfox
の3つが候補として表示されます。
書き味が上がって便利です。次のように指定した入力以外は実行時にエラーを出します。
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
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週目という噂もある