Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using Tab Complete After an Established Parameter in Powershell

I found some code that allows you to output text one character at a time and I am trying to make it fit my use case better by allowing you to select the foreground color as well. I have already added the foreground color to my parameters and it responds correctly, but the tab complete does not cycle trough the colors as it does for Write-Host.

function get-easyview{
    param(
    [int]$Milliseconds= 20,
    [string]$Foregroundcolor="RED")
    $text = $input | Out-String
   
    [char[]]$text | ForEach-Object{
        Write-Host -nonewline -Foregroundcolor $Foregroundcolor $_
        # Only break for a non-whitespace character.
        if($_ -notmatch "\s"){Sleep -Milliseconds $Milliseconds}

    }

 }


$words="my salsa...  my salsa" | get-easyview

This outputs the text in red and $words="my salsa... my salsa" | get-easyview -foregroundcolor green would output it in green, but I want tab to cycle through the colors once I type -foregroundcolor.

Any ideas?

like image 685
mothman Avatar asked Oct 25 '25 03:10

mothman


1 Answers

As PowerShell Core evolves, more options become available for Argument Completion in your functions, below you can find some examples of their implementation.

All of these options allows you to cycle through the set with the Tab key.

about Functions Advanced Parameters has listed these alternatives with some fine examples, I would also personally recommend you this excellent article from vexx32.


ValidateSet attribute

Validates and throw an exception if the argument used is not in the set.

[ValidateSet('Red','White','Blue','Yellow')]
[string] $ForegroundColor = 'Red'

ArgumentCompletions attribute

This attribute was introduced in PowerShell 6, it's not compatible with Windows PowerShell and as the docs states, the main difference is:

However, unlike ValidateSet, the values are not validated and more like suggestions. Therefore the user can supply any value, not just the values in the list.

[ArgumentCompletions('Red','White','Blue','Yellow')]
[string] $ForegroundColor = 'Red'

PowerShell 6 also introduced the IValidateSetValuesGenerator Interface and with it the ability to create custom classes that, when implementing this interface, can offer completion and validation at the same time:

class ColorCompleter : System.Management.Automation.IValidateSetValuesGenerator {
    static [string[]] $Colors

    static ColorCompleter() {
        [ColorCompleter]::Colors = [System.Drawing.Color].GetProperties(
            [System.Reflection.BindingFlags] 'Static, Public').Name
    }

    [string[]] GetValidValues() {
        return [ColorCompleter]::Colors
    }
}

function Test-IValidateSetValuesGenerator {
    [CmdletBinding()]
    param([ValidateSet([ColorCompleter])] $Color)

}

Test-IValidateSetValuesGenerator <TAB>

For other alternatives compatible with Windows PowerShell 5.1 see below.


Register-ArgumentCompleter

This cmdlet allows us to define a custom argument completer for a function, cmdlet or command.

Steps for registering a custom argument completer

  1. Define our Function:
function Test-ArgumentCompleter {
    [cmdletbinding()]
    param([string] $ForegroundColor)

    $ForegroundColor
}
  1. Define the ScriptBlock which will dynamically generate the Tab auto completion:

The script block you provide should return the values that complete the input. The script block must unroll the values using the pipeline (ForEach-Object, Where-Object, etc.), or another suitable method. Returning an array of values causes PowerShell to treat the entire array as one tab completion value.

$scriptBlock = {
    param(
        $commandName,
        $parameterName,
        $wordToComplete,
        $commandAst,
        $fakeBoundParameters
    )

    'Red', 'White', 'Blue', 'Yellow' |
        Where-Object { $_ -like "*$wordToComplete*" }
}
  1. Lastly, register the Argument Completer:
$params = @{
    CommandName   = 'Test-ArgumentCompleter'
    ParameterName = 'ForegroundColor'
    ScriptBlock   = $scriptBlock
}
Register-ArgumentCompleter @params

Using a custom PowerShell Class

Since PowerShell 5.1, we can define our own PowerShell Class that implements the IArgumentCompleter Interface.

  1. We define our class:
using namespace System.Management.Automation
using namespace System.Management.Automation.Language
using namespace System.Collections
using namespace System.Collections.Generic

class Completer : IArgumentCompleter {
    static [string[]] $Set

    [IEnumerable[CompletionResult]] CompleteArgument(
        [string] $CommandName,
        [string] $ParameterName,
        [string] $WordToComplete,
        [CommandAst] $CommandAst,
        [IDictionary] $FakeBoundParameters
    ) {
        [CompletionResult[]] $result = foreach($item in $this.Set) {
            if($item -like "*$wordToComplete*") {
                [CompletionResult]::new("'$item'")
            }
        }
        return $result
    }
}
  1. We decorate the desired parameter with ArgumentCompleterAttribute passing the custom class type as argument, essentially, the ArgumentCompleterAttribute(Type) overload:
# define the new set here (can be also hardcoded in the class)
[Completer]::Set = 'foo', 'bar', 'baz'

function Test-ArgumentCompleter {
    [cmdletbinding()]
    param(
        [ArgumentCompleter([Completer])]
        [string] $Argument
    )

    $Argument
}

It's also worth noting that ArgumentCompleterAttribute has another overload for Script Blocks, thus, something like this is also perfectly valid:

function Test-ArgumentCompleter {
    [cmdletbinding()]
    param(
        [ArgumentCompleter({
            param(
                $commandName,
                $parameterName,
                $wordToComplete,
                $commandAst,
                $fakeBoundParameters
            )

            'Red', 'White', 'Blue', 'Yellow' |
                Where-Object { $_ -like "*$wordToComplete*" }
        })]
        [string] $Argument
    )

    $Argument
}

The main advantage of using a custom Class is that it gives us more room for customizations, for example we could have a Class that can handle Completion and Validation by inheriting from ValidateEnumeratedArgumentsAttribute and IArgumentCompleter.

For example, the class definition could look like this:

using namespace System.Management.Automation
using namespace System.Management.Automation.Language
using namespace System.Collections
using namespace System.Collections.Generic

class ValidateCustomSet : ValidateEnumeratedArgumentsAttribute, IArgumentCompleter {
    static [string[]] $Set
    static [string] $ErrorMessage

    static ValidateCustomSet() {
        if (-not [ValidateCustomSet]::ErrorMessage) {
            [ValidateCustomSet]::ErrorMessage = "'{0}' is not in set! Valid Values '{1}'"
        }
    }

    ValidateCustomSet() { }

    ValidateCustomSet([string] $ErrorMessage) {
        [ValidateCustomSet]::ErrorMessage = $ErrorMessage
    }

    ValidateCustomSet([scriptblock] $Set, [string] $ErrorMessage) {
        [ValidateCustomSet]::Set = $Set.InvokeReturnAsIs()
        [ValidateCustomSet]::ErrorMessage = $ErrorMessage
    }

    ValidateCustomSet([scriptblock] $Set) {
        [ValidateCustomSet]::Set = $Set.InvokeReturnAsIs()
    }

    [void] ValidateElement([object] $Element) {
        if ($Element -notin [ValidateCustomSet]::Set) {
            throw [MetadataException]::new(
                [string]::Format([ValidateCustomSet]::ErrorMessage,
                    $Element, [string]::Join(',', [ValidateCustomSet]::Set)))
        }
    }

    [IEnumerable[CompletionResult]] CompleteArgument(
        [string] $CommandName,
        [string] $ParameterName,
        [string] $WordToComplete,
        [CommandAst] $CommandAst,
        [IDictionary] $FakeBoundParameters
    ) {
        [List[CompletionResult]] $result = foreach ($i in [ValidateCustomSet]::Set) {
            if ($i.StartsWith($wordToComplete, [System.StringComparison]::InvariantCultureIgnoreCase)) {
                [CompletionResult]::new(
                    "'" + [CodeGeneration]::EscapeSingleQuotedStringContent($i) + "'",
                    $i,
                    [CompletionResultType]::ParameterValue,
                    $i)
            }
        }
        return $result
    }
}

Then the implementation would require to decorate the same parameter with ArgumentCompleter passing our custom class and also to decorate it with the custom class (for the validation part):

function Test-Set {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [ValidateCustomSet(
            # Positional binding here!
            # If using the (scriptblock, string) overload must be in that order!
            { 'foo', 'bar', 'baz' }, 
            'Invalid Value: {0}. Must be one of the following: {1}.'
        )]
        [ArgumentCompleter([ValidateCustomSet])]
        [string] $Item
    )

    $Item
}

demo

like image 79
Santiago Squarzon Avatar answered Oct 26 '25 17:10

Santiago Squarzon