这个问题针对以下场景:
-
是否可以使用参数级
[ArgumentCompleter()]
属性或Register-ArgumentCompleter
cmdlet,根据先前传递给另一个参数的值动态确定给定命令的选项卡补全? -
如果是,这种方法的局限性是什么?
示例场景:
一个假设的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.1
和7.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, ...
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-CommandCompleter
cmdlet单独定义自定义完成逻辑。
:
-
[参见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
)
# ...
}