TB

MoppleIT Tech Blog

Welcome to my personal blog where I share thoughts, ideas, and experiences.

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-Help works 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 -Examples is 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 -Examples

Notes:

  • Defaults are visible: Surface defaults in both the .DESCRIPTION and parameter signature to prevent surprise.
  • Examples are copy-pasteable: Every example should run as-is and finish quickly.
  • [CmdletBinding()] enables common parameters like -Verbose and 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: SupportsShouldProcess adds -WhatIf/-Confirm, which Get-Help documents automatically.
  • Discoverable defaults: CompressionLevel displays its allowed values and the default in both help and intellisense.
  • Pipeline-friendly: ValueFromPipeline is advertised by .INPUTS and evident in examples.

Conventions and Best Practices

  1. 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.
  2. Document defaults in two places. Put defaults in the parameter signature and repeat them in .DESCRIPTION or .PARAMETER to avoid ambiguity.
  3. Keep examples fast and safe. Examples should run in < 5 seconds and avoid destructive state. Use -WhatIf for risky actions.
  4. Validate early. Use [ValidateSet()], [ValidateRange()], and [ValidateNotNullOrEmpty()]. The help will show accepted values, reducing errors.
  5. Describe outputs explicitly. Set [OutputType()] and include .OUTPUTS. Callers can pipe or cast correctly.
  6. One responsibility per function. Narrow focus makes help shorter and usage consistent.
  7. 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 -EnableExit

Test: 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 Detailed

From 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-US

Security and Reliability Tips

  • Advertise safety switches: Use [CmdletBinding(SupportsShouldProcess = $true)] and show -WhatIf/-Confirm usage in examples.
  • Never echo secrets: Avoid logging sensitive parameters; note redaction behavior in .NOTES if relevant.
  • Declare inputs and outputs: .INPUTS/.OUTPUTS help users compose safe pipelines and avoid unintended type conversions.
  • Handle errors predictably: Document failure modes in .DESCRIPTION and 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.

← All Posts Home →