PowerShell Parameters: Unlock the Power of Scripting

Published:1 February 2021 - 13 min. read

Azure Cloud Labs: these FREE, on‑demand Azure Cloud Labs will get you into a real‑world environment and account, walking you through step‑by‑step how to best protect, secure, and recover Azure data.

All PowerShell commands can have one or more parameters, sometimes called arguments. If you’re not using PowerShell parameters in your PowerShell functions, you’re not writing good PowerShell code!

In this article, you’re going to learn just about every facet of creating and use PowerShell parameters or arguments!

This is a sample from my book PowerShell for SysAdmins. If you want to learn PowerShell or learn some tricks of the trade, check it out!

Why Do You Need a Parameter?

When you begin to create functions, you’ll have the option to include parameters or not and how those parameters work.

Let’s say you have a function that installs Microsoft Office. Maybe it calls the Office installer silently inside of the function. What the function does, doesn’t matter for our purposes. The basic function looks like this with the name of the function and the script block.

function Install-Office {
    ## run the silent installer here
}

In this example, you just ran Install-Office without parameters and it does it’s thing.

It didn’t matter whether the Install-Office function had parameters or not. It apparently didn’t have any mandatory parameters; else, PowerShell would not have let us run it without using a parameter.

When to Use a PowerShell Parameter

Office has lots of different versions. Perhaps you need to install Office 2013 and 2016. Currently, you have no way to specify this. You could change the function’s code every time you wanted to change the behavior.

For example, you could create two separate functions to install different versions.

function Install-Office2013 {
    Write-Host 'I installed Office 2013. Yippee!'
}

function Install-Office2016 {
    Write-Host 'I installed Office 2016. Yippee!'
}

Doing this works, but it’s not scalable. It forces you to create a separate function for every single version of Office that comes out. You’re going to have to duplicate lots of code when you don’t have to.

Instead, you need a way to pass in different values at runtime to change the function’s behavior. How do you do this?

Yep! Parameters or what some people call arguments.

Since we’d like to install different versions of Office without changing the code every time, you have to add at least one parameter to this function.

Before you quickly think up a PowerShell parameter to use, it’s essential first to ask yourself a question; “What is the smallest change or changes you expect will be needed in this function?”.

Remember that you need to rerun this function without changing any of the code inside of the function. In this example, the parameter is probably clear to you; you need to add a Version parameter. But, when you’ve got a function with tens of lines of code, the answer won’t be too apparent. As long as you answer that question as precisely as possible, it will always help.

So you know you need a Version parameter. Now what? You now can add one, but like any great programming language, there are multiple ways to skin that cat.

In this tutorial, I’ll show you the “best” way to create parameters based on my nearly decade of experience with PowerShell. However, know that this isn’t the only way to create a parameter.

There is such a thing as positional parameters. These parameters allow you to pass values to parameters without specifying the parameter name. Positional parameters work but aren’t considered “best practice.” Why? Because they are harder to read especially when you have many parameters defined on a function.

Creating a Simple PowerShell Parameter

Creating a parameter on a function requires two primary components; a param block and the parameter itself. A param block is defined by the param keyword followed by a set of parentheses.

function Install-Office {
    [CmdletBinding()]
    param()
    Write-Host 'I installed Office 2016. Yippee!'
}

At this point, the function’s actual functionality hasn’t changed a bit. We’ve just put together some plumbing, preparing us for the first parameter.

Once we’ve got the param block in place, you’ll now create the parameter. The method that I’m suggesting you create a parameter includes the Parameter block, followed by the type of parameter, it is followed by the parameter variable name below.

function Install-Office {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]$Version
    )
    Write-Host 'I installed Office 2016. Yippee!'
}

We’ve now created a function parameter in PowerShell but what exactly happened here?

The Parameter block is an optional yet recommended piece of every parameter. Like the param block, it is “function plumbing,” which prepares the parameter for adding additional functionality. The second line is where you define the type of parameter it is.

In this case, we’ve chosen to cast the Version parameter to a string. Defining an explicit type means that any value passed to this parameter will always attempt to be “converted” to a string if it’s not already.

The type isn’t mandatory but is highly encouraged. Explicitly defining the type of parameter it is will significantly reduce many unwanted situations down the road. Trust me.

Now that you’ve got the parameter defined, you can run the Install-Office command with the Version parameter passing a version string to it like 2013. The value passed to the Version parameter is sometimes referred to as parameters’ arguments or values.

PS> Install-Office -Version 2013
I installed Office 2016. Yippee!

What’s going on here anyway? You told it you wanted to install version 2013, but it’s still telling you it’s installed 2016. When you add a parameter, you must then remember to change the function’s code to a variable. When the parameter is passed to the function, that variable will be expanded to be whatever value was passed.

Change the static text of 2016 and replace it with the Version parameter variable and converting the single quotes to double quotes so the variable will expand.

function Install-Office {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]$Version
    )
    Write-Host "I installed Office $Version. Yippee!"
}

Now you can see that whatever value you pass to the Version parameter will then get passed into the function as the $Version variable.

The Mandatory Parameter Attribute

Recall that I mentioned the [Parameter()] line was just “function plumbing” and needed to prepare the function for further work? Adding parameter attributes to a parameter is the additional work I was speaking about earlier.

A parameter doesn’t have to be a placeholder for a variable. PowerShell has a concept called parameter attributes and parameter validation. Parameter attributes change the behavior of the parameter in a lot of different ways.

For example, one of the most common parameter attributes you’ll set is the Mandatory keyword. By default, you could call the Install-Office function without using the Version parameter and it would execute just fine. The Version parameter would be optional. Granted, it wouldn’t expand the $Version variable inside the function because there was no value, but it would still run.

Many times when creating a parameter, you’ll want the user always to use that parameter. You’ll be depending on the value of the parameter inside of the function’s code somewhere, and if the parameter isn’t passed, the function will fail. In these cases, you want to force the user to pass this parameter value to your function. You want that parameter to become mandatory.

Forcing users to use a parameter is simple once you’ve got the basic framework built as you have here. You have to include the keyword Mandatory inside of the parameter’s parentheses. Once you have that in place, executing the function without the parameter will halt execution until a value has been input.

function Install-Office {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$Version
    )
    Write-Host "I installed Office $Version. Yippee!"
}

PS> Install-Office
cmdlet Install-Office at command pipeline position 1
Supply values for the following parameters:
Version:

The function will wait until you specify a value for the Version parameter. Once you do so and hit Enter, PowerShell will execute the function and move on. If you provide a value for the parameter, PowerShell will not prompt you for the parameter every time.

PowerShell Parameter Validation Attributes

Making a parameter mandatory is one of the most common parameter attributes you can add, but you can also use parameter validation attributes. In programming, it’s always essential to restrict user input as tightly as possible. Limiting the information that users (or even you!) can pass to your functions or scripts will eliminate unnecessary code inside your function that has to account for all kinds of situations.

Learning by Example

For example, in the Install-Office function, I demonstrated passing the value 2013 to it because I knew it would work. I wrote the code! I’m assuming (never do this in code!) that it’s obvious that anyone who knows anything would specify the version as either 2013 or 2016. Well, what’s obvious to you may not be so obvious to other people.

If you want to get technical about versions, it’d probably be more accurate to specify the 2013 version as 15.0 and 2016 as 16.0 if Microsoft were still using the versioning schema, they have in the past. But what if, since you’re assuming they’re going to specify a version of 2013 or 2016, you’ve got the code inside of the function that looks for folders with those versions in it or something else?

Below is an example of where you may be using the $Version string in a file path. If someone passes a value that doesn’t complete a folder name of Office2013 or Office2016, it will fail or do something worse, like remove unexpected folders or change things you didn’t account for.

function Install-Office {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$Version
    )
    Get-ChildItem -Path "\\SRV1\Installers\Office$Version"
}
PS> Install-Office -Version '15.0'
Get-ChildItem : Cannot find path '\SRV1\Installers\Office15.0' because it does not exist.
At line:7 char:5

Get-ChildItem -Path "\\SRV1\Installers\Office$Version"

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

CategoryInfo          : ObjectNotFound: (\SRV1\Installers\Office15.0:String) [Get-ChildItem], ItemNotFoundExcep
tion
FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Commands.GetChildItemCommand

To limit the user to what you expect them to input, you can add some PowerShell parameter validation.

Using the ValidateSet Parameter Validation Attribute

There are various kinds of parameter validation you can use. For a full listing, run Get-Help about_Functions_Advanced_Parameters. In this example, the ValidateSet attribute would probably be the best.

The ValidateSet validation attribute allows you to specify a list of values that are allowed as the parameter value. Since we’re only accounting for the string 2013 or 2016, I’d like to ensure that the user can only specify these values. Otherwise, the function will fail immediately, notifying them of why.

You can add parameter validation attributes right under the original Parameter keyword. In this example, inside the parameter attribute’s parentheses, you have an array of items; 2013 and 2016. A parameter validation attribute tells PowerShell that the only values that are valid for Version are 2013 or 2016. If you try to pass something besides what’s in the set, you’ll receive an error notifying you that you only have a specific number of options available.

function Install-Office {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [ValidateSet('2013','2016')]
        [string]$Version
    )
    Get-ChildItem -Path "\\SRV1\Installers\Office$Version"

}
PS> Install-Office -Version 15.0
Install-Office : Cannot validate argument on parameter 'Version'. The argument "15.0" does not belong to the set
"2013,2016" specified by the ValidateSet attribute. Supply an argument that is in the set and then try the command
again.
At line:1 char:25

Install-Office -Version 15.0

                    ~~~~

CategoryInfo          : InvalidData: (:) [Install-Office], ParameterBindingValidationException
FullyQualifiedErrorId : ParameterArgumentValidationError,Install-Office

The ValidateSet attribute is a common validation attribute to use. For a complete breakdown of all of the ways parameter values can be restricted, check out the Functions_Advanced_Parameters help topic by running Get-Help about_Functions_Advanced_Parameters.

Parameter Sets

Let’s say you only want certain PowerShell parameters used with other parameters. Perhaps you’ve added a Path parameter to the Install-Office function. This path will install whatever version the installer is. In this case, you don’t want the user using the Version parameter.

You need parameter sets.

Parameters can be grouped into sets that can only be used with other parameters in the same set. Using the function below, you can now use both the Version parameter and the Path parameter to come up with the path to the installer.

function Install-Office {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [ValidateSet('2013','2016')]
        [string]$Version,
        
        [Parameter(Mandatory)]
        [string]$Path
    )
    
    if ($Version) {
        Get-ChildItem -Path "\\SRV1\Installers\Office$Version"
    } elseif ($Path) {
        Get-ChildItem -Path $Path
    }
}

This poses a problem, though, because the user could use both parameters. Besides, since both parameters are mandatory, they’re going to be forced to use both when that’s not what you want. To fix this, we can place each parameter in a parameter set like below.

function Install-Office {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory,ParameterSetName = 'ByVersion')]
        [ValidateSet('2013','2016')]
        [string]$Version,
        
        [Parameter(Mandatory, ParameterSetName = 'ByPath')]
        [string]$Path
    )
    
    if ($Version) {
        Get-ChildItem -Path "\\SRV1\Installers\Office$Version"
    } elseif ($Path) {
        Get-ChildItem -Path $Path
    }
}

By defining a parameter set name on each parameter, this allows you to control groups of parameters together.

The Default Parameter Set

What if the user tries to run Install-Office with no parameters? This isn’t accounted for, and you’ll see a friendly error message.

No parameter set
No parameter set

To fix this, you’ll need to define a default parameter set within the CmdletBinding() area. This tells the function to pick a parameter set to use if no parameters were used explicitly changing [CmdletBinding()] to [CmdletBinding(DefaultParameterSetName = 'ByVersion')]

Now, whenever you run Install-Office, it will prompt for the Version parameter since it will be using that parameter set.

Pipeline Input

In the examples so far, you’ve been creating functions with a PowerShell parameter that can only be passed using the typical -ParameterName Value syntax. But, as you’ve learned already, PowerShell has an intuitive pipeline that allows you to seamlessly pass objects from one command to another without using the “typical” syntax.

When you use the pipeline, you “chain” commands together with the pipe symbol | allowing a user to send the output of a command like Get-Service to Start-Service as a shortcut for passing the Name parameter to Start-Service.

The “Old” Way Using a Loop

In the custom function you’re working with; you’re installing Office and have a Version parameter. Let’s say you’ve got a list of computer names in a CSV file in one row with the version of Office that needs to be installed on them in the second row. The CSV file looks something like this:

ComputerName,Version
PC1,2016
PC2,2013
PC3,2016

You’d like to install the version of Office that’s next to each computer on that computer.

First, you’ll have to add a ComputerName parameter onto the function to pass a different computer name in for each iteration of the function. Below I’ve created some pseudocode that represents some code that may be in the fictional function and added an Write-Host instance to see how the variables expand inside the function.

function Install-Office {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [ValidateSet('2013','2016')]
        [string]$Version,
    
        [Parameter()]
        [string]$ComputerName
    )

    <#
    ## Connect to the remote with some code here
    Invoke-Command -ComputerName $ComputerName -ScriptBlock {
        ## Do stuff to install the version of office on this computer
        Start-Process -FilePath 'msiexec.exe' -ArgumentList 'C:\Setup\Office{0}.msi' -f $using:Version
    }
    #>
    Write-Host "I am installing Office version [$Version] on computer [$ComputerName]"
}

Once you’ve got the ComputerName parameter added to the function; you could make this happen by reading the CSV file and passing the values for the computer name and the version to the Install-Office function.

$computers = Import-Csv -Path 'C:\ComputerOfficeVersions.csv'
foreach ($pc in $computers) {
    Install-Office -Version $_.Version -ComputerName $_.ComputerName
}

Building Pipeline Input for Parameters

This method of reading the CSV rows and using a loop to pass each row’s properties to the function is the “old” way of doing it. In this section, you want to forego the foreach loop entirely and use the pipeline instead.

As-is, the function doesn’t support the pipeline at all. It would make intuitive sense to assume that you could pass each computer name and version to the function using the pipeline. Below, we’re reading the CSV and directly passing it to Install-Office, but this doesn’t work.

PS> Import-Csv -Path 'C:\ComputerOfficeVersions.csv' | Install-Office

You can assume all you want, but that doesn’t just make it work. We’re getting prompted for the Version parameter when you know that Import-Csv is sending it as an object property. Why isn’t it working? Because you haven’t added any pipeline support yet.

There are two kinds of pipeline input in a PowerShell function; ByValue (entire object) and ByPropertyName (a single object property). Which do you think is the best way to stitch together the output of Import-Csv to the input of Install-Office?

Without any refactoring at all, you could use the ByPropertyName method since, after all, Import-Csv is already returning the properties Version and ComputerName since they’re columns in the CSV.

To add pipeline support to a custom function is much simpler than you may be thinking. It’s merely a parameter attribute represented with one of two keywords; ValueFromPipeline or ValueFromPipelineByPropertyName.

In the example, you’d like to bind the ComputerName and Version properties returned from Import-Csv to the Version and ComputerName parameters of Install-Office so you’ll be using ValueFromPipelineByPropertyName.

Since we’d like to bind both of these parameters, you’ll add this keyword to both parameters as shown below and rerun the function using the pipeline.

function Install-Office {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory,ValueFromPipelineByPropertyName)]
        [ValidateSet('2013','2016')]
        [string]$Version,
        
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [string]$ComputerName
)

    <#
    ## Connect to the remote with some code here
    Invoke-Command -ComputerName $ComputerName -ScriptBlock {
        ## Do stuff to install the version of office on this computer
        Start-Process -FilePath 'msiexec.exe' -ArgumentList 'C:\Setup\Office{0}.msi' -f $using:Version
    }
    #>
    Write-Host "I am installing Office version [$Version] on computer [$ComputerName]"
}

That’s weird. It only ran for the last row in the CSV. What’s going on? It just executed the function for the last row because you skipped over a concept that isn’t required when building functions without pipeline support.

Don’t Forget the Process Block!

When you need to create a function that involves pipeline support, you must include (at a minimum) an “embedded” block inside of your function called. process. This process block tells PowerShell that when receiving pipeline input, to run the function for every iteration. By default, it will only execute the last one.

You could technically add other blocks like begin and end as well, but scripters don’t use them near as often.

To tell PowerShell to execute this function for every object coming in, I’ll add a process block that includes the code inside of that.

function Install-Office {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory,ValueFromPipelineByPropertyName)]
        [ValidateSet('2013','2016')]
        [string]$Version,
        
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [string]$ComputerName
)

    process {
        <#
        ## Connect to the remote with some code here
        Invoke-Command -ComputerName $ComputerName -ScriptBlock {
            ## Do stuff to install the version of office on this computer
            Start-Process -FilePath 'msiexec.exe' -ArgumentList 'C:\Setup\Office{0}.msi' -f $using:Version
        }
        #>
        Write-Host "I am installing Office version [$Version] on computer [$ComputerName]"
    }
}

Now you can see that each object’s Version and ComputerName properties returned from Import-Csv was passed to Install-Office and bound to the Version and ComputerName parameters.

Resources

To dive deeper into how function parameters work, check out my blog post on PowerShell functions.

I also encourage you to check my Pluralsight course entitled Building Advanced PowerShell Functions and Modules for an in-depth breakdown of everything there is to know about PowerShell functions, function parameters, and PowerShell modules.

Hate ads? Want to support the writer? Get many of our tutorials packaged as an ATA Guidebook.

Explore ATA Guidebooks

Looks like you're offline!