PowerShell Best Practices & Style Guide
Based on the PowerShell Practice and Style Guide.
Style Guide
Capitalization
- PascalCase for all public identifiers: function names, parameters, variables, module names
- lowercase for language keywords (
foreach,if,param) - lowercase for operators (
-eq,-match,-gt) - UPPERCASE for comment-based help keywords (
.SYNOPSIS,.DESCRIPTION) - Two-letter acronyms: both caps (
$PSBoundParameters,Get-PSDrive)
Naming Conventions
- Functions:
Verb-Nounformat with approved verbs (Get-,Set-,New-,Remove-) - Parameters: PascalCase, descriptive names
- Variables: PascalCase for public, optionally camelCase for private
Brace Style (One True Brace Style)
# Opening brace on same line, closing brace on own line
function Get-Something {
[CmdletBinding()]
param (
[string]$Name
)
if ($Name) {
"Hello, $Name"
} else {
"Hello, World"
}
}
# Exception: short scriptblocks on one line
Get-ChildItem | Where-Object { $_.Length -gt 10mb }
Function Structure
Always start with [CmdletBinding()] and use proper block order:
function Verb-Noun {
[CmdletBinding()]
param (
[Parameter(Mandatory)]
[string]$RequiredParam,
[string]$OptionalParam = "default"
)
begin {
# Initialization
}
process {
# Pipeline processing
}
end {
# Cleanup
}
}
Formatting
- Indentation: 4 spaces (not tabs)
- Line length: Max 115 characters
- Blank lines: 2 before functions, 1 between methods
- No trailing whitespace
- No semicolons as line terminators
- Spaces around operators:
$x = $y + 1not$x=$y+1 - Spaces inside braces:
$( ... )and{ ... }
Splatting for Long Commands
# Instead of long lines, use splatting
$params = @{
Path = $FilePath
Destination = $DestPath
Force = $true
Recurse = $true
}
Copy-Item @params
Error Handling
Use -ErrorAction Stop
try {
Get-Item -Path $Path -ErrorAction Stop
Remove-Item -Path $Path -ErrorAction Stop
} catch {
Write-Error "Failed: $_"
}
Set $ErrorActionPreference for Non-Cmdlets
$ErrorActionPreference = 'Stop'
try {
# External command or .NET call
[System.IO.File]::ReadAllText($Path)
} catch {
Write-Error "Failed: $_"
} finally {
$ErrorActionPreference = 'Continue'
}
Avoid Anti-Patterns
# BAD: Using flags
$success = $false
try { Do-Thing; $success = $true } catch { }
if ($success) { Do-Next }
# GOOD: Keep transaction together
try {
Do-Thing -ErrorAction Stop
Do-Next -ErrorAction Stop
} catch {
Handle-Error $_
}
# BAD: Testing $?
Do-Something
if (-not $?) { Write-Error "Failed" }
# GOOD: Use try/catch
try {
Do-Something -ErrorAction Stop
} catch {
Write-Error "Failed: $_"
}
Copy Error to Variable
catch {
$errorRecord = $_ # Copy immediately
Write-Log "Error: $($errorRecord.Exception.Message)"
Write-Log "At: $($errorRecord.InvocationInfo.PositionMessage)"
}
Security
Always Use PSCredential
function Connect-Service {
[CmdletBinding()]
param (
[Parameter(Mandatory)]
[System.Management.Automation.PSCredential]
[System.Management.Automation.Credential()]
$Credential
)
# If you must pass plaintext (avoid if possible)
$plainPassword = $Credential.GetNetworkCredential().Password
}
Secure String Handling
# Prompt securely
$secureString = Read-Host -Prompt "Enter secret" -AsSecureString
# Save encrypted to disk (user/machine specific)
ConvertFrom-SecureString -SecureString $secureString |
Out-File -Path "$env:APPDATA\secret.bin"
# Load from disk
$secureString = Get-Content -Path "$env:APPDATA\secret.bin" |
ConvertTo-SecureString
# Save full credential
Get-Credential | Export-Clixml -Path "$env:APPDATA\cred.xml"
$cred = Import-Clixml -Path "$env:APPDATA\cred.xml"
Cross-Platform Considerations
Path Handling
# Use Join-Path instead of string concatenation
$fullPath = Join-Path -Path $BaseDir -ChildPath "subdir" -AdditionalChildPath "file.txt"
# Use [System.IO.Path] for cross-platform
$tempFile = [System.IO.Path]::GetTempFileName()
$separator = [System.IO.Path]::DirectorySeparatorChar
Environment Variables
# Cross-platform temp directory
$tempDir = [System.IO.Path]::GetTempPath()
# User home directory
$homeDir = $env:HOME ?? $env:USERPROFILE
Output Best Practices
Return Objects, Not Strings
# BAD: Returning formatted strings
function Get-DiskInfo {
"Disk C: has 50GB free"
}
# GOOD: Return objects
function Get-DiskInfo {
[PSCustomObject]@{
Drive = "C:"
FreeGB = 50
TotalGB = 500
UsedPct = 90
}
}
Use Write-* Cmdlets Appropriately
Write-Verbose "Processing item $i" # -Verbose to see
Write-Debug "Variable state: $var" # -Debug to see
Write-Information "Status update" # Informational
Write-Warning "This might cause issues" # Warnings
Write-Error "Something failed" # Errors (non-terminating)
throw "Critical failure" # Terminating error
