| applyTo | **/*.ps1,**/*.psm1,**/*.psd1 |
|---|---|
| description | PowerShell coding standards and best practices |
Style rules for PowerShell code based on Microsoft guidelines and community standards.
- Always start functions with
[CmdletBinding()]attribute - Always include explicit
param()block - Use
process {}block when accepting pipeline input - For system-modifying cmdlets, use
[CmdletBinding(SupportsShouldProcess)] - Document output types with
[OutputType([TypeName])]attribute - Include comment-based help for all functions
function Get-Data {
[CmdletBinding()]
[OutputType([hashtable])]
param(
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string]
$Name
)
# Implementation
}
# Function with pipeline input
function Get-PipelineInput {
[CmdletBinding()]
[OutputType([PSCustomObject])]
param(
[Parameter(ValueFromPipeline)]
[ValidateNotNull()]
[string]
$InputData
)
process {
# Process each pipeline item
}
}Prefer type accelerators over full .NET type names:
[string],[int],[bool],[array],[hashtable][PSCustomObject],[PSCredential],[datetime],[regex]
# Good - type accelerators in parameter declarations
function Get-Setting {
[CmdletBinding()]
[OutputType([PSCustomObject])]
param(
[Parameter(Mandatory)]
[hashtable]
$Configuration
)
}
# Avoid - full .NET type names
function Get-Setting {
[CmdletBinding()]
[OutputType([System.Management.Automation.PSCustomObject])]
param(
[Parameter(Mandatory)]
[System.Collections.Hashtable]
$Configuration
)
}- Use approved PowerShell verbs only (verify with
Get-Verb) - Use singular nouns for function names (
Get-ItemnotGet-Items) - Use PascalCase for function names and parameters
- Use camelCase for local variables (
$userName,$itemCount) - Use descriptive variable names that indicate purpose
- Use full cmdlet names, never aliases (
Get-Processnotgps)
# Good - descriptive variable names
$backupFiles = Get-ChildItem -Path $backupPath -Filter '*.bak'
$activeUsers = Get-ADUser -Filter { Enabled -eq $true }
# Bad - generic variable names
$files = Get-ChildItem -Path $backupPath -Filter '*.bak'
$users = Get-ADUser -Filter { Enabled -eq $true }- Use full parameter names in scripts and functions
- Always use quotes around string parameter values
- Include validation on every parameter
- Place each component on its own line
function Get-UserData {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string]
$UserName,
[Parameter()]
[ValidateRange(1, 100)]
[int]
$MaxResults = 10,
[Parameter(ValueFromPipeline)]
[ValidateNotNull()]
[string[]]
$ComputerName
)
}- Opening brace
{at end of line, closing brace}on new line - Use 4 spaces per indentation level
- Maximum line length: 115 characters
- Use splatting for long parameter lists
- Two blank lines before function definitions
- One blank line at end of file
function Test-Code {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[ValidateRange(1, 100)]
[int]
$Value
)
if ($Value -gt 10) {
Write-Output 'Greater'
}
elseif ($Value -eq 10) {
Write-Output 'Equal'
}
else {
Write-Output 'Lesser'
}
}
# Good - splatting for readability
$parameters = @{
Uri = 'https://api.example.com/endpoint'
Method = 'Post'
Headers = $headers
Body = $body
}
Invoke-RestMethod @parameters- Use
$PSScriptRootfor script-relative paths - Use
$Env:UserProfileor$HOMEinstead of~ - Use
Join-Pathto construct paths
# Good
$configPath = Join-Path -Path $PSScriptRoot -ChildPath 'config.json'
$userPath = Join-Path -Path $Env:UserProfile -ChildPath 'Documents'
# Bad
$configPath = '.\config.json'
$userPath = '~\Documents'- Use
-ErrorAction 'Stop'for cmdlets within try/catch - Immediately copy
$_in catch blocks before other commands
try {
Get-Item -Path $path -ErrorAction Stop
}
catch {
$errorRecord = $_ # Capture immediately
Write-Error "Failed: $($errorRecord.Exception.Message)"
}- Use
[PSCredential]for credential parameters, never[string]for passwords - Make credentials optional when the function can run without them
- Use
[System.Management.Automation.Credential()]attribute for flexibility
function Connect-Service {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]
$Server,
[Parameter()]
[ValidateNotNull()]
[System.Management.Automation.PSCredential]
[System.Management.Automation.Credential()]
$Credential = [System.Management.Automation.PSCredential]::Empty
)
# Check if credentials were provided
if ($Credential -eq [System.Management.Automation.PSCredential]::Empty) {
# Use current user context
}
else {
# Use provided credentials
}
}- Write objects to pipeline immediately, don't batch into arrays
- Use
Write-Verbosefor detailed operation information - Use
Write-Warningfor potential issues
# Good - immediate output
foreach ($item in $collection) {
$result = Process-Item $item
$result # Output immediately
}
# Bad - batching
$results = @()
foreach ($item in $collection) {
$results += Process-Item $item
}
$resultsAll functions must include comment-based help:
function Get-UserData {
<#
.SYNOPSIS
Brief one-line description.
.DESCRIPTION
Detailed description of behavior.
.PARAMETER UserName
Description of the parameter.
.EXAMPLE
Get-UserData -UserName 'jsmith'
Retrieves data for user jsmith.
.OUTPUTS
System.Management.Automation.PSCustomObject
#>
[CmdletBinding()]
[OutputType([PSCustomObject])]
param(
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string]
$UserName
)
# Implementation
}- Use single quotes for string literals
- Use double quotes only when variable expansion is needed
- Quote hashtable keys only when necessary (hyphens, spaces)
# Good
$headers = @{
Authorization = "Bearer $token" # Needs expansion
'User-Agent' = 'PowerShell' # Key has hyphen
}
$branchName = "feature/issue-$issueNumber"
$title = 'Static string'- Spaces around all operators:
$x = 1 + 2 - Spaces around comparison operators:
$value -eq 10 - Space after commas and semicolons
- No trailing spaces
- Do not use semicolons as line terminators
- Place each hashtable element on its own line
# Good
$options = @{
Name = 'Value'
Size = 100
}
# Bad
$options = @{ Name = 'Value'; Size = 100 }