So I'm experimenting with PowerShell and having a little trouble understanding parameters. From what I've read if I specify a parameter to be at the same position as another but place it in a separate ParameterSet PowerShell will only require one of these parameters to be present.
In this example that works as expected -
[CmdletBinding(DefaultParameterSetName='MultiUser')]
Param(
[Parameter(Mandatory=$True,Position=1)]
[string]$Token,
[Parameter(Mandatory=$True,Position=2, ParameterSetName="MultiUser")]
[string]$UsernamesFile,
[Parameter(Mandatory=$True,Position=2, ParameterSetName="SingleUser")]
[string]$SingleUsername,
[Parameter(Mandatory=$False)]
[switch]$SpecialCase
)
But if I wanted to expand on this so that as above you have to specify a token and then you must specify either a single username or a usernames files but now I would like to specify a group the user was going to be in.
Now let's assume the user has to go into one of two groups and I don't want to worry about dealing with different ways a user may input a group name so I use two switches. I only want the user to be in a single group rather than both so the switches should be at the same position but in different parameter sets (based on what I've read and that the above example works).
So my second example looks like this -
[CmdletBinding(DefaultParameterSetName='MultiUser')]
Param(
[Parameter(Mandatory=$True,Position=1)]
[string]$Token,
[Parameter(Mandatory=$True,Position=2, ParameterSetName="MultiUser")]
[string]$UsernamesFile,
[Parameter(Mandatory=$True,Position=2, ParameterSetName="SingleUser")]
[string]$SingleUsername,
[Parameter(Mandatory=$True,Position=3, ParameterSetName="GroupA")]
[switch]$GroupA,
[Parameter(Mandatory=$True,Position=3, ParameterSetName="GroupB")]
[switch]$GroupB,
[Parameter(Mandatory=$False)]
[switch]$SpecialCase
)
This doesn't work as expected however, this gives me an error -
Could someone explain why this doesn't work and correct my understanding of PowerShell parameters?
Thanks!
See bottom for an explanation of the problem with the OP's approach.
To get exactly what you're asking for, you'd have to use the following:
# - Make sure that parameters are NON-positional unless explicitly marked otherwise.
# - Specify the default parameter set.
[CmdletBinding(PositionalBinding=$False, DefaultParameterSetName='MultiUserA')]
Param(
# Belongs to all parameter sets.
[Parameter(Mandatory, Position=1)]
[string]$Token,
# Mandatory and positional both when combined with -GroupA or -GroupB.
[Parameter(Mandatory, Position=2, ParameterSetName='MultiUserA')]
[Parameter(Mandatory, Position=2, ParameterSetName='MultiUserB')]
[string] $UsernamesFile,
# Mandatory - but not positional - both when combined with -GroupA or -GroupB.
[Parameter(Mandatory, ParameterSetName='SingleUserA')]
[Parameter(Mandatory, ParameterSetName='SingleUserB')]
[string] $SingleUsername,
# Mandatory, whether combined with -UsernamesFile or -SingleUsername
[Parameter(Mandatory, ParameterSetName='SingleUserA')]
[Parameter(Mandatory, ParameterSetName='MultiUserA')]
[switch] $GroupA,
# Mandatory, whether combined with -UsernamesFile or -SingleUsername
[Parameter(Mandatory, ParameterSetName='SingleUserB')]
[Parameter(Mandatory, ParameterSetName='MultiUserB')]
[switch] $GroupB,
# Belongs to all parameter sets. Non-mandatory by default.
[switch] $SpecialCase
)
As you can see,
You need to define 4 parameter sets that amount to all users-file-vs.-single-user and groupA-vs.-group-B combinations.
You need to assign each parameter to multiple parameter sets, choosing the appropriate subset.
[Parameter(...)]
attribute can only specify 1 parameter set, and whatever other attributes you specify there - Mandatory
, Position
, ... - apply to the parameter only in the context of the given parameter set.When you invoke your script with -?
(or pass it to Get-Help
) you'll see the resulting syntax diagram:
script.ps1 [-Token] <string> [-UsernamesFile] <string> -GroupA [-SpecialCase] [<CommonParameters>]
script.ps1 [-Token] <string> [-UsernamesFile] <string> -GroupB [-SpecialCase] [<CommonParameters>]
script.ps1 [-Token] <string> -SingleUsername <string> -GroupB [-SpecialCase] [<CommonParameters>]
script.ps1 [-Token] <string> -SingleUsername <string> -GroupA [-SpecialCase] [<CommonParameters>]
However, this approach is ill-advised for the following reasons:
You shouldn't have mandatory [switch]
parameters, as they're by definition optional.
When you invoke the script without parameters for interactive argument entry, PowerShell won't let you specify a switch value (no values I've tried work: not true
, false
, 1
, 0
, ... - try with ./script someToken someFile
)
./script.ps1 someToken -SingleUsername someUser
gives a generic error message (Parameter set cannot be resolved using the specified named parameters.
) rather than specifically pointing out that either -GroupA
or -GroupB
is missing, because PowerShell cannot know whether you meant parameter set SingleUserA
or SingleUserB
.
./script someToken someFile
- with implied -UsernamesFile
- is unambiguous, because MultiUserA
is the default parameter set, but due to the -GroupA
switch being mandatory, you still get a prompt for its value).Last, but not least, as Mathias R. Jessen points out, using distinct, mutually exclusive switches (-GroupA
vs. -GroupB
) doesn't scale well, as adding more -Group*
switches quickly makes the number of combinations that must each be reflected in their own parameter set unmanageable - see below for how to avoid that.
As pointed out in Mathias R. Jessen's helpful answer, the better approach is to use a single parameter for the target group that accepts only a value from a given set of values, which the [ValidationAttribute]
can ensure:
[CmdletBinding(PositionalBinding=$False, DefaultParameterSetName='MultiUser')]
Param(
[Parameter(Mandatory, Position=1)]
[string] $Token,
[Parameter(Mandatory, Position=2, ParameterSetName='MultiUser')]
[string] $UsernamesFile,
[Parameter(Mandatory, ParameterSetName='SingleUser')]
[string] $SingleUsername,
# Single -Group parameter that only accepts values 'GroupA' and 'GroupB'
# Input validation is case-INsensitive, as usual.
[Parameter(Mandatory)]
[ValidateSet('GroupA', 'GroupB')]
[string] $Group,
[switch] $SpecialCase
)
This gives us the following syntax diagrams (note that the set of valid values for -Group
is not reflected):
script.ps1 [-Token] <string> [-UsernamesFile] <string> -Group <string> [-SpecialCase] [<CommonParameters>]
script.ps1 [-Token] <string> -SingleUsername <string> -Group <string> [-SpecialCase] [<CommonParameters>]
This reduces the number of required parameter sets to 2.
It supports interactive entry of the group name (although, somewhat unfortunately, supplying an invalid name aborts the invocation).
If you omit -Group
, both the ./script.ps someToken someFile
and the ./script.ps someToken -SingleUserName someUser
now behave the same: they prompt for the -Group
value.
While having to type -Group
and a value is perhaps slightly more cumbersome on invocation than having distinct switches -GroupA
and -GroupB
,
As for problems with your original approach:
As Clijsters points out, your attempt to invoke ./script.ps1 -Token a -UsernamesFile someFile -GroupA
failed, because:
PowerShell needs to unambiguously resolve the given combination of parameters to a parameter set.
-GroupA
only belongs to parameter set GroupA
, whereas -UsersnameFile
only belongs to parameter set MultiUser
, so these parameters are in effect mutually exclusive, and PowerShell cannot determine what parameter set to use.
All non-switch parameters are positional by default - unless you explicitly deactivate that with [CmdletBinding(PositionalBinding=$False,...)]
- then only individual [Parameter(...)]
attributes explicitly marked with a Position
attribute become positional.
Also, there is no point in making a [switch]
parameter positional, as they're by definition non-positional: you always have to specify their name (unambiguously), and that allows them to be placed anywhere.