tech.guitarrapc.cóm

Technical updates

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

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 というパラメータを入力した段階でcatdogfoxの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週目という噂もある