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. |
| PowerShell Testing | Pester test naming and the Simple, Standard, and Advanced profiles for PowerShell modules. |
| Messaging | Write-Verbose for user-facing operational progress and normal troubleshooting; Write-Debug for developer-focused internals and deep diagnostics. |
| Version Constraints | Express module and package version constraints as NuGet version ranges — the canonical notation across PSResourceGet, .NET package references, and (mapped) #Requires and module manifests. |
| Module Requirements | Valid #Requires -Modules version specifications — minimum, major-lock (with the N.* wildcard), exact, and GUID identity pinning — with an executable proof. |
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. - Map data and integration verbs predictably. Data modules use
ConvertFrom-/ConvertTo-around a neutral PowerShell object shape,Import-/Export-for file or store round trips, andFormat-,Merge-,Compare-,Test-, orRemove-<Noun>Entryfor structure operations. API and integration modules name commands after the resource and intent, not the transport: mapGETtoGet-, create-stylePOSTtoNew-/Add-,PUT/PATCHtoSet-/Update-,DELETEtoRemove-, and non-CRUD operations to the approved verb that matches the user intent. - 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.- Use lowercase language keywords (
if,else,foreach,class,enum,return) and reserve PascalCase for commands and named symbols. - 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.
- Avoid semicolons, ternary expressions, and regions in shared code. Put one statement on each line, use ordinary
if/elsewhen a conditional value matters, and structure code with functions, modules, and headings rather than#regionblocks.
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. - Match text with the operator built for it. Use
-likefor wildcard patterns and-matchfor regular expressions instead of hand-rolled string surgery; both default to case-insensitive, so add the-cprefix (-clike,-cmatch,-ceq) when a comparison must be case-sensitive. - Use the built-in intent checks for strings and wildcards. Use
[string]::IsNullOrWhiteSpace($value)for blank input and[System.Management.Automation.WildcardPattern]::ContainsWildcardCharacters($pattern)when deciding whether a value contains wildcard syntax. - Prefer owned code over third-party dependencies in modules. For a module we ship, exhaust PowerShell itself, the .NET base class library, and code we already own before adding an external module, DLL, or package. A third-party dependency is a long-term trust and maintenance commitment, so take it only when the capability is large enough or specialized enough that owning it ourselves is the worse trade.
- Reuse before you build. Work down the reuse order inside the code we control — a built-in cmdlet or operator, then an existing function (public or private), then shared code we own, then new code. Reach for a trusted external module (
#Requires -Modules/RequiredModules) only after that bar is cleared, and state its acceptable versions as a version range. - PowerShell already is .NET; work at that level rather than wrapping it. Casts, type accelerators (
[datetime],[int]), the-split/-replace/-matchoperators, and member methods (.Trim(),.Where()) all resolve to the base class library — using .NET means reaching for BCL types and methods for the computation, not restating everything as[Namespace.Type]::Method(...). Where idiomatic PowerShell already resolves to the same .NET call, leave it; reach for explicit .NET only where it is measurably faster or more precise, and keep cmdlets and the pipeline where you need them for glue or readability. - Do the work in .NET when you implement it. When you write the logic yourself — or fix an internal function that is too slow or imprecise on a hot path — call the .NET base class library directly instead of a cmdlet pipeline:
[System.IO.File]::ReadAllText($path)overGet-Content -Raw,[System.IO.Path]::Combine(...)for paths,[System.Text.StringBuilder]for repeated concatenation,[int]::TryParse(...)for parsing. .NET methods are faster and their contracts are precise; keep cmdlets where their clarity is worth more than the speed. The next two rules are specific cases. - 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. - Guard a value that must not change. Declare it with
Set-Variable -Name Pi -Value 3.14159 -Option ReadOnly— or-Option Constantfor one that can never be reassigned or removed — so an accidental write fails loudly instead of quietly winning. - 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. - Read cross-platform environment values through .NET when the provider is not the point. Prefer
[Environment]::GetEnvironmentVariable('NAME')for process environment reads that should behave the same on every platform; use$env:NAMEwhen you are intentionally working through the PowerShell environment provider.
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 PowerShell Testing for module test layouts.