読者です 読者をやめる 読者になる 読者になる

tech.guitarrapc.cóm

C#, PowerShell, Unity, Cloud Platfrom Technical Update and Features

PowerShell の ダイナミックパラメータを利用して動的にパラメータを組み立てる

PowerShell AdventCalendar

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 というパラメータを入力した段階でcatdogfoxの3つがIntellisenseに候補として表示されます。

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

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週目という噂もある