Parameters are a way for the caller to submit information to code. They are a fundamental feature of PowerShell scriptblocks: a param() block at the beginning of the scriptblock content defines “public” variables that can be changed by the caller with his arguments. All other variables remain private and inaccessible from the outside.
The [Parameter()] attribute can be used to add special behaviors to parameters. For example, you can use the attribute value Mandatory to make a parameter mandatory. The attribute supports these values:
Name Type
---- ----
DontShow System.Boolean
HelpMessage System.String
HelpMessageBaseName System.String
HelpMessageResourceId System.String
Mandatory System.Boolean
ParameterSetName System.String
Position System.Int32
ValueFromPipeline System.Boolean
ValueFromPipelineByPropertyName System.Boolean
ValueFromRemainingArguments System.Boolean
In this article, you’ll learn how to use all of them:
Attribute | Description |
---|---|
[DontShow()] | Hide parameter in IntelliSense and tab completion |
[HelpMessage()] | Provide a help message for mandatory parameters |
[HelpMessageBaseName()] | Name of resource assembly that contains compiled help messages |
[HelpMessageResourceId()] | Resource identifier for help message |
[Mandatory()] | Make parameter mandatory and prompt for value when the user does not supply one |
[ParameterSetName()] | Define mutually exclusive parameters |
[Position()] | Assign positions to parameters and use arguments without parameter names |
[ValueFromPipeline()] | Assign pipeline input to a parameter |
[ValueFromPipelineByPropertyName()] | Assign a specific property of pipeline input to a parameter |
[ValueFromRemainingArguments()] | Create a ParamArray and assign any unbound argument to a parameter |
Quick Primer on Parameters
Let’s take a quick look at PowerShell parameters before diving deep into the [Parameter()] attribute use cases. If you are familiar with parameters already you can safely skip this part.
The most common use case for parameters are PowerShell functions:
function Test-This
{
# define parameters
param
(
# variables define one or more parameters
# this is a comma-separated list!
$Name = $env:username, # <-- don't forget the comma!
$Id = 5
)
"Hello $Name, your ID is $Id."
}
Test-This -Name Tobias -Id 2
PowerShell supports a shortcut for param() blocks: place the parenthesis after the function name to implicitly create a param() block:
function Test-This($Name = $env:username, $Id = 5) { "Hello $Name, your ID is $Id." } Test-This -Name Tobias -Id 2 # internally, PowerShell creates a param() block # (view function source code): ${function:Test-This}
It’s up to you whether you fall in love with the shortcut syntax or stick to param() blocks for your function definitions. I prefer using the param() block because this technique always works the same across various scenarios and produces code that is easier to format and read. In addition, without substantial changes you can turn your code into an anonymous scriptblock, or save it as a PowerShell script.
Scriptblocks Implement Parameters
Parameters really are a basic feature of scriptblocks, so functions are just a specific use case of scriptblocks. Parameters work for anonymous scriptblocks just as well:
$code = {
param
(
$Name = $env:username,
$Id = 5
)
"Hello $Name, your ID is $Id."
}
& $code -Name Tobias -Id 2
So when you execute an anonymous scriptblock via Remoting and Invoke-Command
, you can submit arguments via -ArgumentList and receive them on the remote side via parameters:
$code = {
# receive the arguments submitted by the caller:
param
(
$Name = $env:username,
$Id = 5
)
"Hello $Name, your ID is $Id."
}
# execute scriptblock on another computer, and submit list of
# arguments to the remote machine
$UserName = 'Tobias'
$UserId = 2
Invoke-Command -ScriptBlock $code -ArgumentList $UserName, $UserId <# -ComputerName abc #>
Invoke-Command
supports a shortcut for submitting arguments: when you prefix a variable with using:, PowerShell automatically sends the caller variable to the remote system:$code = { "Hello $using:UserName, your ID is $using:UserId." } # the variable prefix "using:" is supported only when executing # code on a remote machine. The prefix fails when Invoke-Command is # used without the parameter -ComputerName. $UserName = 'Tobias' $UserId = 2 Invoke-Command -ScriptBlock $code -ComputerName 127.0.0.1
However, this shortcut is not as versatile as using param() blocks, and chances are the above code won’t work on your machine. The prefix using: is supported only in true remoting scenarios and fails when
Invoke-Command
is not using the parameter -ComputerName.It’s up to you whether you fall in love with using: or stick to param() blocks for remoting code. I prefer using the param() block because this technique always works the same across various scenarios and is more predictable than the automagical using:.
PowerShell Scripts are Scriptblocks, Too
Since .ps1 PowerShell script files are technically just one big scriptblock, param() works for script files as well:
# define parameters
param
(
# variables define one or more parameters
# this is a comma-separated list!
$Name = $env:username, # <-- don't forget the comma!
$Id = 5
)
"Hello $Name, your ID is $Id."
When you store above code as c:\test\script1.ps1, submit your arguments like this:
& "c:\test\script1.ps1" -Name Tobias -Id 2
Using Mandatory Parameters
By default, PowerShell parameters are optional. When a user does not submit arguments to a parameter, PowerShell uses its default value. If no default value exists, the parameter value is $null.
This is not always desired. There are situations when default values simply do not make sense. For example, the cmdlet Get-EventLog
reads eventlog entries and must know the name of the eventlog it should read. It can neither work without an eventlog name, nor does it make sense to default to any eventlog name.
That’s when mandatory parameters are helpful: the user must submit a value, and if no value is specified, PowerShell prompts the user. Get-EventLog
has declared the parameter -LogName mandatory, so when you run the cmdlet without parameters, you get prompted for a log name. With the help of [Parameter()], you can do the same with your own parameters.
Workarounds
Before [Parameter()] was introduced in PowerShell 2, there was a common workaround to turn optional parameters into mandatory parameters: the default value was set to a command requesting a value from the user:
function Test-This
{
param
(
# turn optional parameter into mandatory parameter
# by assigning a command as default value:
$Name = $(Read-Host -Prompt 'Enter a name please'),
$Id = 5
)
"Hello $Name, your ID is $Id."
}
Test-This
The advantage of this approach is your flexibility: you entirely control the prompting:
PS> Test-This
Enter a name please: Tobias
Hello Tobias, your ID is 5.
The disadvantage is inconsistency because of this flexibility: the prompt can be designed in a multitude of ways, irritating a user. In addition, by looking at the syntax you cannot identify such mandatory parameters because technically, they are still optional parameters:
PS> help Test-This -Parameter *
-Id <Object>
Required? false
Position? 1
Accept pipeline input? false
Parameter set name (All)
Aliases None
Dynamic? false
-Name <Object>
Required? false
Position? 0
Accept pipeline input? false
Parameter set name (All)
Aliases None
Dynamic? false
Creating Mandatory Parameters
The attribute [Parameter()] can turn any parameter into a mandatory parameter and uses below values to control this behavior:
Name | Type | Description |
---|---|---|
Mandatory | Boolean | Parameter is mandatory. PowerShell prompts for a value if the user omits the parameter. |
HelpMessage | String | Help message that shows when the user enters !? at the prompt |
HelpMessageBaseName | String | Name of a resource assembly that contains compiled help messages |
HelpMessageResourceId | String | Resource identifier for the particular help message to be used |
HelpMessageBaseName and HelpMessageResourceId are not used in PowerShell code since PowerShell cannot create resource assemblies. These values exist for C# developers that create binary cmdlets and want to read localized help messages from their DLL files.
Here is an example for a PowerShell function with a mandatory parameter and help message:
function Test-This
{
param
(
# Name must be submitted:
[Parameter(Mandatory,HelpMessage='Enter the user name!')]
$Name,
# Id is optional and uses the value 5 by default
$Id = 5
)
"Hello $Name, your ID is $Id."
}
Test-This -Name Tobias -Id 2
When you omit the parameter -Name, PowerShell prompts for a value:
PS> Test-This
cmdlet Test-This at command pipeline position 1
Supply values for the following parameters:
(Type !? for Help.)
Name: !?
Enter the user name!
Name: Freddy
Hello Freddy, your ID is 5.
Since a help message was defined, PowerShell adds “(Type !? for Help.)” to the prompt, and when the user enters !?, the help message displays.
Gotcha: Assign Types to Mandatory Parameters
When PowerShell prompts the user for a mandatory value, it always reads a string value, even if the user enters numbers.
That’s why you should always assign the appropriate type to a mandatory parameters. If you don’t, bad things can happen. Have a look:
Example: Currency Converter With A Flaw
Here is a currency converter that seems to run just fine:
function Convert-Currency
{
param
(
[Parameter(Mandatory,HelpMessage='Dollars to convert')]
$Dollar,
$Rate = 1.12
)
$Dollar * $Rate
}
Convert-Currency -Dollar 20 -Rate 2.5
Mandatory Parameter Values Are Strings
Once you omit the mandatory parameter, PowerShell prompts for it. The value for $dollar now is of type string, and the result is unexpected:
PS> Convert-Currency
cmdlet Convert-Currency at command pipeline position 1
Supply values for the following parameters:
(Type !? for Help.)
Dollar: !?
Dollars to convert
Dollar: 100
100
When you play with -Rate, the result looks even stranger:
PS> Convert-Currency -Rate 12.54
cmdlet Convert-Currency at command pipeline position 1
Supply values for the following parameters:
(Type !? for Help.)
Dollar: 100
100100100100100100100100100100100100100
The reason: because of the prompt, $Dollar is a string, and the result is a string concatenation:
PS> "100" * 5
100100100100100
PS> 100 * 5
500
Simple Fix
Fixing this issue is easy: assign the desired type to the parameter so PowerShell converts the prompted value into the correct type:
function Convert-Currency
{
param
(
[double] # <- add the desired type!
[Parameter(Mandatory,HelpMessage='Dollars to convert')]
$Dollar,
[double]
$Rate = 1.12
)
$Dollar * $Rate
}
While it’s a good idea to add the appropriate types routinely to every parameter, it is crucial to do so at least for mandatory parameters because of the way they can be read from a prompt.
Arrays: Prompting for Multiple Values
When a mandatory parameter type is an array (can have multiple values), PowerShell adjusts the prompting and allows the user to enter more than one value. Array types are defined by adding [] to the type name. The example below accepts any number of strings:
function Test-This
{
param
(
# allow parameter to be an array
[String[]]
[Parameter(Mandatory)]
$Name
)
# set output field separator to a comma:
$ofs = ','
"Names: $Name"
}
# supply more than one value to the parameter:
Test-This -Name Tobias,Alexandar,Rob
# test the automatic prompting:
Test-This
When you run the code, you now get multiple prompts and an index number:
PS> Test-This
Names: Tobias,Alexandar,Rob
cmdlet Test-This at command pipeline position 1
Supply values for the following parameters:
Name[0]: First Name
Name[1]: Second Name
Name[2]:
Names: First Name,Second Name
To end input, press Enter
with an empty input.
Controlling Pipeline Support
The truth is: PowerShell functions are self-focused and don’t care much about the outside world. They are never pipeline-aware and receive user input always exclusively via their parameters.
It is PowerShell that manages the pipeline. In a pipeline, PowerShell takes the output from upstream commands and feeds them to the appropriate parameters of a following command. For a command, there is no difference whether user input was directly assigned to parameters or streamed into the command via pipeline.
Prerequisites
For *PowerShell functions to work inside a pipeline, two things are required:
- The attribute [Parameter()] clearly defines the parameters that should receive input from an upstream command
- The function code is separated into the blocks begin, process, and end (see below)
These are the attribute values that control pipeline support:
Name | Type | Description |
---|---|---|
ValueFromPipeline | Boolean | Parameter should receive pipeline input if it matches the type |
ValueFromPipelineByPropertyName | Boolean | Parameter should receive pipeline input if the received object has a property named like the parameter, and the property value matches the parameter type |
ISA and HASA Contract
The challenge of pipeline support is to find a way how commands from all kinds of vendors and programmers can peacefully work together. This is done by the “ISA/HASA Contract”:
- ISA (“is a”): the incoming data is exactly what the parameter needs (defined by attribute value ValueFromPipeline)
- HASA (“has a”): the incoming data contains what the parameter needs in one of its properties (defined by attribute value ValueFromPipelineByPropertyName)
Binding Pipeline Data
Here is a simple example: the function Test-This
accepts any string input from the pipeline:
function Test-This
{
param
(
# this parameter cannot receive data from the pipeline:
[string]
$Filter = '*',
# this parameter CAN receive data from the pipeline,
# provided it can be converted to string:
[Parameter(ValueFromPipeline)]
[string]
$InputData
)
"Received: $InputData"
}
# user can assign arguments to parameters directly:
Test-This -InputData SomeText
# user can pipe data to function via pipeline:
Get-Process | Test-This
You can assign arguments directly to the parameter of Test-This
, but on top you can also pipe any data into the function that converts to string:
PS> Get-Process | Test-This
Received: System.Diagnostics.Process (YourPhone)
PS> Get-ChildItem c:\windows | Test-This
Received: write.exe
PowerShell takes the incoming data and looks at the parameters provided by Test-This
. It identifies the parameter -InputData as willing to accept pipeline data, and assigns the value to it.
PS> help Test-This -Parameter *
-Filter <string>
Required? false
Position? 0
Accept pipeline input? false
Parameter set name (All)
Aliases None
Dynamic? false
-InputData <string>
Required? false
Position? 1
Accept pipeline input? true (ByValue)
Parameter set name (All)
Aliases None
Dynamic? false
However, apparently only the last pipeline item made it into Test-This
.
Looping Pipeline Data
To process multiple pipeline items, the function needs a loop. This loop is built into PowerShell scriptblocks because internally, each scriptblock has three compartments:
- begin: executes once before any pipeline data is received. Can be used to initialize whatever might be necessary
- process: this is the loop. It repeats for every received pipeline item.
- end: executes once after all pipeline data is processed. Can be used to do cleanup work.
If you do not specify these compartments, PowerShell always places the entire scriptblock code into the end block which is why above example only processed the last pipeline element.
If you do specify at least one of these compartments, all of your code must be assigned to any of these three compartments. Here is the fully functional code:
function Test-This
{
param
(
# this parameter cannot receive data from the pipeline:
[string]
$Filter = '*',
# this parameter CAN receive data from the pipeline,
# provided it can be converted to string:
[Parameter(ValueFromPipeline)]
[string]
$InputData
)
# place the code into the process block to run it for each
# incoming pipeline element:
process
{
"Received: $InputData"
}
}
A Custom Grep Command
Thanks to pipeline support, building powerful command line tools is easy. Here is a function called grep that filters data by string keyword. Unlike classic grep commands, this one is fully object-oriented and yields rich object output.
Text-filtering is quick and dirty and never precise. It’s easy to use though and can be a powerful tool for data exploration.
function grep
{
param
(
# filter string that must be present in object string representation
[Parameter(Mandatory)]
[string]
$Filter,
# data to be filtered
[Parameter(ValueFromPipeline)]
[object]
$InputData
)
process
{
# check all properties for search filter
$include = ($InputData | Out-String) -like "*$Filter*"
# output original object if filter matched:
if ($include) { $InputData }
}
}
When you pipe data to grep, it temporarily converts each incoming object to string using Out-String
. If the object string representation contains the filter word, the incoming object is passed on.
PS> Get-Service | grep run
Status Name DisplayName
------ ---- -----------
Running AdobeARMservice Adobe Acrobat Update Service
Running Appinfo Application Information
Running AudioEndpointBu... Windows Audio Endpoint Builder
...
PS> Get-ChildItem -Path c:\windows | grep '-a--s-'
Directory: C:\windows
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a--s- 15.02.2020 10:53 67584 bootstat.dat
Binding Pipeline Data By Property
Incoming pipeline data can be bound to more than one parameter. This of course doesn’t make much sense when you use the value ValueFromPipeline because every parameter would receive the same information.
However, you can bind the properties of incoming objects to different parameters, and that makes a whole lot more sense. Just use ValueFromPipelineByPropertyName. PowerShell takes the incoming object and binds its properties to the parameters that match in name and type.
Here is an example:
function Test-PipelineBinding
{
param
(
# receive the entire incoming object
[Parameter(ValueFromPipeline)]
$EntireObject,
# receive the property ID
[Parameter(ValueFromPipelineByPropertyName)]
[int]
$Id,
# receive the property Name
[Parameter(ValueFromPipelineByPropertyName)]
[string]
$Name
)
process
{
# emit all received properties
$PSBoundParameters
}
}
# pipe suitable objects into the function:
Get-Process | Test-PipelineBinding
Test-PipelineBinding
can deal with objects that have a name property of type string and an id property of type int.
Get-Process
returns such objects, so you can pipe the results directly into the new function.
Adding Parameter Aliases
As you have seen, the name of your parameters define the name of the properties that are bound to them. What if your information source uses names that you cannot use as parameter name? For example, what if the incoming object property is called SuperCrypticStupidID and you don’t want to name your function parameter like this?
Or even more relevant: what if your function needs to support more than one information source?
This is what parameter aliases are for. Just add as many alias names to your parameter. PowerShell then tries to bind the incoming object first by parameter name, then by one of the parameter aliases. First match wins.
function Test-PipelineBinding
{
param
(
# receive the entire incoming object
[Parameter(ValueFromPipeline)]
$EntireObject,
# receive the property ID
[Parameter(ValueFromPipelineByPropertyName)]
[int64]
# add two more names for this parameter:
[Alias('Length','ifIndex')]
$Id,
# receive the property Name
[Parameter(ValueFromPipelineByPropertyName)]
[string]
$Name
)
process
{
# emit all received properties
$PSBoundParameters
}
}
# pipe suitable objects into the function:
Get-Process | Test-PipelineBinding
# thanks to the aliases, your function is compatible to a
# wide range of data (even if it does not make much sense
# in this demo)
dir c:\windows | Test-PipelineBinding
Get-NetAdapter | Test-PipelineBinding
Controlling Parameter Binding
Binding is the process when PowerShell takes user arguments and assigns them to the appropriate parameters. [Parameter()] controls binding with these values:
Name | Type | Description |
---|---|---|
Position | Integer | Defines positional parameters that are bound based on argument position rather than parameter name |
ValueFromRemainingArguments | Boolean | Assigns all unbound arguments to this parameter |
Positional Parameters
Positional parameters exist solely for convenience: when you assign a position to a parameter, the user can omit the parameter name provided the arguments are specified in the expected order.
# read event ids 1 - 20 from system event log
# use named parameters in scripts for better readability:
Get-EventLog -LogName System -InstanceId (1..20)
# use positional parameters in the shell when hair is on fire:
Get-EventLog System (1..20)
Read carefully: you can omit parameters when you assign a position to them. Without an assigned position, a parameter cannot be used positional.
Positional By Default
Unfortunately, PowerShell uses a confusing scheme to determine whether a function parameter is positional or not. By default, all parameters are positional, and PowerShell assigns positions to parameters in the order in which they are defined in your code:
function Test-Positional
{
param
(
[String]
$Name,
[Int]
$Id
)
# return all submitted parameters:
$PSBoundParameters
}
# use named parameters
Test-Positional -Name Tobias -Id 1
# use positional parameters
Test-Positional Tobias 1
Both calls return the same information:
Key Value
--- -----
Name Tobias
Id 1
PowerShell automatically assigned position 0 to the first parameter and position 1 to the second:
(Get-Command Test-Positional).ParameterSets.Parameters | Select-Object -Property Name, Position
Name Position
---- --------
Name 0
Id 1
Explicitly Assigning Positions
If you want to assign positions only to some parameters, use the value Position. As a best practice, only the most important parameters should receive a position:
function Test-Positional
{
param
(
[Parameter(Position=0)]
[String]
$Name,
[Int]
$Id
)
# return all submitted parameters:
$PSBoundParameters
}
Once you define positions, all parameters without a position become named: you must now use the parameter name. After you run the code above, take a look at the syntax:
Get-Help Test-Positional
SYNTAX
Test-Positional [[-Name] <string>] [-Id <int>] [<CommonParameters>]
The parameter -Id is no longer positional and no longer embraced by brackets: the parameter name -Id is not optional anymore and must be specified.
Removing All Positions
What if you want all of your parameters to be named? That’s hard because as you have seen, PowerShell assigns positions automatically.
There are two ways:
- Assign all parameters to a Parameter Set. When you use Parameter Sets, PowerShell no longer assigns positions automatically.
- Use the attribute [CmdletBinding()] to explicitly turn off automatic positions.
Here is the first approach to turn all parameters into named parameters:
function Test-Positional
{
[CmdletBinding()]
param
(
[Parameter(ParameterSetName='Dummy')]
[string]
$Name,
[Parameter(ParameterSetName='Dummy')]
[int]
$Id
)
# return all submitted parameters:
$PSBoundParameters
}
PS> Get-Help Test-Positional -Parameter *
-Id <int>
Required? false
Position? Named
Accept pipeline input? false
Parameter set name Dummy
Aliases None
Dynamic? false
-Name <string>
Required? false
Position? Named
Accept pipeline input? false
Parameter set name Dummy
Aliases None
Dynamic? false
With the attribute [CmdletBinding()], you can achieve the same:
function Test-Positional
{
[CmdletBinding(PositionalBinding=$false)]
param
(
[string]
$Name,
[int]
$Id
)
# return all submitted parameters:
$PSBoundParameters
}
Using ParamArrays
By default, each parameter takes exactly one argument (which can be an array). Any unbound arguments (extra arguments) surface in the automatic variable $args. Sometimes, there may be the need for a parameter to take any number of arguments, though.
C# programmer create so-called ParamArrays and use the keyword params. In PowerShell, this functionality is available, too, but implemented differently (see below).
Here is a diagnostic function that illustrates the default binding behavior:
function Test-ParamArray
{
param
(
[int[]]
$Values
)
# return all submitted parameters:
$PSBoundParameters
"Extra arguments: $args"
}
Extra Arguments Surface in $args
When you assign one value, it is bound to $Values:
Test-ParamArray -Values 1
Key Value
--- -----
Values {1}
Extra arguments:
You can assign more than one value as long as you submit them as one array (because the parameter $Values was assigned the type string array ([string[]])):
Test-ParamArray -Values 1,2,3
Key Value
--- -----
Values {1, 2, 3}
Extra arguments:
Once you submit more than one argument, though, the extra arguments remain unbound and spill over in $args:
Test-ParamArray -Values 1 2 3
Key Value
--- -----
Values {1}
Extra arguments: 2 3
Assigning Extra Arguments to Parameter
If you’d like to assign all extra (unbound) arguments to a parameter instead of $args, use the value ValueFromRemainingArguments:
function Test-ParamArray
{
param
(
[Parameter(ValueFromRemainingArguments)]
[int[]]
$Values
)
# return all submitted parameters:
$PSBoundParameters
"Extra arguments: $args"
}
Now, all extra arguments go into $values:
PS> Test-ParamArray 1,2,3
Key Value
--- -----
Values {1, 2, 3}
Extra arguments:
PS> Test-ParamArray 1 2 3
Key Value
--- -----
Values {1, 2, 3}
Extra arguments:
Mutually Exclusive Parameters
Sometimes it just doesn’t make sense to combine parameters. For example, if you design a function that can look up processes either by Process Id or by Name, it wouldn’t make sense to provide both parameters and in fact confuse the user.
In fact, Get-Process
is a perfect example for such a case: it retrieves processes either by name or by process id. That’s why the parameters -Name and -Id are mutually exclusive. Once you use one of them, the other one becomes unavailable.
When parameters are mutually exclusive, they should be grouped by Parameter Sets. Once the user submits a parameter that belongs to a Parameter Set, all parameters assigned to other Parameter Sets become unavailable.
C# programmers use overloads on methods to achieve the same. Parameter Sets are the overloads for functions and cmdlets.
You can discover Parameter Sets by looking at the syntax of any command, i.e. Get-Process
:
(Get-Help -Name Get-Process).Syntax
Get-Process [[-Name] <String[]>] [-ComputerName <String[]>] [-FileVersionInfo] [-Module] [<CommonParameters>]
Get-Process [-ComputerName <String[]>] [-FileVersionInfo] -Id <Int32[]> [-Module] [<CommonParameters>]
Get-Process [-ComputerName <String[]>] [-FileVersionInfo] -InputObject <Process[]> [-Module] [<CommonParameters>]
Get-Process -Id <Int32[]> -IncludeUserName [<CommonParameters>]
Get-Process [[-Name] <String[]>] -IncludeUserName [<CommonParameters>]
Get-Process -IncludeUserName -InputObject <Process[]> [<CommonParameters>]
Apparently, Get-Process
has six different Parameter Sets, and you cannot mix parameters from different Parameter Sets. If you do, PowerShell raises an exception:
PS> Get-Process -Name notepad -Id 1332 Get-Process : Parameter set cannot be resolved using the specified named parameters.
Investigating Parameter Sets
Before you learn how to use Parameter Sets in your own PowerShell functions, it is a good idea to investigate existing Parameter Sets in existing commands.
Parameter Set Names
PowerShell can dump information about Parameter Sets for any cmdlet or function. Let’s investigate Get-Process
. The cmdlet Get-Command
retrieves all information about this cmdlet:
# Command to investigate:
$CommandName = 'Get-Process'
# get command info:
$command = Get-Command -Name $CommandName
Each Parameter Set must have a unique name that is solely used internally to organize the parameter sets (so these names do not matter to the end user):
# Number of parameter sets and their names:
$command.ParameterSets.Name
Name
NameWithUserName
Id
IdWithUserName
InputObject
InputObjectWithUserName
Default Parameter Set Name
One of these Parameter Sets can be made the default Parameter Set: PowerShell uses the default Parameter Set when there are ambiguities, i.e. when the user called the command without any arguments, or when the arguments cannot clearly distinguish the available Parameter Sets:
# Default parameter set:
$command.DefaultParameterSet
Name
Parameters in Parameter Sets
To find out details about a given Parameter Set, simply group them by name and return the groups as hashtable:
# Detailed information per parameter set:
$info = $command.ParameterSets | Group-Object -Property Name -AsHashTable -AsString
Now it is easy to retrieve the details for a particular Parameter Set. Let’s look up the details for Parameter Set “Id”:
# show details for parameter set "Id":
$info["Id"]
Parameter Set Name: Id
Is default parameter set: False
Parameter Name: Id
ParameterType = System.Int32[]
Position = -2147483648
IsMandatory = True
IsDynamic = False
HelpMessage =
ValueFromPipeline = False
ValueFromPipelineByPropertyName = True
ValueFromRemainingArguments = False
Aliases = {PID}
Attributes =
Parameter Name: ComputerName
ParameterType = System.String[]
Position = -2147483648
IsMandatory = False
IsDynamic = False
HelpMessage =
ValueFromPipeline = False
ValueFromPipelineByPropertyName = True
ValueFromRemainingArguments = False
Aliases = {Cn}
Attributes =
Parameter Name: Module
ParameterType = System.Management.Automation.SwitchParameter
Position = -2147483648
IsMandatory = False
IsDynamic = False
HelpMessage =
ValueFromPipeline = False
ValueFromPipelineByPropertyName = False
ValueFromRemainingArguments = False
Aliases = {}
Attributes =
...
You get back rich information per parameter and can actually see the effect of the other attributes described in this article:
Property | Controlled by |
---|---|
Position | [Position()] |
IsMandatory | [Mandatory()] |
HelpMessage | [HelpMessage()] |
ValueFromPipeline | [ValueFromPipeline()] |
ValueFromPipelineByPropertyName | [ValueFromPipelineByPropertyName()] |
ValueFromRemainingArguments | [ValueFromRemainingArguments()] |
Aliases | [Alias()] |
In essence, each Parameter Set groups a number of parameters. A parameter does not have to be part of a Parameter Set (in which case it would be available in all Parameter Sets), and a parameter can also be part of more than one Parameter Set.
Listing All Parameters
To dump all available parameters (regardless of Parameter Set), use the property Parameters instead of ParameterSets. That’s the right choice if all you want is to auto-document a command. Or dump otherwise hard-to-get information such as the alias names for parameters:
function Get-ParameterAlias
{
param
(
[String]
[Parameter(Mandatory)]
$CommandName
)
Get-Command -Name $CommandName |
# read the parameters hashtable:
Select-Object -ExpandProperty Parameters |
# get all values from the hashtable:
Select-Object -ExpandProperty Values |
# dump alias information:
Select-Object -Property Name, Aliases, ParameterType |
# sort by name:
Sort-Object -Property Name
}
Now it’s easy to discover the alias names for parameters:
Get-ParameterAlias -CommandName Get-Process
Name Aliases ParameterType
---- ------- -------------
ComputerName {Cn} System.String[]
Debug {db} System.Management.Automation.SwitchParameter
ErrorAction {ea} System.Management.Automation.ActionPreference
ErrorVariable {ev} System.String
FileVersionInfo {FV, FVI} System.Management.Automation.SwitchParameter
Id {PID} System.Int32[]
IncludeUserName {} System.Management.Automation.SwitchParameter
InformationAction {infa} System.Management.Automation.ActionPreference
InformationVariable {iv} System.String
InputObject {} System.Diagnostics.Process[]
Module {} System.Management.Automation.SwitchParameter
Name {ProcessName} System.String[]
OutBuffer {ob} System.Int32
OutVariable {ov} System.String
PipelineVariable {pv} System.String
Verbose {vb} System.Management.Automation.SwitchParameter
WarningAction {wa} System.Management.Automation.ActionPreference
WarningVariable {wv} System.String
Maybe you noticed a difference in detail level when you retrieve parameters via Parameters versus ParameterSets. Retrieving a parameter via Parameters is easy because this property is a hashtable, and you can use the parameter name as key:
# find information about parameter Name in Get-Process # use property "Parameters": (Get-Command -Name Get-Process).Parameters['Name']
Name : Name ParameterType : System.String[] ParameterSets : {[Name, System.Management.Automation.ParameterSetMetadata], [NameWithUserName, System.Management.Automation.ParameterSetMetadata]} IsDynamic : False Aliases : {ProcessName} Attributes : {Name, NameWithUserName, System.Management.Automation.AliasAttribute, System.Management.Automation.ValidateNotNullOrEmptyAttribute} SwitchParameter : False
Retrieving a parameter via ParameterSets is more complex because the property returns an array of Parameter Sets which in turn returns an array of actual parameters, and you need
Where-Object
to pick the parameter you are after:# use property "ParameterSets": (Get-Command -Name Get-Process).ParameterSets | Select-Object -ExpandProperty Parameters | Where-Object {$_.Name -eq 'Name'}
The result is much more detailed, though, and includes properties such as IsMandatory, Position, and many more that were missing above.
Name : Name ParameterType : System.String[] IsMandatory : False IsDynamic : False Position : 0 ValueFromPipeline : False ValueFromPipelineByPropertyName : True ValueFromRemainingArguments : False HelpMessage : Aliases : {ProcessName} Attributes : {Name, NameWithUserName, System.Management.Automation.AliasAttribute, System.Management.Automation.ValidateNotNullOrEmptyAttribute} Name : Name ParameterType : System.String[] IsMandatory : False IsDynamic : False Position : 0 ValueFromPipeline : False ValueFromPipelineByPropertyName : True ValueFromRemainingArguments : False HelpMessage : Aliases : {ProcessName} Attributes : {Name, NameWithUserName, System.Management.Automation.AliasAttribute, System.Management.Automation.ValidateNotNullOrEmptyAttribute}
You may also get back one or more duplicates, and this coincidentally explains the difference in detail level:
Parameters can be part of one or more Parameter Sets (which explains the duplicates), and the missing information are defined by the Parameter Set (which explains why they weren’t included in the first example). Since a parameter can belong to more than one Parameter Set, it can for example be mandatory and optional at the same time, depending on the used Parameter Set.
This may sound a tiny bit confusing at first but becomes clear when you look at this example.
Defining Parameter Sets
Now let’s create your own Parameter Sets and start grouping parameters. Any parameter decorated with the value ParameterSetName is placed into a group. Use whatever string you like to name a group. Any parameter not assigned to a group is available in all groups.
Here is a simple example of mutually exclusive parameters:
function Test-This
{
param
(
[Parameter(ParameterSetName='by Name')]
$Name,
[Parameter(ParameterSetName='by Id')]
$Id
)
$chosenSet = $PSCmdlet.ParameterSetName
if ($chosenSet -eq 'by Name')
{
"You submitted the name $Name."
}
elseif ($chosenSet -eq 'by Id')
{
"You submitted a number: $Id"
}
}
Test-This -Name Tobias
Test-This -Id 12
The automatic variable $PSCmdlet returns the name of the Parameter Set that was used so your code can appropriately respond to the user input. The syntax shows that both parameters are mutually exclusive:
PS> Test-This -?
NAME
Test-This
SYNTAX
Test-This [-Name <string>] [<CommonParameters>]
Test-This [-Id <int>] [<CommonParameters>]
If you use them both, PowerShell throws an exception:
PS> Test-This -Name Tobias -Id 12
Test-This : Parameter set cannot be resolved using the specified named parameters.
Resolving Ambiguity
Once you omit the parameter name, PowerShell may be unable to resolve the parameter sets with the information you submitted, and an exception is thrown:
PS> Test-This Tobias
Test-This : Parameter set cannot be resolved using the specified named parameters.
Resolve by Position and Type
To resolve issues, add more meta data, and explicitly tell PowerShell the parameter position and parameter type:
function Test-This
{
param
(
# Position 0, Type String
[Parameter(ParameterSetName='by Name',Position=0)]
[string]
$Name,
# Also Position 0, Type Integer
[Parameter(ParameterSetName='by Id',Position=0)]
[int]
$Id
)
$chosenSet = $PSCmdlet.ParameterSetName
if ($chosenSet -eq 'by Name')
{
"You submitted the name $Name."
}
elseif ($chosenSet -eq 'by Id')
{
"You submitted a number: $Id"
}
}
Test-This Tobias
Test-This 12
Now PowerShell can bind arguments entirely positional, based on the type submitted by the user:
PS> Test-This Tobias
You submitted the name Tobias.
PS> Test-This 12
You submitted a number: 12
Resolve by Default Parameter Set
There may still be room for ambiguity, for example, when the user calls your function without any arguments.
PS> Test-This
Test-This : Parameter set cannot be resolved using the specified named parameters.
You can either avoid such scenarios by making parameters mandatory (see above). Or you can define a default parameter set name using the attribute [CmdletBinding()]. PowerShell then uses this parameter set whenever there is ambiguity:
function Test-This
{
# define a default parameter set name in case of
# ambiguity:
[CmdletBinding(DefaultParameterSetName='by Name')]
param
(
[Parameter(ParameterSetName='by Name',Position=0)]
[string]
$Name,
[Parameter(ParameterSetName='by Id',Position=0)]
[int]
$Id
)
$chosenSet = $PSCmdlet.ParameterSetName
if ($chosenSet -eq 'by Name')
{
"You submitted the name $Name."
}
elseif ($chosenSet -eq 'by Id')
{
"You submitted a number: $Id"
}
}
Test-This
Mandatory and Optional at the Same Time
Parameter Sets are extremely powerful because thanks to them, you can assign multiple attributes of type [Parameter()] to each parameter, and based on context make a parameter mandatory or optional.
Let’s take a look at a command with this syntax:
Connect-Server [[-ComputerName] <string>]
Connect-Server [-ComputerName] <string> [-Credential] <PSCredential>
It can be used in a number of ways:
# local
Connect-Server
# remote as current user
Connect-Server -ComputerName abc
# remote as different user
Connect-Server -ComputerName abc -Credential user1
However, it cannot be used to supply credentials with a local connection:
# local as different user will NOT work
# when -Credential is specified, -ComputerName automatically becomes mandatory
# and PowerShell will now prompt for the mandatory computer name:
Connect-Server -Credential user1
Clever design of Parameter Sets can take care of a lot of complex validation. Here is the implementation of above command:
function Connect-Server
{
param
(
[Parameter(ParameterSetName='currentUser', Position=0, Mandatory=$false)]
[Parameter(ParameterSetName='differentUser', Position=0, Mandatory=$true)]
[string]
$ComputerName,
[Parameter(ParameterSetName='differentUser', Position=1, Mandatory=$true)]
[pscredential]
$Credential
)
$chosenParameterSet = $PSCmdlet.ParameterSetName
"Parameter Set: $chosenParameterSet"
switch($chosenParameterSet)
{
'currentUser' { 'User has chosen currentUser' }
'differentUser' { 'User has chosen differentUser' }
}
}
Note how the parameter -ComputerName uses two attributes of type [Parameter()]:
[Parameter(ParameterSetName='currentUser', Position=0, Mandatory=$false)]
[Parameter(ParameterSetName='differentUser', Position=0, Mandatory=$true)]
[string]
$ComputerName
PowerShell figures out which attribute to use based on the parameter set: when the user specifies -Credential, this selects the parameter set “different user” and turns -ComputerName into a mandatory parameter. When the user omits -Credential, this selects the parameter set “currentUser”, turning -ComputerName in an optional parameter.
Put differently, the user cannot use credentials on local connections, and PowerShell does all the heavy lifting for you.
Creating complex parameter sets can be tricky. You may want to look at the Syntax-to-Function converter built into ISESteroids. It takes any (even multi-line) syntax and automatically generates the PowerShell code and parameter sets for you.
Hiding Parameters
Let’s finish this article by looking at a lesser-known attribute value: DontShow. It hides parameters from Intellisense and tab completion. You can still use hidden parameters, but you need to know their names now.
As a side-effect, when you use this attribute on any parameter, all Common Parameters are also hidden.
Should you be wondering why this attribute behaves the way it does, you need to understand why it was introduced: [DontShow()] was introduced in PowerShell 5 as part of class support in an effort to re-use the existing IntelliSense and tab completion with the new PowerShell classes.
Since PowerShell classes support hidden properties, there was the need to hide selected items from IntelliSense. And since class methods behave very similar to PowerShell functions but do not support Common Parameters, there was the need to hide all Common Parameters as well.
Turning Off Common Parameters
The most popular use case for this attribute is to visibly turn Advanced Functions into Simple Functions and focus on only the important parameters. This can increase usability for end users.
When PowerShell started, there were only Simple Functions: in IntelliSense, only the parameters showed that were actually defined:
function Test-SimpleFunction
{
param
(
[string]
$Name
)
}
When you run this code and then call Test-SimpleFunction
and tab-complete its parameters, you’ll see only the parameter -Name. Nice and clean.
In PowerShell 2, the team added attributes, and whenever a function uses at least one attribute, it is turned into an Advanced Function. This enables a whole bunch of extra functionality, including automatic support for Common Parameters. So when you declare the parameter -Name mandatory in above function, it turns into an Advanced Function, and the user now sees a long list of parameters. That may be confusing for some.
function Test-AdvancedFunction
{
param
(
[Parameter(Mandatory)]
[string]
$Name
)
}
To use Advanced Functions that only show the parameters you define, add a dummy parameter and hide it:
function Test-AdvancedFunction
{
param
(
[Parameter(Mandatory)]
[string]
$Name,
[Parameter(DontShow)]
[Switch]
$Dummy
)
}
From a user perspective, Test-AdvancedFunction
is now just as simple and clean as Test-SimpleFunction
.
What’s Next
This part took a deep look at the Parameter attribute. In the next part, I’ll look at validation attributes.
So please stay tuned if you are hungry for more! And make sure you have PowerShell Conference EU 2020 on your radar. That’s the place to be in June if you enjoy stuff like this. See you there!