Chapter 9 - Functions
If you're writing PowerShell one-liners or scripts and find yourself often having to modify them for different scenarios, there's a good chance that it's a good candidate to be turned into a function that can be reused.
Whenever possible, I prefer to write functions because they are more tool oriented. I can put the
functions in a script module, put that module in the $env:PSModulePath
, and call the functions
without needing to physically locate where they're saved. Using the PowerShellGet module, it's easy
to share those modules in a NuGet repository. PowerShellGet ships with PowerShell version 5.0 and
higher. It is available as a separate download for PowerShell version 3.0 and higher.
Don't over complicate things. Keep it simple and use the most straight forward 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.
Naming
When naming your functions in PowerShell, use a Pascal case name with an approved verb and a
singular noun. I also recommend prefixing the noun. For example:
<ApprovedVerb>-<Prefix><SingularNoun>
.
In PowerShell, there's a specific list of approved verbs that can be obtained by running Get-Verb
.
Get-Verb | Sort-Object -Property Verb
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
In the previous example, I've sorted the results by the Verb column. The Group column gives you an idea of how these verbs are used. It's important to choose an approved verb in PowerShell when functions are added to a module. The module generates a warning message at load time if you choose an unapproved verb. That warning message makes your functions look unprofessional. Unapproved verbs also limit the discoverability of your functions.
A simple function
A function in PowerShell is declared with the function keyword followed by the function name and then an open and closing curly brace. The code that the function will execute is contained within those curly braces.
function Get-Version {
$PSVersionTable.PSVersion
}
The function shown is a simple example that returns the version of PowerShell.
Get-Version
Major Minor Build Revision
----- ----- ----- --------
5 1 14393 693
There's a good chance of name conflict with functions named something like Get-Version
and default
commands in PowerShell or commands that others may write. This is why I recommend prefixing the noun
portion of your functions to help prevent naming conflicts. In the following example, I'll use 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
Even when prefixing the noun with something like PS, there's still a good chance of having a name conflict. I typically 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 other than using a more sensible 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 Source
----------- ---- ------- ------
Function Get-Version
Function Get-PSVersion
Function Get-MrPSVersion
If you want to remove these functions from your current session, you'll have to 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, the module can be unloaded to remove them.
Remove-Module -Name <ModuleName>
The Remove-Module
cmdlet removes modules from memory in your current PowerShell session, it
doesn't remove them from your system or from disk.
Parameters
Don't statically assign values! Use parameters and variables. When it comes to naming your parameters, use the same name as the default cmdlets for your parameter names whenever possible.
function Test-MrParameter {
param (
$ComputerName
)
Write-Output $ComputerName
}
Why did I use ComputerName and not Computer, ServerName, or Host for my parameter name? It's because I wanted my function standardized like the default cmdlets.
I'll create a function to query all of the commands on a system and return the number of them that have 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 results shown below, 39 commands that have a ComputerName parameter. There aren't any cmdlets that have 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
I also recommend using the same case for your parameter names as the default cmdlets. Use
ComputerName
, not computername
. This makes your functions look and feel like the default
cmdlets. People who are already familiar with PowerShell will feel right at home.
The param
statement allows you to define one or more parameters. The parameter definitions are
separated by a comma (,
). For more information, see about_Functions_Advanced_Parameters.
Advanced Functions
Turning a function in PowerShell into an advanced function is really simple. One of the differences between a function and an advanced function is that advanced functions have a number of common parameters that are added to the function automatically. These common parameters include parameters such as Verbose and Debug.
I'll start out with the Test-MrParameter
function that was used in the previous section.
function Test-MrParameter {
param (
$ComputerName
)
Write-Output $ComputerName
}
What I want you to notice is that the Test-MrParameter
function doesn't have any common
parameters. There are a couple of different ways to see the common parameters. One is by viewing the
syntax using Get-Command
.
Get-Command -Name Test-MrParameter -Syntax
Test-MrParameter [[-ComputerName] <Object>]
Another is to drill down into the parameters with Get-Command
.
(Get-Command -Name Test-MrParameter).Parameters.Keys
ComputerName
Add CmdletBinding
to turn the function into an advanced function.
function Test-MrCmdletBinding {
[CmdletBinding()] #<<-- This turns a regular function into an advanced function
param (
$ComputerName
)
Write-Output $ComputerName
}
Adding CmdletBinding
adds the common parameters 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 with 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
SupportsShouldProcess
SupportsShouldProcess
adds WhatIf and Confirm parameters. These 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
Parameter Validation
Validate input early on. Why allow your code to continue on a path when it's not possible to run without valid input?
Always type the variables that are being used for your parameters (specify a datatype).
function Test-MrParameterValidation {
[CmdletBinding()]
param (
[string]$ComputerName
)
Write-Output $ComputerName
}
In the previous example, I've specified String as the datatype for the ComputerName parameter. This causes it to allow only a single computer name to be specified. If more than one computer name is specified via a comma-separated list, an error is generated.
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], ParameterBindingArg
umentTransformationException
+ FullyQualifiedErrorId : ParameterArgumentTransformationError,Test-MrParameterValidation
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 is where the
Mandatory
parameter attribute comes in handy.
function Test-MrParameterValidation {
[CmdletBinding()]
param (
[Parameter(Mandatory)]
[string]$ComputerName
)
Write-Output $ComputerName
}
The syntax used in the previous example is PowerShell version 3.0 and higher compatible.
[Parameter(Mandatory=$true)]
could be specified instead to make the function compatible with
PowerShell version 2.0 and higher. Now that the ComputerName is required, if one isn't
specified, the function will prompt for one.
Test-MrParameterValidation
cmdlet Test-MrParameterValidation at command pipeline position 1
Supply values for the following parameters:
ComputerName:
If you want to allow for more than one value for the ComputerName parameter, use the String datatype but add open and closed square brackets to the datatype to allow for 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, you'll need to
use the ValidateNotNullOrEmpty
parameter validation attribute with a default value.
function Test-MrParameterValidation {
[CmdletBinding()]
param (
[ValidateNotNullOrEmpty()]
[string[]]$ComputerName = $env:COMPUTERNAME
)
Write-Output $ComputerName
}
Even when setting a default value, try not to use static values. In the previous example,
$env:COMPUTERNAME
is used as the default value, which is automatically translated into the local
computer name if a value is not provided.
Verbose Output
While inline comments are useful, especially if you're writing some complex code, they never get seen by users unless they look into the code itself.
The function shown in the following example has an inline comment in the foreach
loop. While this
particular comment may not be that difficult to locate, imagine if the function included hundreds of
lines of code.
function Test-MrVerboseOutput {
[CmdletBinding()]
param (
[ValidateNotNullOrEmpty()]
[string[]]$ComputerName = $env:COMPUTERNAME
)
foreach ($Computer in $ComputerName) {
#Attempting to perform some 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 some action on $Computer"
Write-Output $Computer
}
}
When the function is called without the Verbose parameter, the verbose output won't be displayed.
Test-MrVerboseOutput -ComputerName Server01, Server02
When it's called with the Verbose parameter, the verbose output will be displayed.
Test-MrVerboseOutput -ComputerName Server01, Server02 -Verbose
Pipeline Input
When you want your function to accept pipeline input, some additional coding is necessary. As mentioned earlier in this book, commands can accept pipeline input by value (by type) or by property name. You can write your functions just like the native commands so that they accept either one or both of these types of input.
To accept pipeline input by value, specified the ValueFromPipeline
parameter attribute for
that particular parameter. Keep in mind that you can only accept pipeline input by value from
one of each datatype. For example, if you have two parameters that accept string input, only one of
those can accept pipeline input by value because if you specified it for both of the string
parameters, the pipeline input wouldn't know which one to bind to. This is another reason I call
this type of pipeline input by type instead of by value.
Pipeline input comes in one item at a time similar to the way items are handled in a foreach
loop.
At a minimum, a process
block is required to process each of these items if you're accepting an
array as input. If you're only accepting a single value as input, a process
block isn't necessary,
but I still recommend specifying it 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 it's specified with the
ValueFromPipelineByPropertyName
parameter attribute and it can be specified for any number of
parameters regardless of datatype. The key is that the output of the command that's being piped in
has to 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
would be specified before the PROCESS
block and is
used to perform any initial work prior to the items being received from the pipeline. This is
important to understand. Values that are piped in are not accessible in the BEGIN
block. The END
block would be specified after the PROCESS
block and is used for cleanup once all of the items
that are piped in have been processed.
Error Handling
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 also generates an unhandled exception because the command doesn't generate a terminating error. This is also important to understand. Only terminating errors are caught. Specify the ErrorAction parameter with Stop as the value to turn a non-terminating 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're
using something like .NET directly from within your PowerShell function, you can't specify the
ErrorAction on the command itself. In that scenario, you might need to change the global
$ErrorActionPreference
variable, but if you do change it, change it back immediately after trying
the command.
Comment-Based Help
It's considered to be a best practice to add comment based help to your functions so the people you're sharing them with will 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: http://mikefrobbins.com
Twitter: @mikefrobbins
#>
[CmdletBinding()]
param (
)
#Function Body
}
When you add comment based help to your functions, help can be retrieved for them just like the default built-in commands.
All of the syntax for writing a function in PowerShell can seem overwhelming especially for someone who is just getting started. Often times if I can't remember the syntax for something, I'll open a second copy of the ISE on a separate monitor and view the "Cmdlet (advanced function) - Complete" snippet while typing in the code for my function. Snippets can be accessed in the PowerShell ISE using the Ctrl+J key combination.
Summary
In this chapter you've learned the basics of writing functions in PowerShell to include how to turn a function into an advanced function and some of the more important elements that you should consider when writing PowerShell functions such as parameter validation, verbose output, pipeline input, error handling, and comment based help.
Review
- 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 non-terminating error into a terminating one?
- Why should you add comment based help to your functions?