Discoverable PowerShell Functions with Comment-Based Help: Write Once, Teach Everyone
If teammates can learn a function in seconds, they will actually use it. In PowerShell, comment-based help turns your scripts into discoverable, self-documenting tools. You write the help once, and Get-Help does the rest: in the console, in VS Code hovers, and even in CI where examples can be tested. This post shows you how to craft great help, verify it automatically, and ship consistent, predictable utilities that accelerate onboarding and reduce questions.
Why Comment-Based Help Makes Functions Discoverable
- Built-in discovery:
Get-Helpworks everywhere PowerShell runs. No browser required, no wiki drift. - Inline, always up to date: Help lives next to the code. Refactors and defaults stay in sync.
- Instant syntax and examples:
Get-Help My-Command -Examplesis the fastest path from “What does this do?” to “I’m productive.” - IDE integration: Editors surface synopsis, parameters, and examples as tooltips.
- Automatable: Examples can be validated in CI with Pester. PlatyPS can generate external docs from comment help.
The Anatomy of Great Comment-Based Help
Minimal, complete example
Start with a tiny function that clearly shows defaults, parameters, and usage. Write the help once; let Get-Help expose details and examples.
function Get-Greeting {
<#
.SYNOPSIS
Return a friendly greeting.
.DESCRIPTION
Generates a greeting with an optional name. Defaults to 'world'.
.PARAMETER Name
The name to include.
.EXAMPLE
Get-Greeting -Name 'Morten'
Hello, Morten
.EXAMPLE
Get-Greeting
Hello, world
#>
[CmdletBinding()]
param([string]$Name = 'world')
"Hello, $Name"
}
# Discover help and examples
Get-Help Get-Greeting -ExamplesNotes:
- Defaults are visible: Surface defaults in both the
.DESCRIPTIONand parameter signature to prevent surprise. - Examples are copy-pasteable: Every example should run as-is and finish quickly.
- [CmdletBinding()] enables common parameters like
-Verboseand future-proofs your function.
Sections cheat sheet
.SYNOPSIS: One sentence. What does it do?.DESCRIPTION: A few lines with behavior, defaults, edge cases, and assumptions..PARAMETER <Name>: Type, meaning, accepted values, important defaults..EXAMPLE: Show real tasks. Prefer 2–5 examples..INPUTS/.OUTPUTS: Make pipeline usage and return types explicit..NOTES/.LINK: Maintenance hints and references (e.g.,about_Comment_Based_Help).
Template for production-ready functions
Here is a pattern that demonstrates pipeline support, safe operations (ShouldProcess), and clear help.
function Invoke-Backup {
<#
.SYNOPSIS
Back up a directory to a timestamped zip file.
.DESCRIPTION
Creates a zip of the source directory using a specified compression level. Defaults to 'Optimal'.
.PARAMETER Path
The directory to back up. Accepts pipeline input.
.PARAMETER Destination
Folder to write the archive to. Defaults to the current directory.
.PARAMETER CompressionLevel
Compression level. One of: Optimal, Fastest, NoCompression. Defaults to Optimal.
.EXAMPLE
Invoke-Backup -Path C:\Data -Destination D:\Backups
Creates D:\Backups\Data-2025-01-01-120000.zip
.EXAMPLE
'C:\Logs','C:\Data' | Invoke-Backup -Destination D:\Backups
Creates two zip files from pipeline input.
.INPUTS
System.String
.OUTPUTS
System.IO.FileInfo
.LINK
Get-Help
.LINK
about_Comment_Based_Help
#>
[CmdletBinding(SupportsShouldProcess = $true)]
param(
[Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
[ValidateNotNullOrEmpty()]
[string]$Path,
[Parameter()]
[string]$Destination = ".",
[ValidateSet("Optimal","Fastest","NoCompression")]
[string]$CompressionLevel = "Optimal"
)
process {
$name = Split-Path -Leaf -Path $Path
$timestamp = Get-Date -Format "yyyy-MM-dd-HHmmss"
$zip = Join-Path $Destination "$name-$timestamp.zip"
if ($PSCmdlet.ShouldProcess($Path, "Archive to $zip with $CompressionLevel")) {
Add-Type -AssemblyName System.IO.Compression.FileSystem
[System.IO.Compression.ZipFile]::CreateFromDirectory(
$Path,
$zip,
[System.IO.Compression.CompressionLevel]::$CompressionLevel,
$false
)
Get-Item $zip
}
}
}Why this works:
- Predictable behavior:
SupportsShouldProcessadds-WhatIf/-Confirm, whichGet-Helpdocuments automatically. - Discoverable defaults:
CompressionLeveldisplays its allowed values and the default in both help and intellisense. - Pipeline-friendly:
ValueFromPipelineis advertised by.INPUTSand evident in examples.
Conventions and Best Practices
- Use approved verbs and clear nouns. Stick to approved verbs (e.g., Get, Set, Invoke) so users can predict behavior and find commands via
Get-Command. - Document defaults in two places. Put defaults in the parameter signature and repeat them in
.DESCRIPTIONor.PARAMETERto avoid ambiguity. - Keep examples fast and safe. Examples should run in < 5 seconds and avoid destructive state. Use
-WhatIffor risky actions. - Validate early. Use
[ValidateSet()],[ValidateRange()], and[ValidateNotNullOrEmpty()]. The help will show accepted values, reducing errors. - Describe outputs explicitly. Set
[OutputType()]and include.OUTPUTS. Callers can pipe or cast correctly. - One responsibility per function. Narrow focus makes help shorter and usage consistent.
- Consistent formatting. Use a standard header template so every function looks familiar.
Automate Help Quality in CI
Your help is part of the interface—test it. You can enforce presence, validate examples, and keep formatting consistent during CI runs.
Lint: Require help for exported commands
PSScriptAnalyzer includes a rule (PSProvideCommentHelp) that warns when public functions lack help.
# Lint all scripts; fail on warnings in CI
Invoke-ScriptAnalyzer -Path . -Recurse -Severity Warning -EnableExitTest: Examples compile and run
Use Pester to ensure every example in help executes without throwing. For destructive commands, append -WhatIf when available.
Describe "Help for exported commands" {
$module = Import-Module ./MyModule -Force -PassThru
foreach ($cmdName in $module.ExportedCommands.Keys) {
It "$cmdName has a synopsis" {
(Get-Help $cmdName -ErrorAction SilentlyContinue).Synopsis | Should -Not -BeNullOrEmpty
}
It "$cmdName examples run" {
$help = Get-Help $cmdName -Full
foreach ($ex in $help.Examples.Example) {
$code = ($ex.Code -join "`n")
# Add -WhatIf if the command supports it and example doesn't use it
$syntax = $help.Syntax.syntaxItem
$supportsWhatIf = ($syntax.parameters.parameter.name -contains "WhatIf")
if ($supportsWhatIf -and $code -notmatch "-WhatIf") { $code += " -WhatIf" }
{ Invoke-Expression $code } | Should -Not -Throw
}
}
}
}CI: Run lint and tests on every PR
name: ci
on: [push, pull_request]
jobs:
test:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-powershell@v4
- name: Install tools
shell: pwsh
run: |
Set-PSRepository -Name PSGallery -InstallationPolicy Trusted
Install-Module Pester -Force -Scope CurrentUser
Install-Module PSScriptAnalyzer -Force -Scope CurrentUser
Install-Module PlatyPS -Force -Scope CurrentUser
- name: Lint
shell: pwsh
run: Invoke-ScriptAnalyzer -Path . -Recurse -Severity Warning -EnableExit
- name: Test
shell: pwsh
run: Invoke-Pester -Output DetailedFrom Comment Help to Docs and Tooltips
Once you have solid comment-based help, you can reuse it across channels.
- Generate Markdown with PlatyPS: Convert your help into versioned docs or a wiki without manual duplication.
- Tooltips in editors: VS Code surfaces synopsis and parameters from your help as hovers, reducing context switching.
- Release notes: Use examples to create usage snippets in changelogs automatically.
# Generate markdown reference from help
Import-Module PlatyPS
New-MarkdownHelp -Module MyModule -OutputFolder .\docs -Force
New-ExternalHelp -Path .\docs -OutputPath .\en-USSecurity and Reliability Tips
- Advertise safety switches: Use
[CmdletBinding(SupportsShouldProcess = $true)]and show-WhatIf/-Confirmusage in examples. - Never echo secrets: Avoid logging sensitive parameters; note redaction behavior in
.NOTESif relevant. - Declare inputs and outputs:
.INPUTS/.OUTPUTShelp users compose safe pipelines and avoid unintended type conversions. - Handle errors predictably: Document failure modes in
.DESCRIPTIONand prefer terminating errors for invalid input; users can catch them.
Practical Checklist
- Does every exported function have
.SYNOPSIS,.DESCRIPTION,.PARAMETER,.EXAMPLE,.INPUTS, and.OUTPUTS? - Are defaults obvious in both help and the signature?
- Do examples run quickly and safely as-is?
- Does CI fail if help is missing or examples break?
- Are verbs approved and parameters validated?
Make every function self-explanatory once, and you will get faster onboarding, clearer docs, fewer questions, and consistent usage across your team. Start by adding comment-based help to one function today, then wire up linting and tests so it stays great as the code evolves. For deeper patterns and advanced tooling, see the PowerShell Advanced Cookbook: Build discoverable scripts faster.