Patterns#
A handful of patterns make PowerShell scripts safer and easier to live with. The defaults are looser than zsh / bash strict mode in a few places worth tightening; the advanced function template is the unit of re-use most operators converge on.
Strict Mode#
PowerShell’s default mode is lenient. Set-StrictMode promotes
common silent bugs (typo’d variable names, missing properties,
out-of-bounds array access) into errors.
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
Set-StrictMode -Version Latest, every check the current PowerShell knows about. Add at the top of every script and module file.$ErrorActionPreference = 'Stop', turn non-terminating errors into terminating ones sotry / catchactually catches them.
For pipeline-level control over a single call.
Get-Item missing.txt -ErrorAction Stop
Get-Item missing.txt -ErrorAction SilentlyContinue
Error Handling#
PowerShell exceptions are real .NET Exception objects.
try / catch / finally works as expected; catch
can target a specific exception type.
try {
Get-Content config.toml -ErrorAction Stop
}
catch [System.IO.FileNotFoundException] {
Write-Warning "no config; using defaults"
$defaults
}
catch {
Write-Error "unexpected: $_"
throw # rethrow
}
finally {
'cleanup runs either way'
}
$_ (or $PSItem) inside catch is the
ErrorRecord; $_.Exception is the underlying exception.
Cleanup#
The finally block runs whether the body succeeded or threw,
similar to trap in bash. PowerShell also has its own
trap keyword for shell-style “run this on any error” handling.
$tmp = New-TemporaryFile
try {
# work that uses $tmp
}
finally {
Remove-Item $tmp -Force -ErrorAction SilentlyContinue
}
The same pattern with trap (closer to bash style).
trap { Write-Warning "trapped: $_"; continue }
# rest of the script
Advanced Function Template#
The unit of re-use. [CmdletBinding()] plus typed parameters
plus [Parameter()] and [Validate*()] attributes plus
begin / process / end blocks. The result behaves like
a real cmdlet: parameter binding, -Verbose, -WhatIf,
-Confirm, pipeline streaming, and tab-completion all come
for free.
function Send-Backup {
[CmdletBinding(SupportsShouldProcess = $true)]
param(
[Parameter(Mandatory, ValueFromPipeline,
ValueFromPipelineByPropertyName)]
[ValidateScript({ Test-Path $_ -PathType Container })]
[Alias('Path')]
[string]$Source,
[Parameter(Mandatory)]
[ValidatePattern('^https://')]
[string]$Endpoint,
[ValidateRange(1, 10)]
[int]$RetryCount = 3,
[switch]$Force
)
begin {
Set-StrictMode -Version Latest
Write-Verbose "starting; endpoint = $Endpoint"
}
process {
foreach ($s in $Source) {
if ($PSCmdlet.ShouldProcess($s, "back up to $Endpoint")) {
# do the work
}
}
}
end {
Write-Verbose "done"
}
}
Parameter Validation#
Push validation into attributes so the bind fails before the
function body runs. Less code, better error messages, free
Get-Help.
Attribute |
Effect |
|---|---|
|
Reject |
|
String length bounds |
|
Numeric range |
|
String must match regex |
|
Custom predicate |
|
Enum-like list of accepted values |
Logging Through Streams#
Use the right stream for the right audience. The operator
running the script sees Success and Error by default; -Verbose
turns on Verbose; -Debug turns on Debug.
Write-Output, the answer. Pipeline traffic.Write-Verbose, detail; visible only with-Verbose.Write-Warning, caution; always visible, does not stop.Write-Error, non-terminating error;$Error[0]retains it.Write-Information, structured info (7+); paired with-InformationAction.Write-Host, terminal-only; bypasses every stream. Avoid in scripts; the output is unreachable from a2>&1capture.
function Get-Thing {
[CmdletBinding()]
param([string]$Name)
Write-Verbose "looking up $Name"
if (-not $Name) {
Write-Warning "no name provided; defaulting to 'self'"
$Name = 'self'
}
Write-Output (Resolve-Thing $Name)
}
Quoting#
Single quotes are literal; double quotes expand. Quote the
literal, not the variable. Inside double quotes, $() evaluates
a subexpression; ${var} disambiguates a variable name.
'literal $HOME' # literal
"expanded $HOME" # expanded
"now is $(Get-Date)" # subexpression
"${env:PATH}" # disambiguate env var
For paths and arguments that may contain spaces, quote the whole expression.
Remove-Item "$env:TEMP\my folder"
Avoid Aliases in Scripts#
Aliases are great at the prompt; in committed scripts they slow
down readers and obscure intent. PSScriptAnalyzer flags this as
PSAvoidUsingCmdletAliases.
# at the prompt
ls | ? Length -gt 1KB | sort Length -desc
# in scripts
Get-ChildItem |
Where-Object Length -gt 1KB |
Sort-Object Length -Descending
Avoid Write-Host#
Write-Host writes straight to the terminal; the output never
hits the pipeline or any redirection target. Use
Write-Information or Write-Output (depending on whether
the message is “for the operator” or “for the next stage”).
References#
Get-Help about_CommonParameters,Get-Help about_Functions_Advanced_Parameters,Get-Help about_Try_Catch_Finally,Get-Help about_Preference_Variables.Overview for the language-level error-handling surface (
trap,try/catch).I/O and Pipelines for the pipeline and stream semantics these patterns build on.
Tools for
PSScriptAnalyzerand the rest of the toolchain.