根据另一个参数的已指定值以 Tab 键完成参数值



这个问题针对以下场景:

  • 是否可以使用参数级[ArgumentCompleter()]属性Register-ArgumentCompletercmdlet,根据先前传递给另一个参数的值动态确定给定命令的选项卡补全?

  • 如果是,这种方法的局限性是什么?

示例场景:

一个假设的Get-Property命令有一个-Object参数,它接受任何类型的对象,一个-Property参数接受一个属性的名称,它的值可以从对象中提取。

现在,在键入Get-Property调用的过程中,如果已经为-Object指定了一个值,则tab补全的-Property应该循环遍历指定对象(公共)属性的名称。

$obj = [pscustomobject] @{ foo = 1; bar = 2; baz = 3 }
Get-Property -Object $obj -Property # <- pressing <tab> here should cycle
# through 'foo', 'bar', 'baz'

@mklement0,关于你的回答中提到的第一个限制

PowerShell调用的自定义完成脚本块({ ... })基本上只看到通过参数指定的值,而不是通过管道.

我为此挣扎,经过一些固执,我得到了一个有效的解决方案。
至少对我的工具来说足够好了,我希望它能让其他许多人的生活更轻松。

此解决方案已被验证可与PowerShell版本5.17.1.2一起工作。

这里我使用了$cmdAst(在文档中称为$commandAst),它包含有关管道的信息。有了它,我们可以了解前面的管道元素,甚至可以区分它只包含一个变量还是一个命令。是的,一个命令,在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}

更新:查看betoz的有用答案对于更完整的解决方案,也支持管道输入

下面的回答部分澄清了输入对象数据类型的预执行检测的局限性,仍然适用。


以下解决方案使用特定于参数的[ArgumentCompleter()]属性作为Get-Property函数本身定义的一部分,但是该解决方案类似地适用于通过Register-CommandCompletercmdlet单独定义自定义完成逻辑。

:

  • [参见betoz关于如何克服此限制的回答] 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())
      • 命令输出(通过(...)$(...)@(...),例如
        -Object (Invoke-RestMethod http://example.org))
      • 这个限制的原因是,在实际提交命令之前评估这些值可能会产生不良的副作用,并且/或者可能需要很长时间才能完成。
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
)
# ...
}

最新更新