description | ms.custom | ms.date | ms.reviewer | title |
---|---|---|---|---|
Learn how to create reusable PowerShell functions, implement best practices, and avoid common pitfalls in function design, error handling, and parameter validation. |
Contributor-mikefrobbins |
01/22/2025 |
mirobb |
Functions |
PowerShell one-liners and scripts that have to be modified often are good candidates to turn into reusable functions.
Write functions whenever possible because they're more tool-oriented. You can add the functions to a
script module, put that module in a location defined in the $env:PSModulePath
, and call the
functions without needing to locate where you saved the functions. Using the PowerShellGet
module, it's easy to share your PowerShell modules in a NuGet repository. PowerShellGet ships
with PowerShell version 5.0 and higher. It's also available as a separate download for PowerShell
version 3.0 and higher.
Don't overcomplicate things. Keep it simple and use the most straightforward way to accomplish a task. Avoid aliases and positional parameters in any code that you reuse. Format your code for readability. Don't hardcode values; use parameters and variables. Don't write unnecessary code even if it doesn't hurt anything. It adds unnecessary complexity. Attention to detail goes a long way when writing any PowerShell code.
When naming your functions in PowerShell, use a Pascal case name with an approved
verb and a singular noun. To obtain a list of approved verbs in PowerShell, run Get-Verb
. The
following example sorts the results of Get-Verb
by the Verb property.
Get-Verb | Sort-Object -Property Verb
The Group property gives you an idea of how the verbs are meant to be used.
Verb Group
---- -----
Add Common
Approve Lifecycle
Assert Lifecycle
Backup Data
Block Security
Checkpoint Data
Clear Common
Close Common
Compare Data
Complete Lifecycle
Compress Data
Confirm Lifecycle
Connect Communications
Convert Data
ConvertFrom Data
ConvertTo Data
Copy Common
Debug Diagnostic
Deny Lifecycle
Disable Lifecycle
Disconnect Communications
Dismount Data
Edit Data
Enable Lifecycle
Enter Common
Exit Common
Expand Data
Export Data
Find Common
Format Common
Get Common
Grant Security
Group Data
Hide Common
Import Data
Initialize Data
Install Lifecycle
Invoke Lifecycle
Join Common
Limit Data
Lock Common
Measure Diagnostic
Merge Data
Mount Data
Move Common
New Common
Open Common
Optimize Common
Out Data
Ping Diagnostic
Pop Common
Protect Security
Publish Data
Push Common
Read Communications
Receive Communications
Redo Common
Register Lifecycle
Remove Common
Rename Common
Repair Diagnostic
Request Lifecycle
Reset Common
Resize Common
Resolve Diagnostic
Restart Lifecycle
Restore Data
Resume Lifecycle
Revoke Security
Save Data
Search Common
Select Common
Send Communications
Set Common
Show Common
Skip Common
Split Common
Start Lifecycle
Step Common
Stop Lifecycle
Submit Lifecycle
Suspend Lifecycle
Switch Common
Sync Data
Test Diagnostic
Trace Diagnostic
Unblock Security
Undo Common
Uninstall Lifecycle
Unlock Common
Unprotect Security
Unpublish Data
Unregister Lifecycle
Update Data
Use Other
Wait Lifecycle
Watch Common
Write Communications
It's important to use an approved verb for your PowerShell functions. Modules that contain functions with unapproved verbs generate a warning message when they're imported into a PowerShell session. That warning message makes your functions look unprofessional. Unapproved verbs also limit the discoverability of your functions.
A function in PowerShell is declared with the function keyword followed by the function name and
then an opening and closing curly brace ({ }
). The code executed by the function is contained
within those curly braces.
function Get-Version {
$PSVersionTable.PSVersion
}
The function shown in the following example is a simple example that returns the version of PowerShell.
Get-Version
Major Minor Build Revision
----- ----- ----- --------
5 1 14393 693
When you use a generic name for your functions, such as Get-Version
, it could cause naming
conflicts. Default commands added in the future or commands that others might write could conflict
with them. Prefix the noun portion of your function names to help prevent naming conflicts. For
example: <ApprovedVerb>-<Prefix><SingularNoun>
.
The following example uses the prefix PS
.
function Get-PSVersion {
$PSVersionTable.PSVersion
}
Other than the name, this function is identical to the previous one.
Get-PSVersion
Major Minor Build Revision
----- ----- ----- --------
5 1 14393 693
You can still have a name conflict even when you add a prefix to the noun. I like to prefix my function nouns with my initials. Develop a standard and stick to it.
function Get-MrPSVersion {
$PSVersionTable.PSVersion
}
This function is no different than the previous two, except for using a more unique name to try to prevent naming conflicts with other PowerShell commands.
Get-MrPSVersion
Major Minor Build Revision
----- ----- ----- --------
5 1 14393 693
Once loaded into memory, you can see functions on the Function PSDrive.
Get-ChildItem -Path Function:\Get-*Version
CommandType Name Version
----------- ---- -------
Function Get-Version
Function Get-PSVersion
Function Get-MrPSVersion
If you want to remove these functions from your current session, remove them from the Function PSDrive or close and reopen PowerShell.
Get-ChildItem -Path Function:\Get-*Version | Remove-Item
Verify that the functions were indeed removed.
Get-ChildItem -Path Function:\Get-*Version
If the functions were loaded as part of a module, you can unload the module to remove them.
Remove-Module -Name <ModuleName>
The Remove-Module
cmdlet removes PowerShell modules from memory in your current PowerShell
session. It doesn't remove them from your system or disk.
Don't statically assign values. Use parameters and variables instead. When naming your parameters, use the same name as the default cmdlets for your parameter names whenever possible.
In the following function, notice that I used ComputerName and not Computer, ServerName, or Host for the parameter name. Using ComputerName standardizes the parameter name to match the parameter name and case like the default cmdlets.
function Test-MrParameter {
param (
$ComputerName
)
Write-Output $ComputerName
}
The following function queries all commands on your system and returns the number with specific parameter names.
function Get-MrParameterCount {
param (
[string[]]$ParameterName
)
foreach ($Parameter in $ParameterName) {
$Results = Get-Command -ParameterName $Parameter -ErrorAction SilentlyContinue
[pscustomobject]@{
ParameterName = $Parameter
NumberOfCmdlets = $Results.Count
}
}
}
As you can see in the following results, 39 commands that have a ComputerName parameter. There aren't any commands with parameters such as Computer, ServerName, Host, or Machine.
Get-MrParameterCount -ParameterName ComputerName, Computer, ServerName,
Host, Machine
ParameterName NumberOfCmdlets
------------- ---------------
ComputerName 39
Computer 0
ServerName 0
Host 0
Machine 0
Use the same case for your parameter names as the default cmdlets. For example, use ComputerName
,
not computername
. This naming scheme helps people familiar with PowerShell discover your functions
and look and feel like the default cmdlets.
The param
statement allows you to define one or more parameters. A comma (,
) separates the
parameter definitions. For more information, see
about_Functions_Advanced_Parameters.
Turning a function into an advanced function in PowerShell is simple. One of the differences between a function and an advanced function is that advanced functions have common parameters that are automatically added. Common parameters include parameters such as Verbose and Debug.
Start with the Test-MrParameter
function that was used in the previous section.
function Test-MrParameter {
param (
$ComputerName
)
Write-Output $ComputerName
}
There are a couple of different ways to see the common parameters. One is by viewing the syntax with
Get-Command
.
Get-Command -Name Test-MrParameter -Syntax
Notice the Test-MrParameter
function doesn't have any common parameters.
Test-MrParameter [[-ComputerName] <Object>]
Another is to drill down into the parameters property of Get-Command
.
(Get-Command -Name Test-MrParameter).Parameters.Keys
ComputerName
Add the CmdletBinding
attribute to turn the function into an advanced function.
function Test-MrCmdletBinding {
[CmdletBinding()] # Turns a regular function into an advanced function
param (
$ComputerName
)
Write-Output $ComputerName
}
When you specify CmdletBinding
, the common parameters are added automatically. CmdletBinding
requires a param
block, but the param
block can be empty.
Get-Command -Name Test-MrCmdletBinding -Syntax
Test-MrCmdletBinding [[-ComputerName] <Object>] [<CommonParameters>]
Drilling down into the parameters property of Get-Command
shows the actual parameter names,
including the common ones.
(Get-Command -Name Test-MrCmdletBinding).Parameters.Keys
ComputerName
Verbose
Debug
ErrorAction
WarningAction
InformationAction
ErrorVariable
WarningVariable
InformationVariable
OutVariable
OutBuffer
PipelineVariable
The SupportsShouldProcess
attribute adds the WhatIf and Confirm risk mitigation
parameters. These parameters are only needed for commands that make changes.
function Test-MrSupportsShouldProcess {
[CmdletBinding(SupportsShouldProcess)]
param (
$ComputerName
)
Write-Output $ComputerName
}
Notice that there are now WhatIf and Confirm parameters.
Get-Command -Name Test-MrSupportsShouldProcess -Syntax
Test-MrSupportsShouldProcess [[-ComputerName] <Object>] [-WhatIf] [-Confirm]
[<CommonParameters>]
Once again, you can also use Get-Command
to return a list of the actual parameter names, including
the common, ones along with WhatIf and Confirm.
(Get-Command -Name Test-MrSupportsShouldProcess).Parameters.Keys
ComputerName
Verbose
Debug
ErrorAction
WarningAction
InformationAction
ErrorVariable
WarningVariable
InformationVariable
OutVariable
OutBuffer
PipelineVariable
WhatIf
Confirm
Validate input early on. Don't allow your code to continue on a path when it can't complete without valid input.
Always specify a datatype for the variables used for parameters. In the following example, String is specified as the datatype for the ComputerName parameter. This validation limits it to only allow a single computer name to be specified for the ComputerName parameter.
function Test-MrParameterValidation {
[CmdletBinding()]
param (
[string]$ComputerName
)
Write-Output $ComputerName
}
An error is generated if more than one computer name is specified.
Test-MrParameterValidation -ComputerName Server01, Server02
Test-MrParameterValidation : Cannot process argument transformation on
parameter 'ComputerName'. Cannot convert value to type System.String.
At line:1 char:42
+ Test-MrParameterValidation -ComputerName Server01, Server02
+ ~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidData: (:) [Test-MrParameterValidation]
, ParameterBindingArgumentTransformationException
+ FullyQualifiedErrorId : ParameterArgumentTransformationError,Test-MrP
arameterValidation
The problem with the current definition is that it's valid to omit the value of the ComputerName
parameter, but a value is required for the function to complete successfully. This scenario is where
the Mandatory
parameter attribute is beneficial.
The syntax used in the following example is compatible with PowerShell version 3.0 and higher.
[Parameter(Mandatory=$true)]
could be specified to make the function compatible with PowerShell
version 2.0 or higher.
function Test-MrParameterValidation {
[CmdletBinding()]
param (
[Parameter(Mandatory)]
[string]$ComputerName
)
Write-Output $ComputerName
}
Now that the ComputerName is required, if one isn't specified, the function prompts for one.
Test-MrParameterValidation
cmdlet Test-MrParameterValidation at command pipeline position 1
Supply values for the following parameters:
ComputerName:
If you want to allow more than one value for the ComputerName parameter, use the String
datatype but add square brackets ([]
) to the datatype to allow an array of strings.
function Test-MrParameterValidation {
[CmdletBinding()]
param (
[Parameter(Mandatory)]
[string[]]$ComputerName
)
Write-Output $ComputerName
}
Maybe you want to specify a default value for the ComputerName parameter if one isn't specified.
The problem is that default values can't be used with mandatory parameters. Instead, use the
ValidateNotNullOrEmpty
parameter validation attribute with a default value.
Even when setting a default value, try not to use static values. In the following example,
$env:COMPUTERNAME
is used as the default value, which is automatically translated to the local
computer name if a value isn't provided.
function Test-MrParameterValidation {
[CmdletBinding()]
param (
[ValidateNotNullOrEmpty()]
[string[]]$ComputerName = $env:COMPUTERNAME
)
Write-Output $ComputerName
}
Inline comments are useful if you're writing complex code, but users don't see them unless they look at the code.
The function in the following example has an inline comment in the foreach
loop. While this
particular comment might not be difficult to locate, imagine if the function contained hundreds of
lines of code.
function Test-MrVerboseOutput {
[CmdletBinding()]
param (
[ValidateNotNullOrEmpty()]
[string[]]$ComputerName = $env:COMPUTERNAME
)
foreach ($Computer in $ComputerName) {
#Attempting to perform an action on $Computer <<-- Don't use
#inline comments like this, use write verbose instead.
Write-Output $Computer
}
}
A better option is to use Write-Verbose
instead of inline comments.
function Test-MrVerboseOutput {
[CmdletBinding()]
param (
[ValidateNotNullOrEmpty()]
[string[]]$ComputerName = $env:COMPUTERNAME
)
foreach ($Computer in $ComputerName) {
Write-Verbose -Message "Attempting to perform an action on $Computer"
Write-Output $Computer
}
}
The verbose output isn't displayed when the function is called without the Verbose parameter.
Test-MrVerboseOutput -ComputerName Server01, Server02
The verbose output is displayed when the function is called with the Verbose parameter.
Test-MrVerboseOutput -ComputerName Server01, Server02 -Verbose
Extra code is necessary when you want your function to accept pipeline input. As mentioned earlier in this book, commands can accept pipeline input by value (by type) or by property name. You can write your functions like the native commands so they accept either one or both of these input types.
To accept pipeline input by value, specify the ValueFromPipeline
parameter attribute for that
particular parameter. You can only accept pipeline input by value from one parameter of each
datatype. If you have two parameters that accept string input, only one of them can accept pipeline
input by value. If you specified by value for both of the string parameters, the input
wouldn't know which parameter to bind to. This scenario is another reason I call this type of
pipeline input by type instead of by value.
Pipeline input is received one item at a time, similar to how items are handled in a foreach
loop.
A process
block is required to process each item if your function accepts an array as input. If
your function only accepts a single value as input, a process
block isn't necessary but is
recommended for consistency.
function Test-MrPipelineInput {
[CmdletBinding()]
param (
[Parameter(Mandatory,
ValueFromPipeline)]
[string[]]$ComputerName
)
process {
Write-Output $ComputerName
}
}
Accepting pipeline input by property name is similar, except you specify it with the
ValueFromPipelineByPropertyName
parameter attribute, and it can be specified for any number of
parameters regardless of datatype. The key is the output of the command being piped in must have a
property name that matches the name of the parameter or a parameter alias of your function.
function Test-MrPipelineInput {
[CmdletBinding()]
param (
[Parameter(Mandatory,
ValueFromPipelineByPropertyName)]
[string[]]$ComputerName
)
process {
Write-Output $ComputerName
}
}
begin
and end
blocks are optional. begin
is specified before the process
block and is used
to perform any initial work before the items are received from the pipeline. Values that are piped
in aren't accessible in the begin
block. The end
block is specified after the process
block
and is used for cleanup after all items piped in are processed.
The function shown in the following example generates an unhandled exception when a computer can't be contacted.
function Test-MrErrorHandling {
[CmdletBinding()]
param (
[Parameter(Mandatory,
ValueFromPipeline,
ValueFromPipelineByPropertyName)]
[string[]]$ComputerName
)
process {
foreach ($Computer in $ComputerName) {
Test-WSMan -ComputerName $Computer
}
}
}
There are a couple of different ways to handle errors in PowerShell. Try/Catch
is the more modern
way to handle errors.
function Test-MrErrorHandling {
[CmdletBinding()]
param (
[Parameter(Mandatory,
ValueFromPipeline,
ValueFromPipelineByPropertyName)]
[string[]]$ComputerName
)
process {
foreach ($Computer in $ComputerName) {
try {
Test-WSMan -ComputerName $Computer
}
catch {
Write-Warning -Message "Unable to connect to Computer: $Computer"
}
}
}
}
Although the function shown in the previous example uses error handling, it generates an unhandled exception because the command doesn't generate a terminating error. Only terminating errors are caught. Specify the ErrorAction parameter with Stop as its value to turn a nonterminating error into a terminating one.
function Test-MrErrorHandling {
[CmdletBinding()]
param (
[Parameter(Mandatory,
ValueFromPipeline,
ValueFromPipelineByPropertyName)]
[string[]]$ComputerName
)
process {
foreach ($Computer in $ComputerName) {
try {
Test-WSMan -ComputerName $Computer -ErrorAction Stop
}
catch {
Write-Warning -Message "Unable to connect to Computer: $Computer"
}
}
}
}
Don't modify the global $ErrorActionPreference
variable unless absolutely necessary. If you change
it in a local scope, it reverts to the previous value when you exit that scope.
If you're using something like .NET directly from within your PowerShell function, you can't specify
the ErrorAction parameter on the command itself. You can change the $ErrorActionPreference
variable just before you call the .NET method.
Adding help to your functions is considered a best practice. Help allows people you share them with to know how to use them.
function Get-MrAutoStoppedService {
<#
.SYNOPSIS
Returns a list of services that are set to start automatically, are not
currently running, excluding the services that are set to delayed start.
.DESCRIPTION
Get-MrAutoStoppedService is a function that returns a list of services
from the specified remote computer(s) that are set to start
automatically, are not currently running, and it excludes the services
that are set to start automatically with a delayed startup.
.PARAMETER ComputerName
The remote computer(s) to check the status of the services on.
.PARAMETER Credential
Specifies a user account that has permission to perform this action. The
default is the current user.
.EXAMPLE
Get-MrAutoStoppedService -ComputerName 'Server1', 'Server2'
.EXAMPLE
'Server1', 'Server2' | Get-MrAutoStoppedService
.EXAMPLE
Get-MrAutoStoppedService -ComputerName 'Server1' -Credential (Get-Credential)
.INPUTS
String
.OUTPUTS
PSCustomObject
.NOTES
Author: Mike F. Robbins
Website: https://mikefrobbins.com
Twitter: @mikefrobbins
#>
[CmdletBinding()]
param (
)
#Function Body
}
When you add comment-based help to your functions, help can be retrieved for them like the default built-in commands.
All the syntax for writing a function in PowerShell can seem overwhelming for someone getting started. If you can't remember the syntax for something, open a second instance of the PowerShell Integrated Scripting Environment (ISE) on a separate monitor and view the "Cmdlet (advanced function) - Complete" snippet while typing in the code for your functions. Snippets can be accessed in the PowerShell ISE using the Ctrl + J key combination.
In this chapter, you learned the basics of writing functions in PowerShell, including how to:
- Create advanced functions
- Use parameter validation
- Use verbose output
- Support pipeline input
- Handle errors
- Create comment-based help
- How do you obtain a list of approved verbs in PowerShell?
- How do you turn a PowerShell function into an advanced function?
- When should WhatIf and Confirm parameters be added to your PowerShell functions?
- How do you turn a nonterminating error into a terminating one?
- Why should you add comment-based help to your functions?