All PowerShell functions can have one or more parameters. If you're not using parameters in your PowerShell functions, you're not writing PowerShell right at all. Learn the ins, outs and in between in this long blog post demystifying the PowerShell parameter.

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

Introduction

When you begin to create your own 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 with all of the goodies to soon be inside.

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 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!'
}
Changing a function's code to change its behavior

Doing this work but it's not scalable and forces you to create a separate function for every single version of PowerShell 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 behavior of the function. How do you do this?

Yep! Parameters.

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

Before you just quickly think up a 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 that performs various tasks, 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. Because this isn't meant to be a comprehensive PowerShell tutorial, I'll be showing you the "best" way to create parameters based on my nearly decade of experience with PowerShell. However, this isn't the only way to create a parameter.

Before I catch flack, I do want to note that there is such a thing as positional parameters that allow you to pass values to parameters without specifying the parameter name. These work but aren't best practice simply because they are harder to read especially when you have many parameters defined on a function. In this post, we will only be talking about named parameters.

Creating a Simple Parameter

Creating a parameter on a function requires two primary components; a param block and the parameter itself. You can just create a param block without a parameter, and I'd suggest just to include that in your muscle memory anyway since it doesn't hurt anything. 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!'
}
An example of a param block

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!'
}
An example of a param block

We've now created a function parameter 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. By defining this type means that any value that's 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 C:\> Install-Office -Version 2013
I installed Office 2016. Yippee!
Passing a Parameter to the Function

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. The reason is that 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!"
}
Modifying function code account for a parameter

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 that it was needed to prepare the function for further work? Adding parameter attributes to a parameter is that additional work I was speaking about earlier.

A parameter doesn't have just to be a placeholder for a variable. PowerShell has a concept called parameter attributes and parameter validation that allows you to 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 an optional parameter. Granted, it wouldn't expand the $Version variable inside of 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 just 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:
Using a mandatory parameter

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'd instead not be prompted for the parameter every time, you'll typically just pass the value as a parameter to the function as you call it using the familiar -ParameterName syntax (Install-Office -Version 2013).

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 as well. 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 of your function that having to account for all kinds of situations.

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, right? 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
Assuming Parameter Values

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

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 of 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
Using the ValidateSet parameter validation attribute

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

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

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 parameter that can only be passed by 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 the other 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.

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 so that you can 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 a Write-Host instance to allow us to see how the variables expand inside of 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]"
}
Adding the ComputerName parameter

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
}

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 C:\> Import-Csv -Path 'C:\ComputerOfficeVersions.csv' | Install-Office
No function pipeline input defined

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]"
}
Adding Pipeline Support

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.

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]"
    }
}
Adding the process block

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 and if you'd like to learn more about creating PowerShell helper functions, I have a great blog post on that too.

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.