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:

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).
Very nice introduction. Didn’t know this existed.
I would be more inclined to use a C# enum to do parameter autocompletion, but Register-ArgumentCompleter would be better for dynamic value lists
Agreed, enums (and [ValidateSet()]) are much easier to get autocompletion. Like you say, though, those won’t work with dynamic values, e.g., querying some data source to get these values (you can use dynamic parameters, but that’s usually more work than the completers).
Also, using completers doesn’t force users to input valid values, it just suggests some values for them. Sometimes this is what you want, but the simple example command above would probably need some sort of validation logic inside of it, where an enum or [ValidateSet()] decoration wouldn’t.
Thanks for your help and thanks for posting this Rohn!
This post is going to help a lot of people 🙂
No problem! Thanks for the suggestion to do it!
Honestly, thank you a lot for your example and your explanation. I was Googling for an example for `fakeBoundParameter`, but what I got was a complete example of how to use `Register-ArgumentCompleter`.
This is a great post, and your 2016 presentation was very helpful tool.
From within an argument completer, do you know how to determine an object’s type that is being piped in? Basically I’m trying to do something like Select-Object does for the -Property argument. `Get-Date | Select-Object -Property ` shows all the DateTime properties. I know the type can’t always be known, but PowerShell seems to do a good job of taking an educated guess. I can see the full pipeline text from the CommandAst but not sure where to go from there to figure out the type info.
Thanks for the kind words!
About your question: I don’t think the completer has direct access to the actual type of any objects piped into it. Since the user may not actually press enter, the code that runs in the background is careful not to cause any side effects, so there are lots of things, by design, that the engine doesn’t try to do, including actually binding any parameters. I don’t think there’s something like $fakeBoundParameters that would serve that info up to you (I could be wrong).
The command’s AST is enough to probably do what you’re asking, though. I actually just mocked up some code that works when a variable is just before it in the pipeline, and also when another command is just before it, and I think it might do what you’re looking for. It’s too big for a comment, so you can see it in this Gist: https://gist.github.com/rohnedwards/1a78c57936d773f2a541d7ac3124f921
The examples show a few instances where I know it won’t work, but it might be a start 🙂
And, of course, there may be some other way to handle this that I’m not aware of. Good luck!
[…] I go any farther, shout out to Rohn Edwards ( blog | twitter ) for his session at PowerShell Summit which got me started with this. (And then being […]
[…] Completing Parameter Values with Other Parameter Values […]