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()]    # With that entry, a function turns into an 'advanced function'.

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 casting issues.

Common Parameters

CmdletBinding() is part of an “advanced functions/script cmdlet”. By that, a script or function automatically supports common parameters like -Verbose, -Debug, -WhatIf, -Confirm, -ErrorAction etc.

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:

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

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

And if you want to pass it 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

Example: Is the argument an existing file? ($_ is a shortcut for the value of the input argument.)

Param
(
    [ValidateScript({ Test-Path -Path $_ -PathType Leaf })]
    [string] $InputFile
)

Note that only incoming arguments are checked, not the default values!

That means, if you set the default value here to something like $InputFile = "C:\Some\Path\To\File.txt", and that path doesn’t exist, ValidateScript will not throw an error; one needs to test it explicitly later in the script.

That’s also why it will only work correctly for mandatory parameters, which then in turn also prevent that a supplied default value will be evaluated (see also Why Doesn’t My ValidateScript() work correctly?).

Code examples Description
[ValidateScript({Test-Path -Path $_ -PathType Leaf})] The expression must be valid (return $true). In this example, the file must exist.
[ValidateRange(0,10)] The parameter must be between zero and ten.
[ValidateRange("Positive")] The parameter value must be greater than zero (enums are: “Positive”, “Negative”, “NonPositive”, “NonNegative”).
[ValidateCount(1,5)] The parameter takes one (minimum) to five (maximum) parameter values.
[ValidateLength(1,10)] The parameter value must have one (minimum) to ten (maximum) characters.
[ValidatePattern("[0-9][0-9][0-9][0-9]")] The parameter value must contain a four-digit number, and each digit must be a number zero to nine.
[ValidateSet("Low", "Medium", "High")] Only one of those three specified values will be accepted. Interesting Gotcha:
"The 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.
.. and some more…

And these are allowed for mandatory parameters:

See also https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_functions_advanced_parameters.


Parameter Set

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

Position

Sometimes, one may want to skip the name of the parameters:

test.ps1 "foo" "bar"

For these cases, one can specify a position for the parameter, so that the correct value will be assigned to the correct parameter:

The following example applies the positional setting to all parameter sets:

[Parameter(Position = 0)] [string] $Param1,
[Parameter(Position = 1)] [string] $Param2

This example defines the positions as only viable for Set_X parameters:

[Parameter(ParameterSetName = 'Set_X', Position = 0)] [string] $ParamX1,
[Parameter(ParameterSetName = 'Set_X', Position = 1)] [string] $ParamX2,

One can then also mix and match named and position-based parameter values:

> test.ps1 -Param1 "foo" -Param2 "bar"
> test.ps1 "foo" "bar"
> test.ps1 -Param1 "foo" "bar"
> test.ps1 "foo" -Param2 "bar"
> test.ps1 -Param2 "bar" "foo"

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! ↩︎