powershelltab-completion

How to reproduce tab completion of properties for generic psobjects on the pipeline in a custom function?


If I type onto the command line:

[pscustomobject]@{ name = 'test'; description='doing test' } | Select-Object

I can press tab and complete the property names—besides the default properties, either of the defined properties name and description. Select-Object is aware of the name and description properties on the input object and will autocomplete them with standard tab behavior.

Now if I use a custom function, such as:

function pipeobject {
    param(
        [Parameter(ValueFromPipeline)]
        $InputObject,

        [Parameter(Position=0)]
        $Property
    )

    process {
        $Property | Foreach-Object { $InputObject.$_ }
    }
}

and enter:

[pscustomobject]@{ name = 'test'; description='doing test' } | pipeobject 

I cannot tab complete the -Property parameter for any of the properties on the input object.

Note: I am aware of this reply which recommends using output types. However, my inquiry refers to parsing the properties of a generic pscustomobject, which Select-Object seems capable of.

Question

How can I reproduce Select-Object's tab completion behavior on the pipeline for properties in my custom functions?


Solution

  • What you're describing, inferring members from a pscustomobject from pipeline, is one of many great tab completion improvements in PowerShell v7.4 from MartinGC94 (PR #18682 in this case) and short answer is you won't find an easy way to replicate what he did in a robust way but to show you how you can approach the problem by looking at the CommandAst should be a start:

    Register-ArgumentCompleter -CommandName pipeobject -ParameterName Property -ScriptBlock {
        param(
            [string] $CommandName,
            [string] $ParameterName,
            [string] $WordToComplete,
            [System.Management.Automation.Language.CommandAst] $CommandAst,
            [System.Collections.IDictionary] $FakeBoundParameters
        )
    
        $ast = $CommandAst.Parent.FindAll({
            $args[0] -is [System.Management.Automation.Language.ConvertExpressionAst] },
            $true)
    
        if (-not $ast.Child.KeyValuePairs) {
            return
        }
    
        $hash = [System.Collections.Generic.HashSet[string]]::new(
            [System.StringComparer]::InvariantCultureIgnoreCase)
    
        foreach ($item in $ast.Child.KeyValuePairs.Item1) {
            $key = $item.ToString()
    
            if (-not $hash.Add($key)) {
                continue
            }
    
            if ($key.StartsWith($WordToComplete, [StringComparison]::InvariantCultureIgnoreCase)) {
                $item
            }
        }
    }
    

    demo