How to add parameter TAB completion to your PowerShell functions

How to add parameter TAB completion to your PowerShell functions

ยท

5 min read

I think we all can agree that TAB completion is very useful and time saving feature that a lot of native PowerShell functions offer. But did you know that you can make this useful feature available even for your own custom functions?

In this post, I will show you two ways to make this work ๐Ÿ‘

PS: By TAB completion I mean pressing TAB key to get available parameter values TAB completion.gif


1. TAB completion defined globally for list of functions

The key here is to use Register-ArgumentCompleter PowerShell function.

Be noted that effect of using Register-ArgumentCompleter won't outlast console closure. To make it "permanent" place the code into your PowerShell profile script.

Register-ArgumentCompleter has three important parameters:

  • 1.CommandName
    • List of functions/cmdlets this TAB completion will be applied to
  • 2.ParameterName
    • CommandName parameter name this TAB completion will be applied to
  • 3.ScriptBlock
    • Special crafted scriptBlock, whose output will be shown as TAB completion

Basic example

# some dumb function with at least one parameter (fullName in this example)
function Open-DesktopFile {
    param ($fullName)

    # open given file
    & $fullName
}

# scriptblock for Register-ArgumentCompleter
$openDesktopSb = {
    param ($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParams)

    $DesktopPath = [Environment]::GetFolderPath("Desktop")

    Get-ChildItem -Path $DesktopPath -File | Where-Object { $_.Name -like "*$WordToComplete*" } | Select-Object -ExpandProperty FullName | ForEach-Object { "'$_'" }
}

# activate `TAB` completion for 'Open-DesktopFile' functions parameter 'fullName'
# what pressing of `TAB` returns, is defined in openDesktopSb scriptblock
Register-ArgumentCompleter -CommandName Open-DesktopFile -ParameterName fullName -ScriptBlock $openDesktopSb

# now call Open-DesktopFile -fullName (and hit `TAB` key to fill it)

Silly example above shows basic scriptBlock for filling path to quoted files on your desktop.

As you can see, the Register-ArgumentCompleter scriptBlock operates with five automatically filled parameters: $commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParams (their names doesn't matter, just their position!)

  • commandName
    • contains name of the function to which this completion will be applied to
  • parameterName
    • contains name of the parameter to which TAB will be applied to
  • wordToComplete
    • contains string user have entered (if any)
  • commandAst
  • fakeBoundParams
    • contains hashtable containing the $PSBoundParameters for the commandName function, before the user pressed TAB

PS: scriptBlock will be run with each hit of TAB key! So make it as resource efficient as you can.

Example of leveraging fakeBoundParams parameter

  • We all use Get-WmiObject and Get-CimInstance cmdlets, but wouldn't be nice to have TAB completion for Class name? Moreover for different Namespaces? ๐Ÿ˜‰
$wmiClassSb = {
    param ($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParams)

    if ($fakeBoundParams.Namespace) {
        # use user entered namespace if available
        $Namespace = $FakeBoundParams.Namespace
    } else {
        # use default namespace
        $Namespace = "ROOT\cimv2"
    }

    Get-WmiObject -List -namespace $Namespace | ? {$_.Name -like "*$WordToComplete*"} | select -exp name
}

# make `TAB` completion work for Class parameter of Get-WmiObject, Get-CimInstance commands
Register-ArgumentCompleter -CommandName Get-WmiObject, Get-CimInstance -ParameterName Class -ScriptBlock $wmiClassSb

The result will look like this TAB completion22.gif


2. TAB completion defined in function param() section

  • As you already know, parameters defined in PowerShell param() block, can have various attributes like type, validation etc. One of this attributes is ArgumentCompleter and is used to define TAB completion
    • The usage is like this:
function Invoke-Greeting {
    param (
        [ArgumentCompleter( {
                param ($Command, $Parameter, $WordToComplete, $CommandAst, $FakeBoundParams)

                Get-LocalUser | Where-Object { $_.Name -like "*$WordToComplete*" }
            })]
        [string] $userName
    )

    "Say hi to $userName"
}

# to test this out, call Invoke-Greeting and just hit TAB to show local user names
  • So for specific parameter ($userName in this case) you define [ArgumentCompleter({ <scriptBlock> })]
    • The <scriptBlock> has same format as for Register-ArgumentCompleter function, so check first section of this post for more details

And the result will look like this TAB completion3.gif


Various examples just for inspiration

Search AD computer by its name or description

  • very handy if you have a lot of servers, and you save their purpose to their description attribute!

    $computerSB = {
         param ($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
    
         $searcher = New-Object System.DirectoryServices.DirectorySearcher (([adsi]"LDAP://OU=Computer_Accounts,DC=contoso,DC=com"), '(objectCategory=computer)', ('name', 'description'))
         ($searcher.findall() | ? { $_.properties.name -match $wordToComplete -or $_.properties.description -match $wordToComplete }).properties.name | Sort-Object | % { "'$_'" }
         $searcher.Dispose()
     }
    
     # TAB completion of AD computer names in computerName parameter of any command from module Scripts, ..
     Register-ArgumentCompleter -CommandName ((Get-Command -Module Scripts).name) -ParameterName computerName -ScriptBlock $computerSB
     Register-ArgumentCompleter -CommandName Enter-PSSession, Invoke-Command -ParameterName computername -ScriptBlock $computerSB
     # TAB completion of AD computer names in identity parameter of commands with "computer" in their name from module ActiveDirectory
     Register-ArgumentCompleter -CommandName ((Get-Command -Module ActiveDirectory -Noun *computer*).name) -ParameterName identity -ScriptBlock $computerSB
    

    Similarly if you have functions related to server or clients, you can limit the completion like this

     $serverSB = {
         param ($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
    
         $searcher = New-Object System.DirectoryServices.DirectorySearcher (([adsi]"LDAP://OU=Servers,OU=Computer_Accounts,DC=contoso,DC=com"), '(objectCategory=computer)', ('name', 'description'))
         ($searcher.findall() | ? { $_.properties.name -match $wordToComplete -or $_.properties.description -match $wordToComplete }).properties.name | Sort-Object | % { "'$_'" }
         $searcher.Dispose()
     }
    
     Register-ArgumentCompleter -CommandName Invoke-MSTSC -ParameterName computerName -ScriptBlock $serverSB
    
     $clientSB = {
         param ($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
    
         $searcher = New-Object System.DirectoryServices.DirectorySearcher (([adsi]"LDAP://OU=Clients,OU=Computer_Accounts,DC=contoso,DC=com"), '(objectCategory=computer)', ('name', 'description'))
         ($searcher.findall() | ? { $_.properties.name -match $wordToComplete -or $_.properties.description -match $wordToComplete }).properties.name | Sort-Object | % { "'$_'" }
         $searcher.Dispose()
     }
    
     Register-ArgumentCompleter -CommandName Assign-Computer -ParameterName computerName -ScriptBlock $clientSB
    

Or what about replacing name for samaccountname in identity parameter of all functions from ActiveDirectory module (that contains 'User' in noun part)?

    $userSB = {
        param ($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)

        $searcher = New-Object System.DirectoryServices.DirectorySearcher (([adsi]"LDAP://OU=User_Accounts,DC=contoso,DC=com"), '(objectCategory=user)', ('name', 'samaccountname'))
        ($searcher.findall() | ? { $_.properties.name -match $wordToComplete }).properties.samaccountname | Sort-Object | % { "'$_'" }
        $searcher.Dispose()
    }

    Register-ArgumentCompleter -CommandName ((Get-Command -Module ActiveDirectory -Noun *user*).name) -ParameterName identity -ScriptBlock $userSB

Summary

As you can see, you have two options to define TAB completion. Inside the function param() block using [ArgumentCompleter()] or globally using Register-ArgumentCompleter. In general I use the latter option in my PowerShell profile script mainly for built-in functions and the first one, for my own custom functions. In either way, its very handy, time saving and cool feature! ๐Ÿ˜‰

Do you have any other cool examples of TAB completion?

Did you find this article valuable?

Support Ondrej Sebela by becoming a sponsor. Any amount is appreciated!

ย