| applyTo | **/*.{ps1,psm1} |
|---|---|
| description | PowerShell style guidelines for consistency across scripts and modules. |
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.
- 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" }- 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- 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- 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- 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
}- 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
}- 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
}- 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- 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
}- 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- 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
}
}- 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
}
}- 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
}- 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 DirectoryBad:
# Slower performance with Out-Null
$list.Add($item) | Out-Null
$collection.Remove($item) | Out-Null
New-Item -Path $path -ItemType Directory | Out-Null- 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- 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- 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 @paramsBad:
Get-ChildItem -Path 'C:\Temp' -Filter '*.txt' -Recurse $true -ErrorAction 'Stop'- 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 collectionsBad:
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- 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
}- 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' }- 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- 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
}
}
}- 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