PowerShell#
How PowerShell is written across the ecosystem. PowerShell is the tool for operational automation — talking to platform APIs, orchestrating cross-platform tasks, and gluing tools together. We target PowerShell 7 LTS (the cross-platform pwsh) and lean on PowerShell's advanced-function machinery rather than plain scripts.
This standard builds on the language-agnostic baseline; where the two overlap, the baseline rules apply and the conventions here add the PowerShell specifics. PowerShell is a heavily used language, so its standard nests: the shared conventions live on this page, and each construct — functions, classes, scripts — has its own page with the doc requirements, formatting, and section structure for that construct.
| Page | Description |
|---|---|
| Functions | Advanced functions — CmdletBinding, typed and validated parameters, pipeline blocks, ShouldProcess, and required comment-based help. |
| Classes | When to reach for a PowerShell class, and how to structure its members, constructors, and documentation. |
| Scripts | Structure for standalone .ps1 scripts — requirements, parameters, help, and keeping the script thin. |
Tools and controllers#
PowerShell falls into two kinds, and the difference decides how a command shapes its output:
- A tool is a reusable unit — an advanced function, usually exported from a module. It takes input only through parameters and emits raw, least-manipulated objects, so it stays usable in situations its author never imagined; a tool that measures a size returns bytes, not a rounded string.
- A controller is a script that automates one process by calling tools. It may reshape, round, or format data for how it will be read, and it is not meant to be reused.
Keep the shaping at the edge: tools stay general and emit raw objects, and a controller — or a format view (.format.ps1xml) — turns those into presentation. This is the thin script rule seen from the other side, and it is why tools emit objects, not text.
Shared conventions#
These hold for all PowerShell, whatever the construct:
Verb-Nounnaming with an approved verb (Get-Verb) and a singular noun:Get-RepositorySecret, notFetch-Secrets.PascalCasefor functions, parameters, public variables, and class members;camelCasefor local variables.- Full cmdlet names, never aliases (
Where-Object, not?;ForEach-Object, not%). - Full parameter names, and standard ones. Pass parameters by name and avoid positional arguments in shared code —
Get-Process -Name pwsh, notGet-Process pwsh— so a call survives parameter-set changes and reads clearly. Name your own parameters after PowerShell's built-ins (Path,Name,ComputerName), not$Param_Computer. - Set
$ErrorActionPreference = 'Stop'at the top of every script and module so errors are terminating, not silently swallowed. - Emit objects, not formatted text. Return rich objects and let the caller format; reserve
Write-Hostfor genuine console UX, and useWrite-Verbose/Write-Informationfor progress narration.
Formatting#
These rules define the layout; PSScriptAnalyzer enforces them (its settings are derived from this standard, not the reverse), so author to them and let the formatter apply them:
- One True Brace Style (OTBS). Opening brace on the statement line, closing brace on its own line; always brace control blocks, even a single statement. No blank line straight after
{or before}, andelse/elseif/catch/finallysit on the line with the preceding closing brace. - Indent with four spaces, never tabs, and indent comment-based help to align with the function it documents.
- One space around operators and after commas (
$a -eq $b,@(1, 2, 3)), and one space between a type and the name —[string] $Name, not[string]$Name. elseifis one word, notelse if.- Blank lines separate logical blocks. No trailing whitespace, and end every file with a single newline.
- Keep code lines readable — aim for roughly 120 columns. When a call grows long, prefer splatting over backtick line-continuations.
Idioms and pitfalls#
Beyond the basics, these language-specific habits keep PowerShell correct and fast:
- Single-quote strings unless you need expansion. Use
'literal'by default; reserve"...$var..."for interpolation or escape sequences, and here-strings (@'...'@,@"..."@) for multi-line text — literal-versus-interpolated intent then stays obvious. - Splat calls that carry many parameters. Build a
@{}of parameters and splat it (Get-Thing @params) instead of a long line of-Param valuepairs or backtick continuations; it reads better and diffs cleanly. - Put
$nullon the left of a comparison —$null -eq $x, never$x -eq $null. Against a collection the right-hand form filters rather than tests. Use-contains/-infor membership, never-eq. - Suppress unwanted output with
$null = ...(or[void]for method calls), not| Out-Null— the pipeline form is markedly slower on hot paths. - Build collections with a typed list, not
+=in a loop.$a += $xreallocates the whole array every iteration; use[System.Collections.Generic.List[T]]with.Add(), and prefer a cmdlet's-Filterover piping toWhere-Objecton large sets. - Keep secrets out of source, and never
Invoke-Expressionuntrusted input. Accept credentials as a[PSCredential]parameter with the[Credential()]attribute rather than callingGet-Credentialinside a reusable function, so a caller can pass one they already hold, and take other sensitive values as[securestring]. Guard state-changing commands withShouldProcess(see Functions); the wider rules live in the Security baseline.
Toolchain#
The toolchain enforces this standard in CI — it does not define it. The rules above are the source of truth; each tool's configuration is derived from them:
- PSScriptAnalyzer is the linter and formatter; its settings are derived from this standard, so passing it cleanly means matching the standard. Let it format — do not hand-format.
- Pester is the test framework; test files are named
*.Tests.ps1. See the Testing baseline.