PowerShell completion scripts for managed registrations, runtime discovery, and safe removal.
CompleterActions is a PowerShell 7+ / Core-only module for registering, discovering, querying, and removing PowerShell argument completers in a consistent way.
Important
This module targets PowerShell 7+ / PowerShell Core only. The manifest declares CompatiblePSEditions = @('Core') and PowerShellVersion = '7.0'.
- Manage both parameter completers and native command completers
- Import completer scripts into managed registration input objects through a strict grammar or, for scripts you own, a trusted tier
- Describe a whole completer repository in one set file, validate it up front, and register it with a single command
- Check completer scripts against the strict grammar and get findings with line numbers and fix hints
- Verify a registration by running tab completion for an input and getting the matches back as objects
- Query module-managed registrations and runtime-discovered registrations
- Remove managed registrations cleanly from both runtime and module state
- Require explicit opt-in before removing unmanaged runtime registrations
- Return rich registration objects with default table formatting
- Support property-name pipeline binding and paging for discovery scenarios
| Command | What it does |
|---|---|
Export-CompleterSet |
Writes a completer set file (.psd1) from registrations that came from scripts, with each script's trust tier and targets |
Get-Completer |
Lists completer registrations known to the module or discovered from the current runtime, filtered by -State and sorted by type, command, and parameter |
Import-CompleterScript |
Converts standalone completer scripts into objects that can be piped to Register-Completer -InputObject; strict grammar by default, -Trusted to run the script as-is |
Import-CompleterSet |
Validates every entry of a completer set up front, then registers the whole set lazily; -SkipInvalid warns and registers the rest |
Register-Completer |
Registers a managed completer and records it in module state; -Path -Lazy registers a completer script that loads on its first tab press |
Test-CompleterRegistration |
Runs tab completion for an input against a registered target and returns the completion matches |
Test-CompleterScript |
Checks completer scripts against the strict import grammar and returns findings with line, column, construct, and a fix hint |
Unregister-Completer |
Removes completer registrations from runtime and, when applicable, from module state |
Get-CompleterRegistration, Register-CompleterRegistration, and Unregister-CompleterRegistration remain as aliases of the renamed commands until 3.0; the first call to each in a process writes a deprecation warning. about_CompleterActions_Migration covers the move from 1.x.
Install-PSResource -Name CompleterActions
Import-Module CompleterActionsImport-Module .\CompleterActions.psd1Import-Module .\build\CompleterActions\CompleterActions.psd1Get-Help about_Import_Completers
Get-Help about_Completer_Sets
Get-Help about_CompleterActions_MigrationRuntime registration discovery and unmanaged-registration removal depend on PowerShell runtime internals. The module is tested on PowerShell 7, but future engine changes may require maintenance in that discovery path.
- Check an existing completer script with
Test-CompleterScript, or decide to import it with-Trusted. - Register a completer directly, or import the script into managed input objects.
- Verify the registration with
Test-CompleterRegistrationand inspect it withGet-Completer. - Replace or remove registrations when the target changes.
- Use
-AllowUnmanagedonly when removing runtime registrations that were not created by the module.
function Invoke-DemoTool {
[CmdletBinding()]
param(
[string] $Name
)
}
$scriptBlock = {
param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)
'alpha', 'beta', 'gamma' |
Where-Object { $_ -like "$wordToComplete*" } |
ForEach-Object {
[System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
}
}
Register-Completer -CommandName Invoke-DemoTool -ParameterName Name -ScriptBlock $scriptBlock$nativeScriptBlock = {
param($wordToComplete, $commandAst, $cursorPosition)
'status', 'switch', 'sync' |
Where-Object { $_ -like "$wordToComplete*" } |
ForEach-Object {
[System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
}
}
Register-Completer -CommandName demoexe -Native -ScriptBlock $nativeScriptBlockImport-CompleterScript -Path .\7z_completer.ps1 |
Register-Completer -PassThru# One script: findings with line, column, construct, message, and hint
Test-CompleterScript -Path .\7z_completer.ps1
# A whole repository: empty when every script conforms
Get-ChildItem -Path ~\Completers -Recurse -Filter *.ps1 |
Test-CompleterScript |
Where-Object Severity -eq ErrorImport-CompleterScript -Path .\git_completer.ps1 -Trusted |
Register-Completer# Strict tier: the targets are read from the script, which runs on the first tab press
Register-Completer -Path .\7z_completer.ps1 -Lazy
# Trusted tier: name the targets, because a trusted script is not parsed
Register-Completer -Path .\git_completer.ps1 -Lazy -Trusted -CommandName git, git.exe -Native
# Pending until the first tab press; Failed, with LoadError, if the script did not load
Get-Completer -State Pending, Failed# Build the set once, without registering anything in this session
Get-ChildItem ~\Completers -Recurse -Filter *_completer.ps1 |
Import-CompleterScript |
Export-CompleterSet -Path ~\Completers\completers.psd1
# The profile then needs one line
Import-CompleterSet -Path ~\Completers\completers.psd1Test-CompleterRegistration -CommandName git -Native -InputText 'git che'
Get-Completer -CommandName Invoke-DemoTool -ParameterName Name |
Test-CompleterRegistration -InputText 'Invoke-DemoTool -Name a'# All known registrations
Get-Completer
# A specific native completer
Get-Completer -CommandName git -Native
# A specific parameter completer
Get-Completer -CommandName Invoke-DemoTool -ParameterName Name
# Only module-managed registrations
Get-Completer -State Active, Pending, Failed, Stale
# Registrations made outside the module, and live values that replaced a managed one
Get-Completer -State Discovered, Conflicted
# Lazy registrations that have not loaded or failed to load
Get-Completer -State Pending, FailedRegister-Completer `
-CommandName Invoke-DemoTool `
-ParameterName Name `
-ScriptBlock $scriptBlock `
-Force# Remove a managed registration
Unregister-Completer -CommandName Invoke-DemoTool -ParameterName Name -Confirm:$false
# Remove by piping a registration record back in
Get-Completer -CommandName demoexe -Native |
Unregister-Completer -Confirm:$false
# Remove a runtime-only registration explicitly
Unregister-Completer `
-CommandName SomeTool `
-ParameterName Name `
-AllowUnmanaged `
-Confirm:$falseA completer set is a .psd1 data file that lists completer scripts, the trust tier each one loads under, and the targets each one registers:
@{
Version = 1
Entries = @(
@{
Path = 'git_completer.ps1'
Trusted = $false
Targets = @(
@{ CommandName = 'git'; Native = $true }
)
}
)
}Import-CompleterSet reads the file with Import-PowerShellDataFile, so the set itself can never run code, and validates every entry before registering anything: the file exists and is a .ps1, Trusted entries declare their Targets, strict entries name their targets with literal Register-ArgumentCompleter arguments so they can be derived from the parsed script and compared against any the entry declares, no target is listed by two entries, and without -Force no target already carries a managed or runtime registration for a different completer. The strict grammar itself runs when a script loads, not at import: importing a set parses each strict script once, to validate the entry, registers the targets that parse derived, and walks none of them; a script that fails the grammar moves to Failed on its first tab press. One error lists every problem; -SkipInvalid turns them into warnings and registers the rest.
Registering a set does not run the scripts. Each target gets a stub and a managed record in state Pending; the first tab press for that target loads the script, swaps in the real completer, and moves the record to Active. A script that fails to load yields no completions for that press, records the error as LoadError with state Failed, and removes its stub so PowerShell's default completion takes over. Nothing the module does hooks PSReadLine key handlers, replaces TabExpansion2, or changes PSReadLine options. about_Completer_Sets covers the schema and lifecycle in full, and tools/Measure-CompleterStartup.ps1 measures the eager and lazy startup cost of a completer repository in child pwsh -NoProfile processes. On a 169-script, 355-target repository (five samples per leg) the eager Import-CompleterScript | Register-Completer pipeline takes a median 7063.3 ms and Import-CompleterSet a median 1315.5 ms, a ratio of 0.19, under the roadmap target of 0.25 (0.18 against the highest eager median recorded on this machine, 7427.7 ms); the remaining lazy cost is one parse per strict script, about a third of the leg, plus record creation and the runtime and managed writes. A set registers as one transaction: its entries are validated and written against one snapshot of the session's registrations, and if any write fails every change the set made is rolled back.
Registration records use the CompleterActions.CompleterRegistration type and have a default table view with:
CommandParameterTypeSourceStateScriptPathLoadError
State is a CompleterState enum. It is Active for managed records whose stored script is the live runtime value and Discovered for live values that no managed record describes. If another caller replaces or removes a managed target with the built-in Register-ArgumentCompleter, the managed record becomes Stale: Get-Completer returns it alongside the live value as Conflicted, Register-Completer requires -Force to reconcile, and Unregister-Completer requires -AllowUnmanaged before it removes the live value together with the stale record. A lazy registration is Pending until its script loads on the first tab press. If that load fails, the press returns no completions and default completion applies exactly as with no completer registered; the record becomes Failed with the message in LoadError, its runtime entry is removed, and Register-Completer -Force retries. ScriptPath names the completer script behind a lazy or imported registration. Lazy loading runs inside the ordinary completer call and never touches PSReadLine key handlers, TabExpansion2, or PSReadLine options.
Test-CompleterScript returns CompleterActions.CompleterScriptFinding records shown as a list grouped by script path, with Line, Column, Severity, Construct, Message, and Hint. A conforming script returns nothing. Test-CompleterRegistration returns CompleterActions.CompletionMatch records shown as a table grouped by target key, with CompletionText, ListItemText, ResultType, and ToolTip.
Get-Completer -State selects any set of states, and the output is sorted by CompleterType, CommandName, and ParameterName before the PowerShell paging parameters apply, so you can do things like:
Get-Completer -State Active, Discovered -First 10
Get-Completer -Skip 10 -First 10 -IncludeTotalCountPipeline highlights:
Get-Completeraccepts registration records fromGet-CompleterandImport-CompleterScriptthroughInputObject, which binds every piped object by value; an input object describes one target, and arrays go to-CommandNameand-ParameterName. Keys are output-only identifiers and are never accepted as typed inputImport-CompleterScriptemits input objects that are ready forRegister-Completer -InputObjectExport-CompleterSetaccepts records fromGet-CompleterandImport-CompleterScript;Import-CompleterSetacceptsGet-ChildItemoutput throughFullNamebindingTest-CompleterScriptacceptsGet-ChildItemoutput directly throughFullNamebindingTest-CompleterRegistrationaccepts registration records fromGet-CompleterandImport-CompleterScriptRegister-Completercan accept input objects that describe a target and expose aScriptBlockUnregister-Completercan accept pipeline input directly fromGet-Completer
Example:
Get-Completer -State Active, Pending, Failed, Stale |
Unregister-Completer -Confirm:$falseThe repository uses Invoke-Build.
Invoke-Build -Task clean
Invoke-Build -Task build
Invoke-Build -Task external_help
Invoke-Build -Task Markdown_templates
Invoke-Build -Task ?The repository uses Pester.
Invoke-Pester -Path .\testsRun the tests in their own pwsh -NoProfile process. Importing PSScriptAnalyzer into the same session registers an argument completer that changes the discovered registration order the paging tests assert on. The tests also compare the tracked build\CompleterActions package against the sources, so run them before Invoke-Build -Task build, which regenerates that package.
The repository includes PSScriptAnalyzerSettings.psd1. CI runs the same two commands.
Invoke-ScriptAnalyzer -Path .\src -Recurse -Settings .\PSScriptAnalyzerSettings.psd1
Invoke-ScriptAnalyzer -Path .\tests -Recurse -Settings .\PSScriptAnalyzerSettings.psd1Once per repository, add a PowerShell Gallery API key as the GALLERY_API_KEY secret under Settings > Secrets and variables > Actions.
Each release is then a tag push:
# bump ModuleVersion in CompleterActions.psd1, move the Unreleased CHANGELOG entries
# under the new version heading, then regenerate and commit the packaged build
Invoke-Build -Task build
git commit -am 'chore(release): bump module version to X.Y.Z'
git tag vX.Y.Z
git push origin main vX.Y.ZThe tag push runs release_check, build, the Pester suite, Publish_build, and finally creates the GitHub release. The tag must equal v plus ModuleVersion or release_check throws before anything is published.
CompleterActions.psd1is the root manifest and defines the exported functions and aliases, formatting file, and PowerShell/Core compatibility.CompleterActions.psm1is a lightweight root loader that dot-sourcessrc\Classes(as one script block, so the classes can reference each other), thensrc\Privateandsrc\Public, runssrc\Bootstrap.ps1, and exports the public function set.src\Bootstrap.ps1holds the import-time work shared by the source root module and the packaged module: the runtime capability probe, module state initialization, and the three legacy aliases. The build appends it to the packaged.psm1after the function definitions.src\Classesdefines the record classes (CompleterRegistration,ImportedCompleterRegistration,CompleterScriptFinding,CompletionMatch) and theCompleterStateandCompleterTypeenums. Each class inserts its dottedPSTypeNamein the constructor, which stays the contract the format file and consumers rely on.src\Publiccontains the user-facing command surface:Export-CompleterSetGet-CompleterImport-CompleterScriptImport-CompleterSetRegister-CompleterTest-CompleterRegistrationTest-CompleterScriptUnregister-Completer- the exported legacy wrappers
Get-CompleterRegistrationLegacy,Register-CompleterRegistrationLegacy, andUnregister-CompleterRegistrationLegacybehind the three aliases
src\Privatecontains the runtime and state helpers that resolve targets, manage the module registration table, and inspect or remove runtime registrations. The strict import grammar lives inTest-CompleterScriptAst, which returnsCompleterActions.CompleterScriptFindingrecords that bothTest-CompleterScriptandImport-CompleterScriptconsume. Completer sets are read byImport-CompleterSetDefinitionand validated entry by entry inResolve-CompleterSetEntry(withGet-CompleterScriptTargetderiving strict-tier targets from the AST) against oneGet-CompleterRegistrationSnapshotof the session;Register-CompleterandImport-CompleterSetboth resolve conflicts throughResolve-CompleterRegistrationConflictand write throughAdd-CompleterRegistration, which writes a batch as one transaction: a whole set forImport-CompleterSet, one target at a time forRegister-Completer.tools\Measure-CompleterStartup.ps1is the startup benchmark: eager import versusImport-CompleterSet, each sampled in freshpwsh -NoProfileprocesses.
The module discovers live completer registrations by reflecting into PowerShell runtime internals to access the underlying completer dictionaries. That makes the current implementation practical and useful, but it also means runtime discovery depends on non-public engine details and may need maintenance if PowerShell internals change in a future release.
Assert-CompleterRuntimeCapability resolves every reflected member once during import. On an engine whose internals have changed, Import-Module fails with a single error that names the running PowerShell version and the members that could not be resolved, instead of a later Get or Register call failing deep inside the module.
- Managed registrations are tracked in module state for the current session.
- Runtime-discovered registrations can be queried even if they were not created by this module.
- Discovery covers the two target kinds the module manages, command-parameter and native completers. A completer registered with
Register-ArgumentCompleter -ParameterNamealone, without-CommandName, is stored under the bare parameter name; it is skipped with a verbose message rather than failing the query, and it is never resolved as a native target of the same name. - Removal of unmanaged runtime registrations is intentionally gated behind
-AllowUnmanaged.
