Comment-Based Help That Scales in PowerShell: Example-Driven, Tested, and CI-Friendly
You can turn your PowerShell functions into self-serve tools by writing concise, example-driven comment-based help and verifying it continuously. When your help is clear and your examples run as written, teammates succeed on the first try, onboarding gets faster, and support questions drop. This post shows how to author scalable help, keep examples runnable, and wire up CI to prevent regressions.
Design comment-based help that scales
The anatomy of great comment-based help
Place a help block directly above your function using <# ... #>. Focus on a crisp .SYNOPSIS, accurate .PARAMETER sections, and runnable .EXAMPLEs. Include .OUTPUTS when the function returns objects. Keep sentences short and imperative.
function Get-ServiceStatus {
<#
.SYNOPSIS
Check the status of a Windows service quickly.
.DESCRIPTION
Returns a concise object describing the service by name. Use it in scripts or pipelines to gate deployments
and troubleshoot startup issues.
.PARAMETER Name
The service name (not display name). Accepts pipeline input by property name (Name) for bulk checks.
.EXAMPLE
Get-ServiceStatus -Name 'Spooler'
Queries the Print Spooler service and returns status, showing the minimal usage.
.EXAMPLE
'Spooler','Dhcp' | ForEach-Object { [pscustomobject]@{ Name = $_ } } | Get-ServiceStatus
Demonstrates pipeline-by-property-name for checking multiple services.
.OUTPUTS
PSCustomObject
.NOTES
Run Get-Help Get-ServiceStatus -Full for details and parameter notes.
#>
[CmdletBinding()]
param(
[Parameter(Mandatory, ValueFromPipelineByPropertyName)]
[ValidateNotNullOrEmpty()]
[Alias('ServiceName')]
[string]$Name
)
process {
try {
$svc = Get-Service -Name $Name -ErrorAction Stop
[pscustomobject]@{
Name = $svc.Name
Status = $svc.Status
CanStop = $svc.CanStop
}
} catch {
throw [System.ArgumentException]::new("Service '$Name' was not found.")
}
}
}
# Quick validation during development
Get-Help Get-ServiceStatus -Full | Out-Null
Get-Help Get-ServiceStatus -Examples | Out-NullStyle guidelines that reduce confusion
- .SYNOPSIS: one sentence, 80-120 chars, start with a verb (“Check”, “Get”, “Set”).
- .DESCRIPTION: 2-4 sentences explaining when to use the function and the shape of the output.
- .PARAMETER: write what the user must supply, accepted formats, and gotchas. Avoid restating the parameter name.
- .EXAMPLE: show real commands that copy/paste and run. Prefer one behavior per example. Comment the intent in one line.
- .OUTPUTS: state the type (e.g., PSCustomObject) and important properties.
- Consistency: reuse nouns and verbs across your tooling so Get-* and Set-* behave predictably.
Keep examples runnable and current
If your examples rot, users lose trust quickly. Treat examples as tests: parse them from help and run them in CI. When an API or parameter changes, the failing example pinpoints what to fix.
Harvest and execute .EXAMPLEs with Pester
The pattern below extracts examples from Get-Help and executes them. Use Skip logic for OS-specific examples, and prefer environment-driven values so CI remains deterministic.
# Tests/Help.Examples.Tests.ps1
param(
[string]$CommandName = 'Get-ServiceStatus'
)
# Import your module if applicable
# Import-Module ./out/MyModule/MyModule.psd1 -Force
Describe "Help examples for $CommandName" -Tag 'help' {
# Arrange: ensure a predictable value for CI
if (-not (Test-Path Env:TEST_SERVICE)) { $env:TEST_SERVICE = 'Spooler' }
$help = Get-Help $CommandName -Full
It "has a synopsis" { $help.Synopsis | Should -Not -BeNullOrEmpty }
It "has at least one example" { $help.examples.example.Count | Should -BeGreaterThan 0 }
foreach ($ex in $help.examples.example) {
$title = ($ex.title | Out-String).Trim()
$code = ($ex.code | Out-String).Trim()
It "example runs: $title" -Skip:(-not $IsWindows) {
# Optionally patch OS-specific literals to env-driven values
$patched = $code -replace "'Spooler'", "'$env:TEST_SERVICE'"
# Act
$result = Invoke-Expression $patched
# Assert: just verify it didn't throw; add stricter checks per command
$PSItem.Exception | Should -BeNullOrEmpty
}
}
}Tips for resilient examples:
- Avoid destructive examples; show
-WhatIfvariants when supported. - Prefer environment-driven placeholders (
$env:TEST_SERVICE) over hard-coded values in CI. - For cross-platform modules, mark Windows-only tests with
-Skip:(-not $IsWindows). - When examples emit objects, assert on shape (properties) rather than exact content.
Wire into GitHub Actions CI
Run Pester and fail the build if help or examples are missing or broken.
# .github/workflows/help-ci.yml
name: help-ci
on:
push:
pull_request:
jobs:
test-help:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- name: Install PowerShell modules
shell: pwsh
run: |
Set-PSRepository PSGallery -InstallationPolicy Trusted
Install-Module Pester -Scope CurrentUser -Force
Install-Module PSScriptAnalyzer -Scope CurrentUser -Force
- name: Lint scripts
shell: pwsh
run: |
Invoke-ScriptAnalyzer -Path . -Recurse -Settings ./ScriptAnalyzerSettings.psd1 -ReportSummary
- name: Test help examples
shell: pwsh
env:
TEST_SERVICE: Spooler
run: |
Invoke-Pester -CI -Path ./Tests -Output DetailedAutomate documentation drift checks with PSScriptAnalyzer
Use PSProvideCommentHelp to ensure exported functions have comment-based help, and add custom checks for synopsis and parameter coverage.
# ScriptAnalyzerSettings.psd1
@{
IncludeRules = @(
'PSProvideCommentHelp',
'PSAvoidUsingWriteHost'
)
Rules = @{
PSProvideCommentHelp = @{ Enable = $true; ExportedOnly = $false }
}
}You can also add a tiny guard to fail when help is missing or low quality.
# Build/Assert-HelpQuality.ps1
$errors = @()
Get-Command -Module . -CommandType Function | ForEach-Object {
$cmd = $_.Name
$help = Get-Help $cmd -Full
if (-not $help.Synopsis -or $help.Synopsis.Trim().Length -lt 10) { $errors += "Weak synopsis for $cmd" }
$declaredParams = (Get-Command $cmd).Parameters.Keys | Where-Object { $_ -ne 'WhatIf' -and $_ -ne 'Confirm' }
foreach ($p in $declaredParams) {
if (-not ($help.parameters.parameter | Where-Object { $_.name -eq $p })) {
$errors += "Missing .PARAMETER documentation for $cmd:$p"
}
}
}
if ($errors) {
$errors | ForEach-Object { Write-Error $_ }
throw "Help quality checks failed: $($errors.Count) issue(s)."
}Patterns for clarity and success on the first try
Parameter sets and targeted examples
If your function supports multiple modes, provide an example for each parameter set. Users should see exactly which switches work together and what output to expect.
function Invoke-Deploy {
<#
.SYNOPSIS
Deploy an app to a target environment.
.DESCRIPTION
Supports dry runs and version pinning. Examples demonstrate each parameter set.
.PARAMETER Environment
The target environment name. Example: Dev, Test, Prod.
.PARAMETER Version
Optional image or package version to deploy. Defaults to latest.
.PARAMETER WhatIf
Shows what would happen without making changes.
.EXAMPLE
Invoke-Deploy -Environment Dev -WhatIf
Dry run for Dev. Safe to copy/paste in CI smoke checks.
.EXAMPLE
Invoke-Deploy -Environment Prod -Version 1.4.3
Pin a production deploy to a specific version.
#>
[CmdletBinding(SupportsShouldProcess)]
param(
[Parameter(Mandatory)][ValidateSet('Dev','Test','Prod')][string]$Environment,
[string]$Version
)
if ($PSCmdlet.ShouldProcess("$Environment", "Deploy version ${Version:-latest}")) {
# deploy logic here
}
}Output-focused examples
Show the shape of the output so users can compose pipelines confidently:
# Good: demonstrate properties
Get-ServiceStatus -Name 'Spooler' | Format-List Name,Status,CanStop
# Good: demonstrate filtering
'Spooler','Dhcp' | ForEach-Object { [pscustomobject]@{ Name = $_ } } | Get-ServiceStatus |
Where-Object Status -eq 'Running'Security and reliability tips
- Validate inputs with
[ValidateSet],[ValidateScript], or[ValidatePattern]and document accepted values. - Prefer throwing meaningful exceptions (
[ArgumentException]) and mention them in.DESCRIPTIONif relevant. - Avoid secrets in examples. Show how to pass credentials securely (e.g.,
Get-Credential) without printing them. - Mark destructive commands with
[CmdletBinding(SupportsShouldProcess)]and include a-WhatIfexample.
A practical checklist
- Write a one-sentence
.SYNOPSISand a short.DESCRIPTION. - Document every user-facing parameter with
.PARAMETER. - Add 2-4 runnable
.EXAMPLEs that cover common scenarios and parameter sets. - State
.OUTPUTStype and key properties. - Run
Get-Help -FullandGet-Help -Examplesas part of your local workflow. - Add Pester tests that execute examples and fail fast on errors.
- Enforce help presence with PSScriptAnalyzer in CI.
- Refine examples when behavior changes; treat them as contracts with your users.
Document once, unblock many. With concise, example-driven help and automated checks, your PowerShell functions become reliable, discoverable tools that scale across your team.