Adding tab completion to your PowerShell parameters (or arguments) is a great way to enhance the user experience. You provide the user with possible values or build a dynamic list of values so the user knows what they can use. This article will teach you five ways to add tab completion to PowerShell functions.

Using ValidateSet Attribute

The simplest and easiest method of implementing tab argument completion for a function is with ValidateSet. ValidateSet provides a valid set of values you can use for a given parameter. The value set provides tab completion so the user can see which values are valid for the parameter.

For example, here is a function named New-Widget with two parameters: $Color and $Size. Each parameter uses the ValidateSet attribute to define which values the user can use for each. For $Color, the valid values are Red, Green, and Yellow. For $Size, the values are Small, Medium, and Large.

function New-Widget {
    param (
        [Parameter(Mandatory = $true)]
        [ValidateSet('Red', 'Green', 'Yellow')]
        [string]
        $Color,
        [Parameter(Mandatory = $true)]
        [ValidateSet('Small', 'Medium', 'Large')]
        [string]
        $Size
    )
    "Creating a $Size widget of color $Color."
}

When the user calls the New-Widget function, they can tab through the possible values for each parameter, like this:

gif showing tab completion with ValidateSet
Tab completion using ValidateSet

ValidateSet validation occurs any time the variable is assigned within the script or function, not just as a parameter. Here’s the New-Widget function again, where the last line of code tries to set $Color to Blue, which is invalid.

function New-Widget {
    param (
        [Parameter(Mandatory = $true)]
        [ValidateSet('Red', 'Green', 'Yellow')]
        [string]
        $Color,
        [Parameter(Mandatory = $true)]
        [ValidateSet('Small', 'Medium', 'Large')]
        [string]
        $Size
    )
    "Creating a $Size widget of color $Color."
    $Color = 'Blue'
}

Calling the function results in this error message:

The variable cannot be validated because the value Blue is not a valid value for the Color variable.

powershell validateset error message
Error message showing ValidateSet enforcing variable assignment value outside of a parameter.

Generating Dynamic ValidateSet Values with Classes

In the example above, you set a static list of possible values for the parameters. You can also dynamically generate values for ValidateSet at runtime using classes. Your custom class uses the IValidateSetValuesGenerator class, which was introduced in PowerShell 6.0.

In the example below, the custom class LogFileName searches three file system paths for log files: C:\logs, C:\Windows\logs, and C:\temp\logs. The class verifies if the path is valid using the Test-Path command, and if the path is valid, it gathers any file names in the directory. The class then returns the file names.

Class LogFileName : System.Management.Automation.IValidateSetValuesGenerator {
    [string[]] GetValidValues() {
        $LogFilePaths = 'C:\logs',
        'C:\Windows\logs',
        'C:\temp\logs'
        $LogFileName = ForEach ($LogFilePath in $LogFilePaths) {
            If (Test-Path $LogFilePath) {
                (Get-ChildItem $LogFilePath -File).Name
            }
        }
        return [string[]] $LogFileName
    }
}

In the function definition, input the class name in the ValidateSet attribute. PowerShell dynamically generates values for you to tab through when you execute the function command.

function Get-LogFile {
    Param(
        [ValidateSet([LogFileName])]
        [string]$FileName
    )
    "Here is the LogFile file you chose: $FileName"
}
gif showing dynamic tabl completion
Generating tab completion using dynamically found files

Adding Suggested Values with ArgumentCompletions

The ArgumentCompletions attribute is very similar to ValidateSet. Both attributes provide a list of values when the user presses the Tab key. However, ArgumentCompletions does not perform validation on the parameter value if it is not in the list of values. The list is more of a suggestion rather than a hard rule. Microsoft introduced ArgumentCompletions in PowerShell 6.0

Let’s look at the New-Widget example function again, this time with ArgumentCompletions instead of ValidateSet. Note in the GIF following the code example, the first command uses the suggested values (Yellow and Large), while the second command allows the user to input their own values (Blue and Jumbo) without error.

function New-Widget {
    param (
        [Parameter(Mandatory = $true)]
        [ArgumentCompletions('Red', 'Green', 'Yellow')]
        [string]
        $Color,
        [Parameter(Mandatory = $true)]
        [ArgumentCompletions('Small', 'Medium', 'Large')]
        [string]
        $Size
    )
    "Creating a $Size widget of color $Color."
}
gif showing tab completion using ArgumentCompleters
Tab completions using Argumentcompletions

Using ArgumentCompleter Tab Completion

The ArgumentCompleter attribute (not to be confused with ArgumentCompletions) adds tab completion values to a specific parameter. You define an ArgumentCompleter attribute for each parameter that needs tab completion. ArgumentCompleter needs a script block that determines the parameter’s possible values.

You define the script block using the function keyword along with a specific set of parameters. The parameter names do not matter as the values are provided positionally. The syntax for ArgumentCompleter looks like this:

function ArgumentCompleterExample {
    param(
        [Parameter(Mandatory)]
        [ArgumentCompleter( {
            param ( $commandName,
                    $parameterName,
                    $wordToComplete,
                    $commandAst,
                    $fakeBoundParameters )
            # Calculate tab completed values here
        } )]
        $ParamName
    )
}

Each parameter provides a different value for determining the tab completion values:

  • $commandName (Position 0): The name of the PowerShell command the script block provides the tab completion values.
  • $parameterName (Position 1): The parameter requiring tab completion values.
  • $wordToComplete (Position 2): The parameter value the user has provided before pressing the Tab key. The script block uses this value to determine tab completion values.
  • $commandAst (Position 3): The parameter is set to the Abstract Syntax Tree (AST) for the current input line.
  • $fakeBoundParameters (Position 4): This parameter contains a hashtable with the cmdlet’s $PSBoundParameters value before the user presses Tab.

The ArgumentCompleter script block uses ForEach-Object, Where-Object, or any other suitable method to iterate through the values. The script block returns an array of values that PowerShell uses as one tab completion value.

The example below defines a function named Get-FruitVariety with two parameters: $Fruit and $Variety. The $Fruit parameter uses the ValidateSet attribute to limit to three types of fruits: Apple, Banana, and Orange. The $Variety parameter uses the ArgumentCompleter attribute and script block VarietyArgumentCompleter to provide tab completion values.

If the user provides a fruit type using the $Fruit parameter, then the script block returns the possible varieties for the $Variety parameter. This logic is found in the first part of the if statement. If the user provides the $Variety parameter first, the script block returns all possible values using the else block.

function VarietyArgumentCompleter {
    param ( $commandName,
        $parameterName,
        $wordToComplete,
        $commandAst,
        $fakeBoundParameters )
    $possibleValues = @{
        Apple  = @('Gala', 'Honeycrisp', 'GrannySmith')
        Banana = @('Cavendish', 'LadyFinger', 'Manzano')
        Orange = @('Mandarin', 'Navel', 'Blood')
    }
    if ($fakeBoundParameters.ContainsKey('Fruit')) {
        $possibleValues[$fakeBoundParameters.Fruit] | Where-Object {
            $_ -like "$wordToComplete*"
        }
    }
    else {
        $possibleValues.Values | ForEach-Object { $_ }
    }
}
function Get-FruitVariety {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateSet('Apple', 'Banana', 'Orange')]
        $Fruit,
        [Parameter(Mandatory = $true)]
        [ArgumentCompleter({ VarietyArgumentCompleter @args })]
        $Variety
    )
    "You selected the fruit $Fruit and variety of $Variety."
}

Watch the GIF below where the Fruit parameter is used first, and Tab completion provides the correct fruit varieties for the Variety parameter.

gif showing tab completion with ArgumentCompleter
Generating tab completions values based on value of another parameter

ArgumentCompleter Script Block Code Improvements

However, the ArgumentCompleter script block implementation above has a few issues. First, if you specify the Variety parameter first, using Tab completion does not match the correct Fruit type, like this:

image showing mismatch of parameter values
Example showing mismatch if Variety parameter is used before Fruit

Second, if you use the Variety parameter first and start entering a variety type, then pressing Tab doesn’t try to match a particular variety. Instead, it starts iterating through the possible keys starting with “Gala” since it is the first value in the first item in the list.

gif showing parameter does not find matching value
Example showing Variety does not find matching tab completion value

The updated example below resolves these issues. First, the VarietyArgumentCompleter adds a Where-Object clause to find a match based on the $wordToComplete variable value.

function VarietyArgumentCompleter {
    param ( $commandName,
        $parameterName,
        $wordToComplete,
        $commandAst,
        $fakeBoundParameters )
    $possibleValues = @{
        Apple  = @('Gala', 'Honeycrisp', 'GrannySmith')
        Banana = @('Cavendish', 'LadyFinger', 'Manzano')
        Orange = @('Mandarin', 'Navel', 'Blood')
    }
    if ($fakeBoundParameters.ContainsKey('Fruit')) {
        $possibleValues[$fakeBoundParameters.Fruit] | Where-Object {
            $_ -like "$wordToComplete*"
        }
    }
    else {
        $possibleValues.Values | ForEach-Object { $_ } | Where-Object { $_ -like "$wordToComplete*" }
    }
}

Second, the Fruit parameter gets its own ArgumentCompleter script block that searches for a matching fruit if the Variety parameter is already specified and Fruit does not have a value. If neither conditions are true, then the script block returns the Fruit parameter’s possible values.

function FruitArgumentCompleter {
    param ( $commandName,
        $parameterName,
        $wordToComplete,
        $commandAst,
        $fakeBoundParameters )
    $possibleValues = @{
        Apple  = @('Gala', 'Honeycrisp', 'GrannySmith')
        Banana = @('Cavendish', 'LadyFinger', 'Manzano')
        Orange = @('Mandarin', 'Navel', 'Blood')
    }
    if ($fakeBoundParameters.ContainsKey('Variety') -and [string]::IsNullOrEmpty($fakeBoundParameters['Fruit'])) {
        $fruitMatchesArray = [System.Collections.ArrayList]::new()
        foreach ($key in $possibleValues.Keys) {
            if ($possibleValues[$key] -contains $fakeBoundParameters['Variety']) {
                $fruitMatchesArray.Add($key) | Out-Null
            }
        }
        $fruitMatchesArray
    }
    else {
        $possibleValues.Keys | ForEach-Object { $_ } | Where-Object { $_ -like "$wordToComplete*" }
    }
}

Finally, the function definition is updated to include the new Fruit ArgumentCompleter script block.

function Get-FruitVariety {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [ArgumentCompleter({ FruitArgumentCompleter @args })]
        $Fruit,
        [Parameter(Mandatory = $true)]
        [ArgumentCompleter({ VarietyArgumentCompleter @args })]
        $Variety
    )
    "You selected the fruit $Fruit and variety of $Variety."
}

The above update example is a bit more complicated as each parameter’s possible values can be dependent on the value found in the other parameter.

Using Register-ArgumentCompleter for Tab Completion

The Register-ArgumentCompleter cmdlet registers a custom argument completer. The difference between this ArgumentCompleter and the prior example is this ArgumentCompleter script block can be used for any command, not just the ones you write.

For example, let’s say you want to configure the Id parameter of Set-TimeZone to tab complete the time zones in your region. By default, the Id parameter does not have tab completion, but you can write a customer ArgumentCompleter to return only the time zones you are interested in.

In the example below, you define a variable named $timeZoneIdScriptBlock. In the script block, you use the same default parameters used earlier in the ArgumentCompleter script block example ($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters).

Next, write the logic to build the tab completion values for the parameter. In this case, the script block searches for time zones where the DisplayName property contains “(US & Canada)“. The script block returns the Id property of the matching time zones.

Finally, you use the Register-ArgumentCompleter cmdlet and specify the CommandName to apply the ArgumentCompleter to (Set-TimeZone), the ParameterName to configure (Id), and the ScriptBlock to use ($timeZoneIdScriptBlock).

$timeZoneIdScriptBlock = {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)
    (Get-TimeZone -ListAvailable |
        Where-Object -Property DisplayName -like -Value "*(US & Canada)*").Id |
        ForEach-Object { "'$_'" }
}
Register-ArgumentCompleter -CommandName Set-TimeZone -ParameterName Id -ScriptBlock $timeZoneIdScriptBlock

With the ArgumentCompleter registered, you can now tab through the possible Id values for the Set-TimeZone cmdlet that matches your criteria. In this case, tab completion should show four primary time zones for the US and Canada: Eastern, Central, Mountain, and Pacific.

gif showing custom tab completion for native powershell command
Tab completion shows matching time zone Id based on ArgumentCompleter script block

Two things to note about this method:

  1. The user can still specify other values for the parameter. Registering the argument completer only provides the suggested values for using the parameter.
  2. The ArgumentCompleter script block is only valid for the current PowerShell session. If you want to use this all the time, you must include it in your PowerShell profile.

PowerShell Tab Completion Summary

Providing tab completion values for your parameters (or arguments) is a great way to enhance your custom advanced functions. Tab completion values provide the function user more context on what is expected for the command and are a great way to show off your advanced PowerShell function writing abilities.

Enjoyed this article? Check out more PowerShell content here!