powershelltab-completion

How to get <tab> value suggestions dynamically, without throwing an error if user provided a value that does not exist?


Lets say, I have two functions get-foo and new-foo, that I am using to read and edit a tree structure resource. Its really nothing sophisticated, I am using the file system to implement the structure.

The issue am having is get-foo works as I want it to, it will force the user to only input values that are found in the tree structure.

My issue is, new-foo is not working as I want it to, I would like values from the tree structure to be suggested similar to how they are with get-foo, but the user must be able to input arbitrary values, so they can extend the structure. Currently new-foo will throw an error if the value does not exist.

My code:

Function get-foo{
    param(
    [ValidateSet([myTree], ErrorMessage = """{0}"" Is not a valid structure")]
    [string]$name
    )
    $name
}

Function new-foo{
    param(
    [ValidateSet([myTree])]
    [string]$name
    )
    $name
}


Class myTree : System.Management.Automation.IValidateSetValuesGenerator{
    [string[]] GetValidValues(){
        return [string[]] (Get-ChildItem -path "C:\temp" -Recurse |ForEach-Object {($_.FullName -replace 'c:\\temp\\')})
    }}

get-foo and new-foo both have a name parameter, the user is expected to provide a name. The functions check the directory c:\temp, for valid names.

For example, if c:temp was as follows:

C:\temp\animal
    C:\temp\animals\cat
    C:\temp\animals\dog
    C:\temp\animals\fish
C:\temp\colours
    C:\temp\colours\green
    C:\temp\colours\orange
    C:\temp\colours\red
C:\temp\plants
    C:\temp\plants\carrots
    C:\temp\plants\patato
    C:\temp\plants\vegatables

Then with get-foo -name anima...<tab>, then the auto competition examples would be:

But new-foo will throw an error if the value name does not already exist. Is there a mechanism that I can use to still get dynamic autocompletion but without the error? I checked the parameter attribute section, from my reading only validatSet seems applicable.

Am on pwsh 7.4

Edit: I made a slight edit above, new-foo's -name parameter was not using the same validateSet class as set-foo, they are both using the same validateSet class.


Solution

  • You can implement IArgumentCompleter in your existing class to suggest values but not validate them:

    using namespace System
    using namespace System.Collections
    using namespace System.Collections.Generic
    using namespace System.Management.Automation
    using namespace System.Management.Automation.Language
    
    class MyTree : IValidateSetValuesGenerator, IArgumentCompleter {
        hidden [StringComparison] $_comparer = [StringComparison]::InvariantCultureIgnoreCase
    
        [string[]] GetValidValues() {
            return $this.GetValues()
        }
    
        [IEnumerable[CompletionResult]] CompleteArgument(
            [string] $commandName,
            [string] $parameterName,
            [string] $wordToComplete,
            [CommandAst] $commandAst,
            [IDictionary] $fakeBoundParameters) {
    
            $values = foreach ($value in $this.GetValues()) {
                if ($value.StartsWith($wordToComplete, $this._comparer)) {
                    [CompletionResult]::new($value)
                }
            }
    
            return [CompletionResult[]] @($values)
        }
    
        hidden [string[]] GetValues() {
            return Get-ChildItem -Path 'c:\temp\' -Recurse |
                ForEach-Object { $_.FullName -replace 'c:\\temp\\' }
        }
    }
    

    Then the implementation for Get-Foo remains the same, using [ValidateSet([MyTree], ...)] but for New-Foo you'd use [ArgumentCompleter(...)] instead:

    function New-Foo {
        param(
            [ArgumentCompleter([MyTree])]
            [string] $Name
        )
    }
    

    Other alternative that doesn't require to implement IArgumentCompleter would be to use Register-ArgumentCompleter, you can find an example in this answer.