Posts Tagged ‘Argument Completion’

On Twitter a few days ago, Aaron Nelson, aka @SQLvariant, was trying to get a command parameter’s completion results to change based on the value of another parameter. It turns out, this is pretty simple with argument completers using PSv5+ (you can do this in PSv3+, but you’ll want to take on a dependency from the TabExpansionPlusPlus module).

 

The trick is using the fifth parameter that the PS engine passes into the parameter’s registered argument completer, which is usually called $fakeBoundParameter (that’s the parameter name I saw in TabExpansionPlusPlus, so that’s what I’ve always used…you can name it whatever you’d like in the param() block for your completer, though). Don’t worry if that doesn’t make sense; you can still work through the example code below, and if it still doesn’t make sense, there’s a link to a video at the end of the blog post that describes this in more detail.

 

To demonstrate what I’m talking about, let’s use a very simple example. Let’s assume we have a command, Get-Food, that has -FoodType and -FoodName parameters:
function Get-Food {
    param(
        [string] $FoodType,
        [string] $FoodName
    )

    "FoodName: ${FoodName}`nFoodType: ${FoodType}"
}
I didn’t say it was a useful command 🙂

Also assume that you’ve got a hash table that has some food types and food names in it, which are the source of the suggested parameter values:
$Foods = @{
    Fruit = echo Apple, Orange, Banana, Peach
    Vegetable = echo Asparagus, Carrot, Edamame, Broccoli, Spinach
    Protein = echo Beef, Pork, Chicken, Fish, Edamame
    Grain = echo Rice, Oatmeal, Pasta, Bread
}
The simple command would look a lot more polished if you could not only have -FoodType and -FoodName suggest values (that’s easy!), but if you could also have the suggested values change if you’ve already provided a parameter. So if -FoodType is ‘Fruit’, you’d want -FoodName to only suggest the fruits from the hash table. Alternatively, if -FoodName is ‘Apple’, -FoodType should only suggest ‘Fruit’.

 

Well, with argument completers, you can do that without too much work. To do it, we’ll use the Register-ArgumentCompleter command, which takes -ParameterName, -ScriptBlock, and, optionally, -CommandName parameters. After calling it, PowerShell will invoke the scriptblock each time a completion result is needed, e.g., when IntelliSense needs to display some information, or when a user presses [TAB] or [TAB] + [SPACE]. When it invokes the scriptblock, it will also pass some parameters to it, including a hash table that we’re going to name $fakeBoundParameter. That hash table will contain simple parameter values that have already been bound (I say simple because if you try to put an expression in the parameter value, $fakeBoundParameter won’t have that information since it could potentially cause side effects, and you don’t want parameter completion to potentially make changes on your system. It’ll have the info if you stick to simple strings, though). To see what I’m talking about, here’s how you’d register completers for the -FoodType and -FoodName parameters:
Register-ArgumentCompleter -ParameterName FoodType -ScriptBlock {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)

    $FoodNameFilter = $fakeBoundParameter.FoodName

    $Foods.Keys | where { $_ -like "${wordToComplete}*" } | where {
        $Foods.$_ -like "${FoodNameFilter}*"
    } | ForEach-Object {
        New-Object System.Management.Automation.CompletionResult (
            $_,
            $_,
            'ParameterValue',
            $_
        )
    }
}

Register-ArgumentCompleter -ParameterName FoodName -ScriptBlock {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)

    $TypeFilter = $fakeBoundParameter.FoodType

    $Foods.Keys | where { $_ -like "${TypeFilter}*" } | ForEach-Object { $Foods.$_ |
        where { $_ -like "${wordToComplete}*" } } |
        sort -Unique | ForEach-Object {
            New-Object System.Management.Automation.CompletionResult (
                $_,
                $_,
                'ParameterValue',
                $_
            )
        }
}
After running those, Get-Food‘s parameters should filter each other as described earlier:
 2017-01-17_21-49-52

 

Note that we didn’t actually need two separate scriptblocks when calling Register-ArgumentCompleter above. Notice that there are $commandName and $parameterName parameters that are passed when the scriptblock gets invoked (again, like any PS function, the parameter names are up to you…I’m just using the same param() block that TabExpansionPlusPlus used). You can use those to figure out what type of completion results to return. Then you can save the scriptblock, and just re-use it in the different calls to Register-ArgumentCompleter. Here’s what that might look like:
$Foods = @{
    Fruit = echo Apple, Orange, Banana, Peach
    Vegetable = echo Asparagus, Carrot, Edamame, Broccoli, Spinach
    Protein = echo Beef, Pork, Chicken, Fish, Edamame
    Grain = echo Rice, Oatmeal, Pasta, Bread
}

function Get-Food {
    param(
        [string] $FoodType,
        [string] $FoodName
    )

    "FoodName: ${FoodName}`nFoodType: ${FoodType}"
}

$GetFoodCompleter = {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
    $Foods.Keys.ForEach({
       $CurrKey = $_
       switch ($parameterName) {
           FoodName {
               $Source = $CurrKey
               $ReturnValue = $Foods[$CurrKey]
               $Filter = $fakeBoundParameter.FoodType
           }
           FoodType {
               $Source = $Foods[$CurrKey]
               $ReturnValue = $CurrKey
               $Filter = $fakeBoundParameter.FoodName
           }

           default { return }
       }
       if ($Source -like "${Filter}*") {
           $ReturnValue
       }
    }) | sort -Unique | where { $_ -like "${wordToComplete}*" } | ForEach-Object {
       [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
    }
}
echo FoodType, FoodName | ForEach-Object {
    Register-ArgumentCompleter -CommandName Get-Food -ParameterName $_ -ScriptBlock $GetFoodCompleter
}
In this example it doesn’t really matter, but it makes sense in a lot of other scenarios to keep that kind of code together.

 

By the way, this barely scratches the surface of what you can do with argument completers. For more information, you can check out this presentation I gave at the PowerShell + DevOps Global Summit 2016 (The code samples from that presentation are on GitHub).
Advertisements