powershell 基于另一个参数的已指定值按Tab键完成参数值

nbysray5  于 2023-01-02  发布在  Shell
关注(0)|答案(2)|浏览(156)

此问题解决以下情况:

  • 给定命令的自定义制表符完成功能是否可以使用参数级[ArgumentCompleter()] * 属性 * 或Register-ArgumentCompleter * cmdlet *,根据先前传递给同一命令行上的 * another * 参数的值动态确定完成?
  • 如果是,这种方法的局限性是什么?

场景示例:
假设的Get-Property命令有一个-Object参数,它接受任何类型的对象,还有一个-Property参数,它接受要从对象中提取其值的属性的名称。
现在,在键入Get-Property调用的过程中,如果已经为-Object指定了一个值,制表符完成-Property应该循环遍历指定对象的(公共)属性的名称。

$obj = [pscustomobject] @{ foo = 1; bar = 2; baz = 3 }

Get-Property -Object $obj -Property # <- pressing <tab> here should cycle
                                    # through 'foo', 'bar', 'baz'
eyh26e7m

eyh26e7m1#

@mklement0,关于your answer中规定的第一个限制
PowerShell调用的自定义完成脚本块({ ... })基本上只能看到通过 * 参数 * 指定的值,而不能看到通过管道指定的值。
我为此而挣扎,经过一番固执之后,我找到了一个可行的解决方案。
至少对我的工具来说已经足够好了,我希望它能让其他许多人的生活更轻松。
此解决方案经验证可与PowerShell版本5.17.1.2配合使用。
这里我使用了$cmdAst(在文档中称为$commandAst),其中包含有关管道的信息。有了它,我们可以了解前面的管道 * 元素 *,甚至可以区分它只包含变量还是命令。是的,COMMAND,它在Get-Command和命令的OutputType()成员方法的帮助下,我们可以得到(建议的)属性名称等!

用法示例

PS> $obj = [pscustomobject] @{ foo = 1; bar = 2; baz = 3 }
PS> $obj | Get-Property -Property # <tab>: bar, baz, foo
PS> "la", "na", "le" | Select-String "a" | Get-Property -Property # <tab>: Chars, Context, Filename, ...
PS> 2,5,2,2,6,3 | group | Get-Property -Property # <tab>: Count, Values, Group, ...

功能代码

请注意,除了现在使用$cmdAst之外,我还添加了[Parameter(ValueFromPipeline=$true)],以便我们实际选取对象,以及PROCESS {$Object.$Property},以便可以测试并查看代码实际工作。

param(
    [Parameter(ValueFromPipeline=$true)]
    [object] $Object,
    [ArgumentCompleter({
        param($cmdName, $paramName, $wordToComplete, $cmdAst, $preBoundParameters)
        # Find out if we have pipeline input.
        $pipelineElements = $cmdAst.Parent.PipelineElements
        $thisPipelineElementAsString = $cmdAst.Extent.Text
        $thisPipelinePosition = [array]::IndexOf($pipelineElements.Extent.Text, $thisPipelineElementAsString)
        $hasPipelineInput = $thisPipelinePosition -ne 0

        $possibleArguments = @()
        if ($hasPipelineInput) {
            # If we are in a pipeline, find out if the previous pipeline element is a variable or a command.
            $previousPipelineElement = $pipelineElements[$thisPipelinePosition - 1]
            $pipelineInputVariable = $previousPipelineElement.Expression.VariablePath.UserPath
            if (-not [string]::IsNullOrEmpty($pipelineInputVariable)) {
                # If previous pipeline element is a variable, get the object.
                # Note that it can be a non-existent variable. In such case we simply get nothing.
                $detectedInputObject = Get-Variable |
                    Where-Object {$_.Name -eq $pipelineInputVariable} |
                        ForEach-Object Value
            } else {
                $pipelineInputCommand = $previousPipelineElement.CommandElements[0].Value
                if (-not [string]::IsNullOrEmpty($pipelineInputCommand)) {
                    # If previous pipeline element is a command, check if it exists as a command.
                    $possibleArguments += Get-Command -CommandType All |
                        Where-Object Name -Match "^$pipelineInputCommand$" |
                            # Collect properties for each documented output type.
                            ForEach-Object {$_.OutputType.Type} | ForEach-Object GetProperties |
                                # Group properties by Name to get unique ones, and sort them by
                                # the most frequent Name first. The sorting is a perk.
                                # A command can have multiple output types. If so, we might now
                                # have multiple properties with identical Name.
                                Group-Object Name -NoElement | Sort-Object Count -Descending |
                                    ForEach-Object Name
                }
            }
        } elseif ($preBoundParameters.ContainsKey("Object")) {
            # If not in pipeline, but object has been given, get the object.
            $detectedInputObject = $preBoundParameters["Object"]
        }
        if ($null -ne $detectedInputObject) {
            # The input object might be an array of objects, if so, select the first one.
            # We (at least I) are not interested in array properties, but the object element's properties.
            if ($detectedInputObject -is [array]) {
                $sampleInputObject = $detectedInputObject[0]
            } else {
                $sampleInputObject = $detectedInputObject
            }
            # Collect property names.
            $possibleArguments += $sampleInputObject | Get-Member -MemberType Properties | ForEach-Object Name
        }
        # Refering to about_Functions_Argument_Completion documentation.
        #   The ArgumentCompleter script block must unroll the values using the pipeline,
        #   such as ForEach-Object, Where-Object, or another suitable method.
        #   Returning an array of values causes PowerShell to treat the entire array as one tab completion value.
        $possibleArguments | Where-Object {$_ -like "$wordToComplete*"}
    })]
    [string] $Property
)

PROCESS {$Object.$Property}
i7uq4tfw

i7uq4tfw2#

更新请参见betoz's helpful answer,了解更完整的解决方案,该解决方案还支持 * 管道输入 *。

下面的回答部分阐明了输入对象数据类型的执行前检测的限制,这一部分仍然适用。
以下解决方案使用特定于参数的[ArgumentCompleter()]属性作为Get-Property函数本身定义的一部分,但该解决方案类似地适用于通过Register-CommandCompleter cmdlet单独定义自定义完成逻辑。

限制

  • [* 请参阅betoz's answer了解如何克服此限制 ] PowerShell调用的自定义完成脚本块({ ... })基本上只能看到通过 * 参数 * 指定的值,而不能 * 通过管道指定的值。
  • 也就是说,如果键入Get-Property -Object $obj -Property <tab>,脚本块可以确定$obj的值将绑定到-Object参数,但这不适用于

$obj | Get-Property -Property <tab>(即使-Object声明为管道绑定)。

  • 基本上,只有那些可以被求值 * 而没有副作用 * 的值才能在脚本块中被实际访问;具体而言,这意味着:
    • 文字 * 值(例如-Object ([pscustomobject] @{ foo = 1; bar = 2; baz = 3 })
    • 简单变量引用 *(例如-Object $obj)或 * 属性访问 * 或 * 索引访问 * 表达式(例如-Object $obj.Foo-Object $obj[0]
  • 值得注意的是,以下值是 * 不可 * 访问的:
    • 方法 *-调用结果(例如-Object $object.Foo()
    • 命令 * 输出(通过(...)$(...)@(...),例如

1米17分1秒)

  • 此限制的原因是,* 在 * 实际提交命令之前评估此类值可能会产生不良副作用和/或可能需要很长时间才能完成。
function Get-Property {

  param(

    [object] $Object,

    [ArgumentCompleter({

      # A fixed list of parameters is passed to an argument-completer script block.
      # Here, only two are of interest:
      #  * $wordToComplete: 
      #      The part of the value that the user has typed so far, if any.
      #  * $preBoundParameters (called $fakeBoundParameters 
      #    in the docs):
      #      A hashtable of those (future) parameter values specified so 
      #      far that are side effect-free (see above).
      param($cmdName, $paramName, $wordToComplete, $cmdAst, $preBoundParameters)

        # Was a side effect-free value specified for -Object?
        if ($obj = $preBoundParameters['Object']) {

          # Get all property names of the objects and filter them
          # by the partial value already typed, if any, 
          # interpreted as a name prefix.
          @($obj.psobject.Properties.Name) -like "$wordToComplete*"

        }
      })]
    [string] $Property

  )

  # ...

}

相关问题