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
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
- List of functions/cmdlets this
- 2.ParameterName
CommandName
parameter name thisTAB
completion will be applied to
- 3.ScriptBlock
- Special crafted scriptBlock, whose output will be shown as
TAB
completion
- Special crafted scriptBlock, whose output will be shown as
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
- contains name of the parameter to which
wordToComplete
- contains string user have entered (if any)
commandAst
- contains AST object of the current input line
fakeBoundParams
- contains hashtable containing the
$PSBoundParameters
for thecommandName
function, before the user pressedTAB
- contains hashtable containing the
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 forClass
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
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 isArgumentCompleter
and is used to defineTAB
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 forRegister-ArgumentCompleter
function, so check first section of this post for more details
- The
And the result will look like this
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?