PowerShell
PowerShell style guidelines#
This document defines the PowerShell style guidelines for all PowerShell files in this repository. These rules follow PowerShell best practices, the One True Brace Style (OTBS), and community standards.
Brace Style (OTBS - One True Brace Style)#
- Opening braces on the same line as the statement (OTBS)
- Closing braces on their own line, aligned with the statement
- Use braces even for single-line statements in control structures
- No empty lines immediately after opening braces or before closing braces
Good:
function Get-Example {
param($Name)
if ($Name) {
Write-Output "Hello, $Name"
} else {
Write-Output "Hello, World"
}
}
foreach ($item in $items) {
Get-Item $item
}
Bad:
function Get-Example
{
# Opening brace should be on same line
}
if ($condition)
Write-Output "Missing braces"
if ($condition) { Write-Output "All on one line" }
Naming Conventions#
Functions and Cmdlets#
- Use approved PowerShell verbs (Get, Set, New, Remove, etc.)
- Follow Verb-Noun naming pattern with PascalCase
- Use singular nouns
- Be specific and descriptive
Good:
function Get-UserProfile { }
function Set-ConfigValue { }
function New-DatabaseConnection { }
function Remove-TempFile { }
Bad:
function GetUser { } # Missing hyphen
function get-user { } # Wrong case
function Do-Something { } # Non-standard verb
function Get-Users { } # Should be singular unless always plural
Parameters and Variables#
- Use PascalCase for parameters and public variables
- Use camelCase for private/local variables
- Use descriptive names, avoid abbreviations unless well-known
- Prefix boolean variables with verbs like
is,has,should - Append 'At' 'In' 'On' for timestamp/location variables
- Use
$_for pipeline variables - Avoid using reserved words as names
- Avoid using automatic variables for custom variables
- Parameter and variable names can be alphanumeric and include underscores.
- The colon character
:and.are significant in PowerShell syntax, if they are a part of text, they must be escaped (`).
Good:
$userName = "John"
$isValid = $true
$hasPermission = $false
$totalCount = 0
param(
[string]$ConfigPath,
[switch]$Force
)
Bad:
$usr = "John" # Too abbreviated
$valid = $true # Boolean should be $isValid
$TOTAL_COUNT = 0 # Wrong case style
Constants#
- Use PascalCase with descriptive names
- Mark as
[System.Management.Automation.Language.ReadOnlyAttribute]or useSet-Variable -Option ReadOnly
Good:
$MaxRetries = 3
$DefaultTimeout = 30
Set-Variable -Name ApiEndpoint -Value "https://api.example.com" -Option ReadOnly
Parameters#
- Always use
[OutputType()],[CmdletBinding()]andparam()block at the top of functions - Use parameter attributes for validation
- Provide meaningful parameter names with PascalCase
- Use type constraints for parameters.
- Have a space between the type and the parameter name
- Group mandatory parameters first
- Use
[switch]for boolean flags that default to$false. - Add help text with parameter descriptions (inside the param block)
Good:
function Get-UserData {
<#
.SYNOPSIS
Retrieves user data from the database.
.DESCRIPTION
Retrieves user data from the database.
.EXAMPLE
Get-UserData -UserId "12345" -IncludeDeleted
#>
[CmdletBinding()]
param(
# The unique identifier of the user.
[Parameter(Mandatory, Position = 0)]
[ValidateNotNullOrEmpty()]
[string] $UserId,
# Include deleted users in the results.
[Parameter()]
[switch] $IncludeDeleted
)
# Function body
}
Bad:
function Get-UserData($id, $del) {
# No param block, no types, unclear names
}
Indentation and Whitespace#
- Use 4 spaces for indentation (not tabs)
- No trailing whitespace at end of lines
- End files with a single newline character
- Use blank lines to separate logical blocks of code
- No blank lines immediately after opening braces or before closing braces
- One space after commas in arrays and parameters
- One space around operators (
=,+,-,-eq,-ne, etc.)
Good:
function Process-Data {
<#
... Docs
#>
[CmdletBinding()]
param(
[Parameter(Mandatory, Position = 0)]
[ValidateNotNullOrEmpty()]
[array] $Items
)
$results = @()
foreach ($item in $Items) {
$processed = Format-Item $item
$results += $processed
}
return $results
}
Bad:
function Process-Data{
param($Items)
$results=@()
foreach($item in $Items){
$processed=Format-Item $item
$results+=$processed
}
return $results
}
Comments#
- Use
#for single-line comments - Use
<# ... #>for multi-line comments and help documentation - Place comments on their own line above the code they describe
- Use comment-based help for functions (
.SYNOPSIS,.DESCRIPTION,.EXAMPLE) - Put comment-based help first, inside the function body, before any code. This makes it cleaner to move, and collapse code.
- Do not use '.PARAMETER' for parameters in comment-based help, use inline comments instead inside the param block above each of the parameters.
- Each section of comment-based help should have a blank line between them.
- The comment-based help must be indented to align with the function definition.
- Keep comments up to date with code changes.
- The code should say what it is doing, comments should explain why.
Good:
function New-UserAccount {
<#
.SYNOPSIS
Creates a new user account.
.DESCRIPTION
Creates a new user account with the specified username and email.
Validates the email format before creation.
.EXAMPLE
New-UserAccount -UserName 'jdoe' -Email 'jdoe@example.com'
Creates a new user account for jdoe with email jdoe@example.com.
.LINK
https://example.com/docs/New-UserAccount
#>
[CmdletBinding()]
param(
# The username for the new account.
[Parameter(Mandatory)]
[string] $UserName,
# The email address for the new account.
[Parameter(Mandatory)]
[string] $Email
)
# Validate email format before processing
if ($Email -notmatch '^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$') {
throw "Invalid email format"
}
# Create the user account
New-Object PSObject -Property @{
UserName = $UserName
Email = $Email
}
}
Bad:
function New-UserAccount {
param($UserName, $Email)
# Check email
if ($Email -notmatch '^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$') { throw "Invalid email format" }
New-Object PSObject -Property @{UserName = $UserName; Email = $Email} # Create user
}
String Handling#
- Use single quotes for strings that don't need variable expansion
- Use double quotes only for strings with variables or escape sequences
- Use here-strings (
@"..."@or@'...'@) for multi-line strings - Use
-foperator or string interpolation for formatting - Avoid string concatenation with
+in loops (use arrays or StringBuilder)
Good:
$name = 'John'
$greeting = "Hello, $name"
$path = 'C:\Temp\file.txt'
$message = "The value is: {0}" -f $value
$multiLine = @"
This is a
multi-line string
with variable: $name
"@
Bad:
$name = "John" # Should use single quotes (no variables)
$greeting = 'Hello, $name' # Variable won't expand
$message = "The value is: " + $value # Use formatting instead
Control Structures#
If/Else Statements#
- Always use braces, even for single statements
- Opening brace on same line (OTBS)
- Use
elseif(one word) notelse if - One space before opening brace
- Comparison operators on separate lines for readability in complex conditions
Good:
if ($condition) {
Do-Something
} elseif ($otherCondition) {
Do-SomethingElse
} else {
Do-Default
}
# Complex condition
if ($user.IsActive -and
$user.HasPermission -and
$user.Age -gt 18) {
Grant-Access
}
Bad:
if ($condition)
{ # Brace should be on same line
Do-Something
}
if ($condition) Do-Something # Missing braces
if($condition){ # Missing spaces
Do-Something
}
Loops#
- Use appropriate loop construct (foreach, for, while, do-while)
- Always use braces
- Opening brace on same line (OTBS)
- Prefer
foreachfor collections overforwhen index not needed
Good:
foreach ($item in $collection) {
Process-Item $item
}
for ($i = 0; $i -lt $count; $i++) {
Process-Index $i
}
while ($condition) {
Update-Condition
}
Bad:
foreach ($item in $collection)
{ # Brace should be on same line
Process-Item $item
}
foreach ($item in $collection) Process-Item $item # Missing braces
Switch Statements#
- Opening brace on same line
- Indent case statements by 4 spaces
- Use
breakorcontinueexplicitly when needed - Use
defaultfor fallback cases
Good:
switch ($value) {
'Option1' {
Do-FirstThing
}
'Option2' {
Do-SecondThing
}
default {
Do-DefaultThing
}
}
Error Handling#
- Use try/catch/finally blocks for error handling
- Be specific with catch blocks (catch specific exception types)
- Always provide meaningful error messages
- Use
throwfor unrecoverable errors - Use
Write-Errorfor non-terminating errors - Set
$ErrorActionPreferenceappropriately
Good:
function Get-FileContent {
param([string]$Path)
try {
if (-not (Test-Path $Path)) {
throw "File not found: $Path"
}
$content = Get-Content -Path $Path -ErrorAction Stop
return $content
} catch [System.IO.IOException] {
Write-Error "IO error reading file: $_"
throw
} catch {
Write-Error "Unexpected error: $_"
throw
} finally {
# Cleanup code here
}
}
Bad:
function Get-FileContent {
param([string]$Path)
try {
Get-Content -Path $Path
} catch {
# Swallowing errors silently
}
}
Output and Logging#
- Use
Write-Outputfor function return values (or implicit return) - Avoid
Write-HostuseWrite-InformationorWrite-Outputinstead. - Use
Write-Verbosefor detailed operation information - Use
Write-Debugfor debugging information - Use
Write-Warningfor warnings - Use
Write-Errorfor errors - Use
Write-Informationfor informational messages (PS 5.0+)
Good:
function Get-ProcessedData {
[CmdletBinding()]
param($Data)
Write-Verbose "Processing $($Data.Count) items"
$result = Process-Data $Data
if ($result.Warnings) {
Write-Warning "Processing completed with warnings"
}
Write-Output $result
}
Bad:
function Get-ProcessedData {
param($Data)
Write-Host "Processing data..." # Should use Write-Verbose
$result = Process-Data $Data
Write-Host $result # Should use Write-Output
}
Suppressing Output#
- Use
$null =to suppress unwanted output from commands - Avoid using
| Out-Nullas it is significantly slower - Use
[void]for method calls that return values you want to discard - This is especially important in loops or performance-critical code
Good:
# Suppress output from .NET method calls
$null = $list.Add($item)
$null = $collection.Remove($item)
# Alternative for methods
[void]$list.Add($item)
# Suppress output from cmdlets
$null = New-Item -Path $path -ItemType Directory
Bad:
# Slower performance with Out-Null
$list.Add($item) | Out-Null
$collection.Remove($item) | Out-Null
New-Item -Path $path -ItemType Directory | Out-Null
Pipeline#
- Design functions to accept pipeline input when appropriate
- Use
[Parameter(ValueFromPipeline = $true)]for pipeline parameters - Implement
processblock for pipeline-aware functions - Use
beginandendblocks when initialization or cleanup needed
Good:
function Update-Item {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[PSCustomObject]$Item
)
begin {
$count = 0
}
process {
# Process each item from pipeline
$Item.LastModified = Get-Date
$count++
Write-Output $Item
}
end {
Write-Verbose "Processed $count items"
}
}
# Usage
$items | Update-Item
Arrays and Hashtables#
- Use
@()for empty arrays - Use
@{}for empty hashtables - Use consistent formatting for multi-line collections
- One item per line for readability in multi-line collections
- Trailing comma optional but allowed on last item
- Align key-value pairs in hashtables for readability
- Use splatting for functions with many parameters
Good:
$emptyArray = @()
$numbers = @(1, 2, 3, 4, 5)
$multiLineArray = @(
'First item'
'Second item'
'Third item'
)
$hashtable = @{
Name = 'John'
Age = 30
City = 'Seattle'
}
$complexHash = @{
Server = @{
Name = 'WebServer01'
Port = 8080
}
Database = @{
Name = 'MainDB'
Port = 5432
}
}
Bad:
$array = 1, 2, 3, 4, 5 # Use @() syntax
$hashtable = @{Name = 'John'; Age = 30; City = 'Seattle'} # Multi-line for readability
Splatting#
- Use splatting for functions with many parameters
- Create hashtable with parameters before splatting
- Use
@symbol for splatting (not$)
Good:
$params = @{
Path = 'C:\Temp'
Filter = '*.txt'
Recurse = $true
ErrorAction = 'Stop'
}
Get-ChildItem @params
Bad:
Get-ChildItem -Path 'C:\Temp' -Filter '*.txt' -Recurse $true -ErrorAction 'Stop'
Comparison Operators#
- Use PowerShell comparison operators (
-eq,-ne,-gt,-lt,-ge,-le) - Don't use C-style operators (
==,!=,>,<) - Use
-likefor wildcard matching,-matchfor regular expression - Use
-containsfor collection membership, not-eq - Add
-iprefix for case-insensitive (default) or-cfor case-sensitive - Caution with $null comparisons. Comparison order is important depending if the variable is a single item or a collection.
$null -eq $varis usually safer for collections as it won't error if $var is $null or empty.
Good:
if ($value -eq 10) { }
if ($name -like 'John*') { }
if ($email -match '^[\w-\.]+@') { }
if ($list -contains $item) { }
if ($name -ceq 'JOHN') { } # Case-sensitive
if ($null -eq $collection) { } # Safe null check for collections
Bad:
if ($value == 10) { } # Wrong operator
if ($list -eq $item) { } # Use -contains for collections
if ($collection -eq $null) { } # Can error if $collection is $null or empty
Script Structure#
- Use
#Requiresstatements at the top for version/module requirements - Place param block after
#Requiresand comment-based help - Group related functions together
- Separate sections with comments
- End script with single newline
Good:
#Requires -Version 7.4
<#
.SYNOPSIS
Script for managing user accounts.
.DESCRIPTION
This script provides functions to create, update, and delete user accounts.
#>
[CmdletBinding()]
param(
[string]$ConfigPath = ".\config.json"
)
# Script-level variables
$ErrorActionPreference = 'Stop'
#region Helper functions
function Get-ConfigData {
param([string]$Path)
# Implementation
}
function New-UserAccount {
param($UserName, $Email)
# Implementation
}
#endregion
# Script execution
try {
$config = Get-ConfigData -Path $ConfigPath
# Main logic
} catch {
Write-Error "Script failed: $_"
exit 1
}
Performance Considerations#
- Avoid
Write-Hostin production scripts, instead useWrite-Information - Use
ArrayListcollection instead of@()arrays in loops. - Use
-Filterparameter instead of piping toWhere-Objectwhen available - Avoid unnecessary pipeline operations
- Use
.ForEach()and.Where()methods for better performance on large collections
Good:
# Efficient array building
$results = [System.Collections.Generic.List[PSObject]]::new()
foreach ($item in $collection) {
$results.Add($processedItem)
}
# Efficient filtering
Get-ChildItem -Path C:\Temp -Filter *.txt
# Method syntax for performance
$filtered = $collection.Where({ $_.Value -gt 10 })
Bad:
# Inefficient array building
$results = @()
foreach ($item in $collection) {
$results += $processedItem # Creates new array each iteration
}
# Inefficient filtering
Get-ChildItem -Path C:\Temp | Where-Object { $_.Name -like '*.txt' }
Line Length#
- Wrap lines at 100-120 characters
- Use backtick (
`) for line continuation (sparingly), prefer splatting - Prefer breaking at natural points (after commas, operators, pipes)
- Align continued lines for readability
Good:
$result = Get-Something -Parameter1 $value1 `
-Parameter2 $value2 `
-Parameter3 $value3
# Better: Use splatting
$params = @{
Parameter1 = $value1
Parameter2 = $value2
Parameter3 = $value3
}
$result = Get-Something @params
Testing#
- Write Pester tests for all functions
- Name test files
*.Tests.ps1 - Group tests with
DescribeandContextblocks - Use
Itblocks for individual test cases - Use
Shouldassertions - Use
BeforeAllandAfterAllfor setup/teardown
Good:
Describe 'Get-UserAccount' {
Context 'When user exists' {
It 'Should return user object' {
$result = Get-UserAccount -UserId '123'
$result | Should -Not -BeNullOrEmpty
$result.UserId | Should -Be '123'
}
}
Context 'When user does not exist' {
It 'Should throw error' {
{ Get-UserAccount -UserId '999' } | Should -Throw
}
}
}
Security Best Practices#
- Never hardcode credentials or secrets
- Use
SecureStringfor sensitive data - Use
Get-Credentialfor credential prompts - Validate all user input
- Use
-WhatIfand-Confirmfor destructive operations - Avoid
Invoke-Expressionwith user input
Good:
function Remove-UserData {
[CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')]
param(
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string]$UserId
)
if ($PSCmdlet.ShouldProcess($UserId, "Remove user data")) {
# Perform deletion
}
}
# Get credentials securely
$cred = Get-Credential -Message "Enter admin credentials"
Bad:
$password = "MyPassword123" # Hardcoded password
Invoke-Expression $userInput # Security risk