PowerShell functions and Parameter Sets

Page content

A PowerShell function can have different parameters depending on how it is called. This is called Parameter Sets. For example, Get-Process has a non mandatory parameter called Name which specifies which processes to Get by Name. But is also has a parameter called ID which also specifies which processes to get, this time by ID. Both parameters exists but are mutually exclusive, you cannot use them both at the same time, since they are defined in two different Parameter Sets.

First some basics

A parameter set is defined in the [Parameter()] block of a Parameter.

For example:

function Test-MultiParamSets {
param(
[Parameter(ParameterSetName = 'Name')]
[string]$Name,

[Parameter(ParameterSetName = 'ID')]
[int]$ID
)
$PSCmdlet.ParameterSetName
}

This defines a function with two ParmeterSets, Name and ID. The ParameterSet Name has one parameter called Name and the ParameterSet ID has one parameter called ID. This means that the parameters can’t be used at the same time.

The function itself will write only the name of the current set to the pipeline. This is accessed by the property ParameterSetName on the automatic variable $PSCmdlet.

There are a few ways to investigate a command and see which parameter sets that are available. This easiest way is to call Get-Command with the parameter –Syntax like this:

image

This shows each parameter set that can be used. The first set has one non-mandatory named parameter, Name and the second one has one non-mandatory named parameter, ID.

All is good as long as one of the parameters is specified but if we try to run the command either without specifying any parameter we will get an error stating:

Parameter set cannot be resolved using the specified named parameters.

Which means that PowerShell did not know which parameter set to use and threw an error.

There are two ways to get around this. Either we can set one, and only one of the Parameters to mandatory. If Name is mandatory and the function is called without parameters, PowerShell will assume that the set is ID since that is the only set without any mandatory parameters.

BUT there is a better way! By setting one parameter set as default, PowerShell will know which one to use if more than one matches. This is done by simply adding this:

[CmdletBinding(DefaultParameterSetName = 'Name')]

One Parameter in multiple Sets

All right, so far things are fairly straight forward. What if I want one parameter to be a member of more than one set?

Then give it more than one [Parameter()] block!

Let’s say that I want to be able to call the function using either only Name, or with both ID and Name but Name should be mandatory only in the first scenario.

Here is a new example:

function Test-MultiParamSets {
[CmdletBinding(DefaultParameterSetName = 'Name')]
param(
[Parameter(ParameterSetName = 'Name', Mandatory = $true)]
[Parameter(ParameterSetName = 'ID')]
[String]$Name,

[Parameter(ParameterSetName = 'ID')]
[int]$ID
)
$PSCmdlet.ParameterSetName
}

Now if I run just Test-MultiParamSets without parameters, PowerShell will use the default set I specified which is ‘Name’ and prompt me for a Name since it’s mandatory.

If I on the other hand run Test-MultiParamSets using the parameter ID, I will not be prompted for a name since it is not mandatory in this parameter set.

image

Running Get-Command –Syntax again will now show this:

image

We can clearly see that the parameter Name is only mandatory in the first set.

Are you still awake? Awsome! Then let’s get one sprint further down the rabbit hole!

Using AllParameterSets

Now I start to get quite satisfied with my function, but after using it regularly I get tired of always typing –Name, I want the parameter Name to always have position 0, meaning that the first unnamed parameter value will always belong the parameter Name (as long as the parameter hasn’t been assigned a value using -Name).

One way to do this is to add a new Parameter block with Position = 0 in it. Since this block doesn’t have any ParameterSetName in it, it will apply to all the sets.

Here is the example code again, this time updated again:

function Test-MultiParamSets {
[CmdletBinding(DefaultParameterSetName = 'Name')]
param(
[Parameter(ParameterSetName = 'Name', Mandatory = $true)]
[Parameter(ParameterSetName = 'ID')]
[Parameter(Position = 0)]
[String]$Name,

[Parameter(ParameterSetName = 'ID')]
[int]
$ID
)
'Set name is:{0}' -f $PSCmdlet.ParameterSetName
'Name is: [{0}], ID is [{1}]' -f $Name, $ID
}

Now I can run Test-MultiParamSets and it will accept Name as a positional parameter. To make it more clear which parameter has which value, the function now also prints the value of each parameter.

image

Let’s get back and have a look at the Syntax from Get-Command:

image

Here is when things get a little bit messy. Get-Command still shows the parameter Name as being named and not Positional!

The caveat here is that the properties in my third Parameter block is actually not added to any parameter set but to a new set called __AllParameterSets.

To see this we can use the object returned from Get-Command. It has a property called Parameters, which is a hashtable with one key for each parameter. If we look only on the parameters Name and ID (the function also has a bunch of common parameters):

image

This shows that the parameter Name is part of three parameter sets, even though only two is shown by Get-Command –Syntax.

The parameter set __AllParameterSets is a hidden parameter set which properties will merge with all the other sets. My experience is that if a property is set in a named ParameterSet, that property wins, otherwise the Property from __AllParameterSets is used. **However, setting a property to $false counts as it being not set, meaning if Mandatory is True in __AllParameterSets and False in both the set Name and ID, True wins.

A screenshot to summarize:

image

Note here that the parameter Name is named (Position is not set) in the sets Name (Green) and ID (Red) but Positional in __AllParameterSets, meaning that the __AllParameterSets wins and parameter is positional.

Any property applied to a parameter set from __AllParameterSets is not shown when using Get-Command –Syntax. Name shows up as a named parameter for both sets in the picture above.

Workaround

To make Get-Command show the correct syntax, don’t use __AllParameterSets, make it a habit to add every property you want in a set to that set, even you have to add it multiple times. I also find the code much more readable this way.

Here is the code once more, written the way I think should be the best practice:

function Test-MultiParamSets {
[CmdletBinding(DefaultParameterSetName = 'Name')]
param(
[Parameter(ParameterSetName = 'Name', Mandatory = $true, Position = 0)]
[Parameter(ParameterSetName = 'ID', Position = 0)]
[String]
$Name,

[Parameter(
ParameterSetName = 'ID'
)]
[int]
$ID
)
'Set name is:{0}' -f $PSCmdlet.ParameterSetName
'Name is: [{0}], ID is [{1}]' -f $Name, $ID
}

Now each set has the property Position = 0

Running Get-Command –Syntax again will show the correct syntax:

image

If you read this far, thank you! And please leave a comment.