Once you get accustomed to writing PowerShell scripts, you need to learn about code modularization. Modularization is just a fancy word for creating code in building blocks. In the PowerShell world, PowerShell functions are one of the best ways to do this.
Not a reader? Watch this related video tutorial!When you write a PowerShell script, you have many options on how you write the code. You could write a thousand lines of code that do hundreds of tasks, all in a single, uninterrupted block of code. That would be a disaster. Instead, you should write functions.
Functions dramatically increase the usability and readability of your code, making it much easier to work with. In this tutorial, you’ll learn to write functions, add and manage your functions’ parameters, and set up functions to accept pipeline input. But first, let’s look at a bit of terminology.
This post was extracted from my book PowerShell for Sysadmins: Workflow Automation Made Easy. If you learn something in this tutorial, be sure to check out the book to learn more about functions and a whole lot more PowerShell goodies.
Functions vs. Cmdlets
The concept of a function might sound familiar because it sounds a bit like the cmdlets you’ve probably been using already. Commands like Start-Service
and Write-Host
, for example, are similar to functions. These are named pieces of code that solve a single problem. The difference between a function and a cmdlet is how each of these constructs is made.
A cmdlet isn’t written with PowerShell. It’s written in another language, typically something like C#. The cmdlet is then compiled and made available inside PowerShell.
Functions, on the other hand, are written in PowerShell; not in another language.
You can see which commands are cmdlets and which are functions by using the Get-Command
cmdlet and its CommandType
parameter as shown below
Get-Command –CommandType Function
This command above returns all the functions currently loaded into your PowerShell session, or inside modules that are available to PowerShell.
Related: Understanding and Building PowerShell Modules
Prerequisites
If you’d like to follow along with all examples, please be sure you a version of PowerShell available. There are no specific version requirements for this tutorial. Also, be sure you have a nice code editor like Visual Studio Code to copy, paste and run some code snippets.
Building a Simple Function
Before you can use a function, you need to define it. To define a function, you use the function keyword, followed by a descriptive, user-defined name, followed by a set of curly braces. Inside the curly braces is a scriptblock that you want PowerShell to execute.
Below you can see a basic function and executing that function. This function called Install-Software
, uses Write-Host
to display a message in the console. Once it’s defined, you can use this function’s name to execute the code inside its scriptblock.
PS> function Install-Software { Write-Host 'I installed some software. Yippee!' }
PS> Install-Software
I installed some software, Yippee!
Naming with Verb-Noun
A function’s name is important. You can name your functions whatever you want, but the name should always describe what the function does. The function-naming convention in PowerShell is the Verb-Noun syntax.
You should always start a function name with a verb followed by a dash and a noun. To find the list of “approved” verbs, use the Get-Verb
cmdlet.
Changing Function Behavior
If you want to change the behavior of a function, you can simply change the code the function executes as shown below.
PS> function Install-Software { Write-Host 'You installed some software, Yay!' }
PS> Install-Software
You installed some software, Yay!
Now that you’ve changed the code inside of the function, it will display a slightly different message.
Defining an Advanced Function
You can define functions in many different places. In the above section, the tutorial assumes you just copied and pasted the code directly into the PowerShell console. But this isn’t the only way. You can also define functions in a script too.
In the previous section, you were working with a tiny function, so defining it in the console wasn’t much of a problem. Most of the time though, you’ll have much bigger functions. It’ll be easier to define those functions in a script or a module and then call that script or module in order to load the function into memory.
As you might imagine, retyping a larger function every time you want to tweak its functionality could get a little frustrating.
I suggest you now open your favorite editor and store the function in a .ps1 file as you work through the rest of the tutorial.
Adding Parameters to Functions
PowerShell functions can have any number of parameters. When you create your own functions, you’ll have the option to include parameters and decide how those parameters work. The parameters can be mandatory or optional, and they can either accept anything or be forced to accept one of a limited list of possible arguments.
Related: Everything you Ever Wanted to Know about PowerShell Parameters
For example, the fictional software you’re installing via the Install-Software
function earlier might have many versions. But currently, the Install-Software
function offers a user no way to specify which version they want to install.
If you were the only one using the function, you could change the code inside of it each time you wanted a specific version, but that would be a waste of time. This process would also be prone to potential errors, not to mention that you want others to be able to use your code.
Introducing parameters into your function allows it to have variability. Just as variables allowed you to write scripts that could handle many versions of the same situation, parameters allow you to write a single function that does one thing in many ways.
In this case, you want it to install versions of the same piece of software, and do so on many computers.
Let’s first add a parameter to the function that enables you or a user to specify the version to install.
Creating a Simple Parameter
Creating a parameter on a function requires a param
block. The param
block holds all the parameters for the function. Define a param
block with the param
keyword followed by parentheses as shown below.
function Install-Software {
[CmdletBinding()]
param()
Write-Host 'I installed software version 2. Yippee!'
}
At this point, your function’s actual functionality hasn’t changed a bit. You’ve just installed the plumbing, preparing the function for a parameter.
For now, the function’s not actually installing any software. It’s just using the
Write-Host
cmdlet to simulate the software installation so you can focus on writing the function.
Once you’ve added the param
block, you can create the parameter by putting it within the param
block’s parentheses as shown below.
function Install-Software {
[CmdletBinding()]
param(
[Parameter()]
[string] $Version
)
Write-Host "I installed software version $Version. Yippee!"
}
Inside the param
block above, you’d first define the Parameter block. Using the Parameter()
block like this will turn it into an “advanced parameter”. An empty Parameter block like the one here does nothing but is required; I’ll explain how to use it in the next section.
Let’s focus instead on the [string]
type in front of the parameter name. By putting the parameter’s type between square brackets before the parameter variable name, you cast the parameter’s value to a specific type. PowerShell will always try to convert any value that’s passed to this parameter into a string—if it isn’t one already. Above, anything passed in as $Version
will always be treated as a string.
Casting your parameter to a type isn’t mandatory, but I highly encourage it. It explicitly defines the type and will significantly reduce errors down the road.
You also add $Version
into your Write-Host
statement now. This means that when you run the Install-Software
function with the Version
parameter and pass it a version number, you should get a statement saying so as shown below.
PS> Install-Software -Version 2
I installed software version 2. Yippee!
Let’s see what you can do with this parameter now.
The Mandatory Parameter Attribute
You can use the Parameter block to control various parameter attributes, which will allow you to change the behavior of the parameter. For example, if you want to make sure anyone calling the function has to pass in a given parameter, you could define that parameter as Mandatory.
By default, parameters are optional. Let’s force the user to pass in a version by using the Mandatory keyword inside the Parameter block as shown below.
function Install-Software {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$Version
)
Write-Host "I installed software version $Version. Yippee!"
}
If you run the function above, you should get the following prompt:
![Prompting for a mandatory parameter](png/untitled-2021-01-31t113522.240.png)
Once you’ve set the Mandatory attribute, executing the function without the parameter will halt execution until the user inputs a value. The function now waits until the user specifies a value for the Version
parameter. Once entered, PowerShell executes the function.
To avoid the mandatory parameter prompt, simply pass a value to the parameter when calling the function as shown below.
Install-Software -Version 2
Default Parameter Values
If, for example, you find yourself passing the same value for a parameter over and over again, you can define a default parameter value. Default parameters are useful when you expect a certain value for a parameter most of the time.
For example, if you want to install version 2 of this software 90 percent of the time, and you’d rather not have to set the value every time you run this function, you could assign a default value of 2 to the $Version
parameter. You can see this example below.
function Install-Software {
[CmdletBinding()]
param(
[Parameter()]
[string]$Version = 2
)
Write-Host "I installed software version $Version. Yippee!"
}
Having a default parameter doesn’t prevent you from passing one in. Your passed-in value will override the default value.
Adding Parameter Validation Attributes
It’s always a good idea to limit what values you can pass into a function via parameters. The best way to do that is with parameter validation attributes.
Limiting the information that users (or even you!) can pass to your functions or scripts will eliminate unnecessary code inside your function. For example, say you pass the value 3 to your Install-Software
function, knowing that version 3 is an existing version.
Your function assumes that every user knows which versions exist, so it doesn’t account for what happens when you try to specify version 4. In that case, the function will fail to find the appropriate folder because it doesn’t exist.
Life Without Parameter Validation
Check out the example below when you use the $Version
string in a file path. If someone passes a value that doesn’t complete an existing folder name (for example, SoftwareV3 or SoftwareV4), the code will fail.
function Install-Software {
param(
[Parameter(Mandatory)]
[string]$Version
)
Get-ChildItem -Path \\SRV1\Installers\SoftwareV$Version
}
![Failing on unexpected parameter values](png/untitled-2021-01-31t113601.935-1024x169.png)
You could write error-handling code to account for this problem, or you could nip the problem in the bud by requiring that the user pass only an existing version of the software. To limit the user’s input, add parameter validation.
Adding Parameter Validation
Various kinds of parameter validation exist, but with respect to your Install-Software
function, the [ValidateSet
attribute](https://adamtheautomator.com/powershell-validateset/) works best. The ValidateSet
validation attribute enables you to specify a list of values allowed for the parameter. If you’re accounting for only the string 1
or 2
, you’d ensure that the user can specify only these values; otherwise, the function will fail immediately and notify the user of why.
Add parameter validation attributes inside the param
block, right under the original Parameter
block as shown below.
function Install-Software {
param(
[Parameter(Mandatory)]
[ValidateSet('1','2')]
[string]$Version
)
Get-ChildItem -Path \\SRV1\Installers\SoftwareV$Version
}
By adding a set of items (1
and 2
) inside of the ValidateSet
attribute’s trailing parentheses, this tells PowerShell that the only values valid for Version
are 1
or 2
. If a user tries to pass something besides what’s in the set, they will receive an error message as shown below notifying them that they have only a specific number of options available.
![Parameter validation stopping PowerShell function execution](png/untitled-2021-01-31t113639.501-1024x105.png)
The ValidateSet
attribute is a common validation attribute, but others are available. For a complete breakdown of all the ways parameter values can be restricted, check out the Microsoft docs.
Accepting Pipeline Input
So far, you’ve created a function with a parameter that can be passed only by using the typical -ParameterName <Value> syntax. This works but you also have another option of passing values to parameters using the PowerShell pipeline. Let’s add pipeline capabilities to our Install-Software function.
Related: Accepting Pipeline Input in the ATA PowerShell Parameters Article
First, add another parameter to your code that specifies the computer on which you want to install the software. Also, add that parameter to your Write-Host
reference to simulate the installation.
function Install-Software {
param(
[Parameter(Mandatory)]
[string]$Version
[ValidateSet('1','2')],
[Parameter(Mandatory)]
[string]$ComputerName
)
Write-Host "I installed software version $Version on $ComputerName. Yippee!"
}
Once you’ve added the ComputerName
parameter to the function, you could now iterate over a list of computer names and pass the values for the computer name and the version to the Install-Software
function, like below.
$computers = @("SRV1", "SRV2", "SRV3")
foreach ($pc in $computers) {
Install-Software -Version 2 -ComputerName $pc
}
Instead of doing all of that, you should learn to use the pipeline instead.
Making the Function Pipeline Compatible
Unfortunately, you can’t take advantage of the PowerShell pipeline with just a simple function built earlier. You must decide which type of pipeline input you want the function to accept and implement.
A PowerShell function uses two kinds of pipeline input: ByValue
(entire object) and ByPropertyName
(a single object property). Here, because the $computers
array contains only strings, you’ll pass those strings via ByValue
.
To add pipeline support, add a parameter attribute to the parameter you want by using one of two keywords: ValueFromPipeline
or ValueFromPipelineByPropertyName
as shown below.
function Install-Software {
param(
[Parameter(Mandatory)]
[ValidateSet('1','2')]
[string]$Version,
[Parameter(Mandatory, ValueFromPipeline)]
[string]$ComputerName
)
Write-Host "I installed software version $Version on $ComputerName. Yippee!"
}
Once you’ve got the update Instal-Software
function loaded, then call it like this:
$computers = @("SRV1", "SRV2", "SRV3")
$computers | Install-Software -Version 2
Run the script again, and you should get something like this:
I installed software version 2 on SRV3. Yippee!
Notice that Install-Software
executes for only the last string in the array. You’ll see how to fix this in the next section.
Adding a process Block
To tell PowerShell to execute this function for every object coming in, you must include a process block. Inside the process block, put the code you want to execute each time the function receives pipeline input. Add a process block to your script as shown below.
function Install-Software {
param(
[Parameter(Mandatory)]
[ValidateSet('1','2')]
[string]$Version,
[Parameter(Mandatory, ValueFromPipeline)]
[string]$ComputerName
)
process {
Write-Host "I installed software version $Version on $ComputerName. Yippee!"
}
}
Now call the function again just like you did earlier. The Install-Software
function will now return three lines (one for each object).
I installed software version 2 on SRV1. Yippee!
I installed software version 2 on SRV2. Yippee!
I installed software version 2 on SRV3. Yippee!
The process block contains the main code you want to execute. You can also use begin
and end
blocks for code that will execute at the beginning and end of the function call. You can learn more about the begin
, process
, and end
blocks via the Microsoft docs.
Next Steps
Functions allow you to compartmentalize code into discrete building blocks. They not only help you break your work into smaller, more manageable chunks but also force you to write readable and testable code.
I now challenge you to look through some old scripts and see where you can add a PowerShell function. Look for patterns in code. Build a function from those patterns. Notice where you’re copying/pasting code snippets and turn those into PowerShell functions!