Designing Professional Parameters

With the help of the attribute [Parameter()], you can define sophisticated PowerShell parameters that enhance usability and versatility of your functions.

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!