# How to add parameter TAB completion to your PowerShell functions

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](https://cdn.hashnode.com/res/hashnode/image/upload/v1620625937245/EDU54959s.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`
 - contains [AST object](https://doitpsway.com/powershell-ast-your-friend-when-it-comes-to-powershell-code-analysis-and-extraction) of the current input line 
- `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](https://cdn.hashnode.com/res/hashnode/image/upload/v1620630422596/LtvZo2VlJ.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](https://cdn.hashnode.com/res/hashnode/image/upload/v1620630692805/0O08oWJDw.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?
