动态参数值取决于另一个动态参数值

时间:2017-03-13 16:13:25

标签: bash powershell autocomplete dynamicparameters

开始前提:非常严格的环境,Windows 7 SP1,Powershell 3.0。使用外部库的可能性有限或没有。

我试图重新编写我之前创建的bash工具,这次是使用PowerShell。在bash中,我实现了自动完成功能,使该工具更加用户友好,我想为PowerShell版本做同样的事情。

bash版的工作原理如下:

./launcher <Tab> => ./launcher test (or dev, prod, etc.)
./launcher test <Tab> => ./launcher test app1 (or app2, app3, etc.)
./launcher test app1 <Tab> => ./launcher test app1 command1 (or command2, command3, etc.).

如你所见,一切都是动态的。环境列表是动态的,应用程序列表是动态的,根据所选的环境,命令列表也是动态的。

问题在于测试→应用程序连接。我想根据用户已经选择的环境显示正确的应用程序。

使用PowerShell的DynamicParam我可以根据文件夹列表获取动态环境列表。但我不能(或者至少我没有发现如何)执行另一个文件夹列表,但这次使用的是基于现有用户选择的变量。

当前代码:

function ParameterCompletion {
    $RuntimeParameterDictionary = New-Object Management.Automation.RuntimeDefinedParameterDictionary

    # Block 1.
    $AttributeCollection = New-Object Collections.ObjectModel.Collection[System.Attribute]

    $ParameterName = "Environment1"
    $ParameterAttribute = New-Object Management.Automation.ParameterAttribute
    $ParameterAttribute.Mandatory = $true
    $ParameterAttribute.Position = 1
    $AttributeCollection.Add($ParameterAttribute)
    # End of block 1.

    $parameterValues = $(Get-ChildItem -Path ".\configurations" -Directory | Select-Object -ExpandProperty Name)
    $ValidateSetAttribute = New-Object Management.Automation.ValidateSetAttribute($parameterValues)
    $AttributeCollection.Add($ValidateSetAttribute)

    $RuntimeParameter = New-Object Management.Automation.RuntimeDefinedParameter($ParameterName, [string], $AttributeCollection)
    $RuntimeParameterDictionary.Add($ParameterName, $RuntimeParameter)

    # Block 2: same thing as in block 1 just with 2 at the end of variables.

    # Problem section: how can I change this line to include ".\configurations\${myVar}"?
    # And what's the magic incantation to fill $myVar with the info I need?
    $parameterValues2 = $(Get-ChildItem -Path ".\configurations" -Directory | Select-Object -ExpandProperty Name)
    $ValidateSetAttribute2 = New-Object Management.Automation.ValidateSetAttribute($parameterValues2)
    $AttributeCollection2.Add($ValidateSetAttribute2)

    $RuntimeParameter2 = New-Object
        Management.Automation.RuntimeDefinedParameter($ParameterName2, [string], $AttributeCollection2)
    $RuntimeParameterDictionary.Add($ParameterName2, $RuntimeParameter2)

    return $RuntimeParameterDictionary
}

function App {
    [CmdletBinding()]
    Param()
    DynamicParam {
        return ParameterCompletion "Environment1"
    }

    Begin {
        $Environment = $PsBoundParameters["Environment1"]
    }

    Process {
    }
}

1 个答案:

答案 0 :(得分:3)

我建议使用在PowerShell 3和4中半曝光的参数完成符,并在5.0及更高版本中完全公开。对于v3和v4,底层功能就在那里,但您必须覆盖TabExpansion2内置函数才能使用它们。对于您自己的会话来说这没关系,但是通常不赞成将这样做的工具分发给其他人的会话(想象一下,如果每个人都试图覆盖该功能)。 PowerShell团队成员有一个为您执行此操作的模块,名为TabExpansionPlusPlus。我知道我说覆盖TabExpansion2很糟糕,但是如果这个模块可以做到的话就没问题了:)

当我需要支持版本3和版本4时,我会将我的命令分发到模块中,并让模块检查是否存在&#39; Register-ArgumentCompleter&#39; command,它是v5 +中的cmdlet,如果你有TE ++模块,它是一个函数。如果模块找到它,它将注册任何完成者,如果它没有,它将通知用户参数完成不会工作,除非他们得到TabExpansionPlusPlus模块。

假设您拥有TE ++模块或PSv5 +,我认为这可以让您走上正轨:

function launcher {

    [CmdletBinding()]
    param(
        [string] $Environment1,
        [string] $Environment2,
        [string] $Environment3
    )

    $PSBoundParameters
}

1..3 | ForEach-Object {
    Register-ArgumentCompleter -CommandName launcher -ParameterName "Environment${_}" -ScriptBlock {
        param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)

        $PathParts = $fakeBoundParameter.Keys | where { $_ -like 'Environment*' } | sort | ForEach-Object {
            $fakeBoundParameter[$_]
        }

        Get-ChildItem -Path ".\configurations\$($PathParts -join '\')" -Directory -ErrorAction SilentlyContinue | select -ExpandProperty Name | where { $_ -like "${wordToComplete}*" } | ForEach-Object {
            New-Object System.Management.Automation.CompletionResult (
                $_,
                $_,
                'ParameterValue',
                $_
            )
        }
    }
}

要使其正常工作,您当前的工作目录将需要一个&#39;配置&#39;包含在其中的目录,并且您至少需要三个级别的子目录(通过您的示例阅读,看起来您将枚举一个目录,并且您将在添加参数时深入了解该结构)。目前对目录的枚举并不是很聪明,如果你只是跳过一个参数,你就可以很容易地把它搞得很简单,例如,launcher -Environment3 <TAB>会尝试给你第一个子目录的完成。

如果您始终有三个可用参数,则此方法有效。如果你需要一个参数变量,你仍然可以使用完成者,但它可能会有点棘手。

最大的缺点是你仍然需要验证用户&#39;输入,因为完成者基本上只是建议,用户不必使用这些建议。

如果你想使用动态参数,那就太疯狂了。可能有更好的方法,但是我没有在不使用反射的情况下在命令行上看到动态参数的值,并且此时您将使用可能在下一个版本中更改的功能(成员通常没有公开的原因)。尝试在DynamicParam {}块中使用$ MyInvocation很有吸引力,但是当用户在命令行中输入命令时它没有填充,并且它只显示命令的一行不使用反射。

以下是在PowerShell 5.1上测试的,因此我无法保证任何其他版本都具有这些完全相同的类成员(它基于我第一次看到Garrett Serack所做的事情)。与前面的示例一样,它取决于当前工作目录中的。\配置文件夹(如果没有,则您不会看到任何-Environment参数)。

function badlauncher {

    [CmdletBinding()]
    param()

    DynamicParam {

        #region Get the arguments 

        # In it's current form, this will ignore parameter names, e.g., '-ParameterName ParameterValue' would ignore '-ParameterName',
        # and only 'ParameterValue' would be in $UnboundArgs
        $BindingFlags = [System.Reflection.BindingFlags] 'Instance, NonPublic, Public'
        $Context = $PSCmdlet.GetType().GetProperty('Context', $BindingFlags).GetValue($PSCmdlet)
        $CurrentCommandProcessor = $Context.GetType().GetProperty('CurrentCommandProcessor', $BindingFlags).GetValue($Context)
        $ParameterBinder = $CurrentCommandProcessor.GetType().GetProperty('CmdletParameterBinderController', $BindingFlags).GetValue($CurrentCommandProcessor)

        $UnboundArgs = @($ParameterBinder.GetType().GetProperty('UnboundArguments', $BindingFlags).GetValue($ParameterBinder) | where { $_ } | ForEach-Object {
            try {
                if (-not $_.GetType().GetProperty('ParameterNameSpecified', $BindingFlags).GetValue($_)) {
                    $_.GetType().GetProperty('ArgumentValue', $BindingFlags).GetValue($_)
                }
            }
            catch {
                # Don't do anything??
            }
        })

        #endregion

        $ParamDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary

        # Create an Environment parameter for each argument specified, plus one extra as long as there
        # are valid subfolders under .\configurations
        for ($i = 0; $i -le $UnboundArgs.Count; $i++) {

            $ParameterName = "Environment$($i + 1)"
            $ParamAttributes = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
            $ParamAttributes.Add((New-Object Parameter))
            $ParamAttributes[0].Position = $i

            # Build the path that will be enumerated based on previous arguments
            $PathSb = New-Object System.Text.StringBuilder
            $PathSb.Append('.\configurations\') | Out-Null
            for ($j = 0; $j -lt $i; $j++) {
                $PathSb.AppendFormat('{0}\', $UnboundArgs[$j]) | Out-Null
            }

            $ValidParameterValues = Get-ChildItem -Path $PathSb.ToString() -Directory -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Name

            if ($ValidParameterValues) {
                $ParamAttributes.Add((New-Object ValidateSet $ValidParameterValues))

                $ParamDictionary[$ParameterName] = New-Object System.Management.Automation.RuntimeDefinedParameter (
                    $ParameterName,
                    [string[]],
                    $ParamAttributes
                )
            }
        }

        return $ParamDictionary

    }

    process {
        $PSBoundParameters
    }
}

关于这个问题的一个很酷的事情是它只要有文件夹就可以继续运行,它会自动进行参数验证。当然,你通过使用反射来获取所有私有成员来破坏.NET的规律,所以我认为这是一个糟糕而脆弱的解决方案,无论它有多么有趣。