Powershell: Parameters for Scripts and (Advanced) Functions

Some notes on parameters for scripts or (advanced) functions in Powershell.

Parameter block for a script or an advanced function

At the beginning of the script, add CmdletBinding() and a Param() block.

Note that in most cases, this block must be the very first line of code in a script (comments and blank lines don’t matter), so don’t try to declare variables or define functions before it.

[CmdletBinding()]    # Turns it to an 'advanced function/script'.
Param
(
    [Parameter(Mandatory=$True)]  [string] $StringParameter,
    [Parameter(Mandatory=$False)] [bool]   $BooleanParameter = $False,
    [Parameter(Mandatory=$False)] $IntegerParameter = 10,
    
    [Parameter(Mandatory=$False)]
    [switch]
    $SwitchParameter
)

As usual, typing the variable (like [string] or [bool]) is not required, but can prevent some errors or weird behavior due type-casting issues.

Also: [Parameter(Mandatory=$True)] can be shortened to [Parameter(Mandatory)]:

Common Parameters

By speifying CmdletBinding(), a script or function automatically supports common parameters, which are these:

But note that this still needs additional support in the implementation to work right:
There are a few cmdlets like Write-Verbose or Write-Debug that do that already, but to do something yourself, is a tiny bit tricky:

Detect the Common Parameter Flags

To handle such a common parameter (like -Debug or Verbose) in the script itself1:

if ($PSCmdlet.MyInvocation.BoundParameters["Debug"]) { <# ... #> }
else                                                 { <# ... #> }

… or use this:

$VerboseMode = $null # ... or $false…

$PSBoundParameters.TryGetValue("Verbose", [ref] $VerboseMode) | out-null

if ($VerboseMode) { "Verbose mode is $VerboseMode" }
else              { "Verbose mode is $VerboseMode" }

By using TryGetValue(), an unlikely (but valid) variant like -Verbose:$false will also be detected:

> script.ps1
Verbose mode is          # Nothing, since the variable was set to $null by default in the example above.

> script.ps1 -Verbose
Verbose mode is True

> script.ps1 -Verbose:$false
Verbose mode is False

> script.ps1 -Verbose:$true
Verbose mode is True

Pass a common parameter to another script/function

Start-Something -Debug:($PSCmdlet.MyInvocation.BoundParameters["Debug"] -eq $true)

Simple Function

For a normal function, parameters can also be provided simpler (and they are not required at all):

No parameters at all:

function Func
{
    # ...
}

Parameters in the function head

function Func ($Param1, [int] $Param2)
{
    # ...
}

Or parameters in the parameter block in the function body:

function Func
{
    param
    (
        $Param1,
        $Param2
    )
}

Validate Parameter Input

See also Parameter and variable validation attributes.

Example: Is the argument an existing file? ($_ is the shortcut for the argument, i.e. the input value).

Param
(
    [ValidateScript({ Test-Path -Path $_ -PathType Leaf })]
    [string] $InputFile
)
Examples Description
[ValidateScript({Test-Path -Path $_ -PathType Leaf})] The expression must be valid (return $true): The file must exist.
[ValidateScript({Test-Path -Path $_})] [System.IO.FileInfo] $x Must be a valid path; parameter will be cast to a FileInfo object.
[ValidateRange(0,10)] The parameter must be between zero and ten.
[ValidateRange("Positive")] [int] $x (since PS 6.1.0) The parameter value must be positive (= greater than zero);
All enums: “Positive”, “Negative”, “NonPositive”, “NonNegative”
Also: One needs to specify a fitting type (e.g. [int]).
[ValidateCount(2,2)] The parameter takes exactly two argument values (minimum, maximum).
[ValidateLength(1,10)] The parameter value must have one (minimum) to ten (maximum) characters.
[ValidatePattern("^[0-3][0-3]$")] Compares the parameter to a regular expression.
[ValidateSet("A", "B", "C")] Only one of the specified values will be accepted. Interesting fact:
"validation occurs whenever that variable is assigned, even within the script"
[ValidateNotNull()] The parameter value may not be null.
[ValidateNotNullOrEmpty()] The parameter value may not be null or empty.
[ValidateDrive("C", "D"] The parameter value must be a PSDrive.
[AllowNull()] Accept $null (handy for a mandatory parameter).
[AllowEmptyString()] Accept "" (handy for a mandatory parameter).
[AllowEmptyCollection()] Accept @() (handy for a mandatory parameter).

List the values of a ValidateSet parameter

If one needs the values of a ValidateSet-limited parameter later in the code:

Param ( [ValidateSet("A", "B", "C")] $ParameterX )
$VS = ((Get-Variable 'ParameterX').Attributes | select ValidValues).ValidValues

The (… | select ValidValues).ValidValues may look weird, but is necessary: Other ways didn’t work (exception) :-/
(Note: A shorter notation would require a lower or unset strict mode)

Accept multiple values (of a ValidateSet) for a parameter

function Start-Foo
{
    Param
    (
        [ValidateSet('A', 'B', 'C')]            $x,
        [ValidateSet('D', 'E', 'F')] [string[]] $y,
        [ValidateSet('G', 'H', 'I')] [string]   $z
    )
    
    # ...
}

Start-Foo -x A, B -y D, E -z G, H 

Parameter Sets

Useful for mutually exclusive arguments (more at Simon Wahlin: PowerShell functions and Parameter Sets).

Example

Parameter Param1 is always available for the combinations of sets that are specified in its [Parameter()] attributes(!), but either Enable_A or Enable_B can additionally and optionally be chosen:

With [CmdletBinding(DefaultParameterSetName = "...")] a default paramter will be set, in cases where it can’t be determined otherwise.

function Set-Something
{
    [CmdletBinding(DefaultParameterSetName = "Default")]
    param
    (
        [Parameter(ParameterSetName="Default", Mandatory=$true)]
        [Parameter(ParameterSetName="Set_A")]
        [Parameter(ParameterSetName="Set_B")]
        [string] $Param1,
        
        [Parameter(ParameterSetName="Default")]
        [Parameter(ParameterSetName="Set_A")]
        [int] $Param2,

        [Parameter(ParameterSetName="Set_A", Mandatory=$true)]
        [switch] $Enable_A,
        
        [Parameter(ParameterSetName="Set_A")]
        [switch] $Enable_A_optional,

        [Parameter(ParameterSetName="Set_B", Mandatory=$true)]
        [switch] $Enable_B,
        
        [Parameter(ParameterSetName="Set_B")]
        [switch] $Enable_B_optional
    )

    # ...
}

Set-Something -Param1 "foo"
Set-Something -Param1 "foo" -Enable_A
Set-Something -Param1 "foo" -Enable_B
Set-Something -Param1 "foo" -Enable_A -Enable_B    # Will not autocomplete and will throw an error if you enforce it manually!
Set-Something -Param1 "foo" -Param2 100 -Enable_B  # Error, since Param2 doesn't list 'Set_B' in its [Parameter()] attribute!

Detect and handle a specific parameter set

Via $PSCmdlet.ParameterSetName one can determine which set of parameters is currently effective:

if ($PSCmdlet.ParameterSetName -eq 'NameOfTheSet')
{
    # ...
}

Another option is to use a switch-case statement:

switch ($PSCmdlet.ParameterSetName)
{
    'Set1'
    {
        write-host "Using parameter set no. 1."
        break
    }

    'Set2'
    {
        write-host "Using parameter set no. 2."
        break
    }
}

‘Gotcha’ regarding mandatory parameters

To make parameters mandatory only for a specific set, the right way is to add Mandatory=$true to the Parameter attribute:

[Parameter(ParameterSetName="Set_A", Mandatory=$true)] $A_man
[Parameter(ParameterSetName="Set_A")]                  $A_opt,
[Parameter(ParameterSetName="Set_B", Mandatory=$true)] $B_man,
[Parameter(ParameterSetName="Set_B")]                  $B_opt

I had to struggle with it in the beginning, because I did it the wrong way and split the attribute settings.
That way, even when using Set_A parameters, I was asked for a mandatory parameter of Set_B.
Because by using it the as shown below, the parameters will always be mandatory, regardless of the currently active parameter set!

[Parameter(ParameterSetName="Set_A")]
[Parameter(Mandatory=$true)]          # !
$A,

[Parameter(ParameterSetName="Set_B")]
[Parameter(Mandatory=$true)]          # !
$B

Positional Parameters

Normally, I prefer to type the full cmdlet/function names (thanks to autocomplete) and to used named parameters, instead of using aliases and positional parameters. But that’s also caused by the fact that I write (more complex) scripts/functions for others, which must be understable even months and years later.

On the other hand: Sometimes, when one must execute the same task often as a quick one-liner on the CLI, a short version may be preferable:
Compare Update-FoobarThingy -ParameterX arg1 -ParameterY arg2
with uf arg1 arg2

Default behaviour

By default, all parameters are available by position and the default order is the order the parameters are defined:

Param
(
    $ParameterX, # Position 1
    $ParameterY  # Position 2
)

Write-Host "This is ParameterX: $ParameterX"
Write-Host "This is ParameterY: $ParameterY"

Explicit order

But one can also explicity define a different order:

Param
(
    [Parameter(Mandatory, Position=1)] $ParameterX, # Now at position 2!
    [Parameter(           Position=0)] $ParameterY, # Not at position 1! (Index starts at 0)
)

Write-Host "This is ParameterX: $ParameterX"
Write-Host "This is ParameterY: $ParameterY"

But which can become irritating and troublesome if one isnt’t aware or doesn’t pay attention!

Disallow positional parameters (but still allow some)

One can also define that positional parameters should generally be disallowed, but still allow it for some (ideally only a selected few) parameters.
The remaining arguments must then be provided by named parameters.

Interestingly (and contradicting the official documentation), this is the default behaviour anyways, so PositionalBinding=$False is not even strictly necessary:

[CmdletBinding(PositionalBinding = $False)]     #   <--
Param
(
    [Parameter(Position=0)] $ParameterX
    $ParameterY,
)

Write-Host "This is ParameterX: $ParameterX"
Write-Host "This is ParameterY: $ParameterY"
> .\test.ps1 x y
test.ps1 : A positional parameter cannot be found that accepts argument 'y'.

> .\test.ps1 x -ParameterY y
This is ParameterX: x
This is ParameterY: y

This could also be (mis)used in “interesting” ways:

Param
(
    [Parameter(Position=0)] $ParameterX,
    $ParameterY,
    [Parameter(Position=2)] $ParameterZ
)

Write-Host "This is ParameterX: $ParameterX"
Write-Host "This is ParameterY: $ParameterY"
Write-Host "This is ParameterZ: $ParameterZ"
> .\test.ps1 x
This is ParameterX: x
This is ParameterY:
This is ParameterZ:

> .\test.ps1 x y z
test.ps1: A positional parameter cannot be found that accepts argument 'z'.

> .\test.ps1 x -ParameterY y z
This is ParameterX: x
This is ParameterY: y
This is ParameterZ: z

So, in short: Nice, but take care when using it!

Based on:


Pass multiple values to a single parameter

By the way: Originally I used $input as the name for the parameter, but I soon discovered that it is a predefined Powershell variable in functions and script blocks; so, better use a different name.

function Func
{
    [CmdletBinding()]
    param
    (
        [string[]] $InputStr = @("String One", "String Two"')
    )

    foreach ($item in $InputStr) { Write-Output $item }
}

Func -InputStr "foo", "bar"

Some notes:


Alias

Another neat feature, that I was only dimly aware of as yet, are parameter aliases (a very helpful tutorial on that topic is The Snazzy Secret of PowerShell Parameter Aliases):

function Func
{
    [CmdletBinding()]
    param
    (
        [Alias('Data', 'Text')]
        [string[]] $InputStr,
        
        [Alias('Bar')]
        [switch] $Foo
    )

    # ...
}

The two most prominent advantages that come with parameter aliases are these:

  1. Keeping backward compatibility if a paramter name is changed: The old calls will still work.
  2. Offering different terms for different users or use cases.

That means that the call to Func -InputStr "Blah" -Foo or Func -Text "Blah" -Bar or Func -Data "Blah" -Foo do all the same.

By the way: Get-Help won’t let one get the aliases, so one must use Get-Command to get the details – which is a bit strange, because on the console, one can easily get to that data, but not from within a script…

> (get-help -name Func).Parameters.parameter | select Name, Aliases

name     aliases
----     -------
Foo      Bar
InputStr Data, Text

> (get-command -name Func).Parameters.Values | select Name, Aliases

Name                Aliases
----                -------
InputStr            {Data, Text}
Foo                 {Bar}
Verbose             {vb}
[...]

> (get-command -name Func).Parameters['InputStr']

Name            : InputStr
ParameterType   : System.String[]
ParameterSets   : {[__AllParameterSets, System.Management.Automation.ParameterSetMetadata]}
IsDynamic       : False
Aliases         : {Data, Text}
Attributes      : {, System.Management.Automation.AliasAttribute, System.Management.Automation.ArgumentTypeConverterAttribute}
SwitchParameter : False

> (get-command -name Func).Parameters['InputStr'].Aliases

Data
Text

Dynamic Parameter

Another interesting feature, which I knew about for a while, but for which I only recently had a real necessity, are dynamic (or conditional) parameters: They are cool, but also a bit tricky and cumbersome to set up.

👉 For that reason, I put it on its own page.


  1. Sometimes(!?) there is also IsPresent (e.g. ...["Debug"].IsPresent) to test against, but that value is not always available (no idea, why not); if not, then an exception will be thrown! ↩︎