en-US/about_Import_Completers.help.txt
|
TOPIC about_Import_Completers SYNOPSIS Explains how to import standalone completer scripts into this module, which of the two import tiers to use, how to scaffold a new completer script, and how to check and verify a completer script. LONG DESCRIPTION CompleterActions can import an existing standalone completer script without registering it immediately in the live session. `Import-CompleterScript` executes the script inside a temporary capture module that shadows `Register-ArgumentCompleter`, collects what the script tried to register, and emits one or more `CompleterActions.ImportedCompleterRegistration` objects. Those output objects pipe directly into `Register-Completer -InputObject`. This lets you move an existing script into the module's managed workflow while keeping the live runtime unchanged until you explicitly register the imported definitions. The import has two tiers, and the difference between them is what happens before the script executes: - The strict tier, which is the default, validates the script against a closed grammar first and refuses to run it if anything outside the grammar appears at script scope. - The trusted tier, selected with `-Trusted`, skips the grammar and dot-sources the script as-is. A strict script registered lazily is checked earlier as well, when it is registered: `Register-Completer -Lazy` runs the grammar on the parse that reads the script's targets and refuses a script that does not conform, with one line per finding, and `Import-CompleterSet` reports such a script as an invalid entry, unless the entry's `Hash` matches and the script is not parsed at all. The check before the script executes still runs on its first tab press, so a script edited after it was registered is checked in its edited form. Three more commands support the workflow around the import: - `New-CompleterScript` writes a new native completer script that already conforms to the strict grammar, with a subcommand table read from the tool's help. It is the starting point for a new script. - `Test-CompleterScript` runs the strict grammar without executing the script and returns every finding as an object with a line number and a fix hint. It is the conformance step. - `Test-CompleterRegistration` runs tab completion for an input against a registered target and returns the matches. It is the verification step. THE STRICT TIER The strict tier exists for scripts you did not write, or for scripts you want to keep import-safe by construction. Its promise is simple: nothing at script scope runs unless the grammar recognizes it, so importing a script cannot load a module, call a method, write a file, or set an environment variable as a side effect. Script scope may contain only: - `Set-StrictMode` - function definitions - `if` statements whose conditions and bodies stay inside the grammar (typically a `Get-Variable` guard around a `$script:` assignment) - script-scope `Register-ArgumentCompleter` calls with literal arguments Everything else must live inside a function body or inside the literal `-ScriptBlock`, where it runs lazily, at completion time, and only after you have registered the completer. The full rule set is listed under THE STRICT GRAMMAR below. When a script breaks a rule, the import fails with the same findings that `Test-CompleterScript` reports, so you can either fix the script or import it through the trusted tier. THE TRUSTED TIER The trusted tier exists for a completer repository you own. Every exception the strict grammar grows is a script its owner already trusts, so instead of growing the grammar, `-Trusted` says "this is my code, run it": Import-CompleterScript -Path .\git_completer.ps1 -Trusted | Register-Completer Under `-Trusted`: - The grammar is skipped entirely. Top-level assignments, `try`/`catch`, loops, `[pscustomobject]` literals, method calls, wrapper functions around `Register-ArgumentCompleter`, splatting, `#requires -Modules`, and anything else the script does at script scope all run at import time, exactly as they would when the script is dot-sourced from a profile. - The script still runs inside the capture module, so `Register-ArgumentCompleter` is still shadowed, the live runtime tables are still untouched until you register, and helper functions and `$script:` state are still preserved on the imported script block. - Registrations are discovered the same way as in the strict tier: any `Register-ArgumentCompleter` call the script makes while it runs is captured, whether it sits at script scope, inside a function the script calls, or inside a `try` block. - The emitted records carry `Trusted` set to `$true`. Strict imports carry `Trusted` set to `$false`. The trusted tier does not change what a well-formed completer looks like. A script that conforms to the strict grammar imports identically under both tiers. CHOOSING A TIER - Use the strict tier for scripts from other people, scripts you are converting for the first time, and scripts you want a conformance test to guard. - Use `-Trusted` for your own completer repository when a script needs a shape the grammar does not allow and rewriting it is not worth the effort. - Run `Test-CompleterScript` in either case. It tells you which tier a script needs, and its hints show what a strict-tier rewrite would take. WHEN TO USE IMPORT-COMPLETERSCRIPT Use `Import-CompleterScript` when you already have a `.ps1` file that calls `Register-ArgumentCompleter` and you want to: - inspect what the script would register before changing the session - convert old standalone completer scripts into managed registrations - preserve helper functions and script-scope state used by the completer - register the imported definitions later with `Register-Completer` Do not use `Import-CompleterScript` if you only need to create a new completer from a script block you already have in memory. In that case, call `Register-Completer` directly. HOW IMPORT-COMPLETERSCRIPT WORKS `Import-CompleterScript` performs four steps for each input file: 1. Resolve the input path to a single `.ps1` file. 2. In the strict tier, parse the file and validate it against the grammar. Any finding stops the import before the script runs. The trusted tier skips this step. 3. Execute the script inside a temporary module that shadows `Register-ArgumentCompleter`, so the script can declare its completers without modifying the live runtime tables. 4. Emit normalized import objects for each resolved target. A single `Register-ArgumentCompleter` call can produce more than one output object. For example, a script that registers one native completer for both `tool` and `tool.exe` produces two imported registrations, one per command target. PARAMETERS `Import-CompleterScript` has two path parameters and one tier switch: - `-Path` Accepts one or more paths. Wildcards are resolved. This parameter accepts pipeline input and property-name binding from `FullName`. - `-LiteralPath` Accepts one or more literal file paths. Wildcards are not expanded. This parameter accepts property-name binding from `PSPath`. - `-Trusted` Selects the trusted tier for every file in the call. Both path parameters require `.ps1` files. Directories and non-`.ps1` files are rejected. `Test-CompleterScript` takes the same two path parameters. OUTPUT OBJECTS Each imported object is a `CompleterActions.ImportedCompleterRegistration` record with properties that are ready for `Register-Completer -InputObject`. The most useful properties are: - `Key` and `RegistrationKey` The module's normalized key for the target. - `RuntimeKey` The target key as PowerShell runtime registration logic expects it. - `CommandName` The command or native executable name. - `ParameterName` The parameter name for parameter completers. Native completers leave this empty. - `Native` and `IsNative` Indicate whether the target is a native completer. - `CompleterType` Either `Parameter` or `Native`. - `Trusted` `$true` when the record came through the trusted tier, `$false` when it came through the strict grammar. - `Path` and `SourcePath` The source file that produced the imported registration. - `ScriptBlock` The imported script block. It preserves the temporary module context that contains helper functions and script-scope state from the source script, and it keeps the source script as its file, so `$PSScriptRoot` and `$PSCommandPath` inside the completer name the script's directory and path exactly as they do when the script is dot-sourced. - `ScriptText` The script block text, which is useful for inspection and debugging. SCAFFOLDING A COMPLETER WITH NEW-COMPLETERSCRIPT `New-CompleterScript` writes a new native completer script that already follows the strict grammar, with a table of subcommands read from the tool's help. Start every new script from it, then check the file: New-CompleterScript -CommandName cargo ` -Path .\cargo_completer\cargo_completer.ps1 Test-CompleterScript -LiteralPath .\cargo_completer\cargo_completer.ps1 `Test-CompleterScript` reports no findings, because the file conforms before you touch it. Without `-PassThru` the command returns nothing; with it, it returns the written file, which pipes into `Test-CompleterScript` and `Import-CompleterScript`. The folder must exist, the path must end in `.ps1`, and an existing file is replaced only with `-Force`. A call that fails leaves nothing at the path. Seeding the table The subcommand table is seeded in one of three ways, one per parameter set: - The probe, which is the default, runs the tool's help and parses it. Use it for a console program that prints its help and exits. Without `-HelpArgument` the probe passes `--help`. On Windows, when `--help` ran to completion, gave no subcommand, and printed fewer than five non-blank lines, which is what a tool that rejects `--help` prints, `/?` runs once and its result is used; `schtasks`, `reg`, `bcdedit`, and `netsh` behave that way. On Linux and macOS `/?` is never tried. `-HelpArgument` passes one argument of your choice instead, such as `help` or `-h`, and the program runs once. - `-HelpText` parses help you captured yourself, and nothing runs. Use it for a tool the probe refuses to run, such as a `.cmd` shim or an app execution alias, for help that needs more than one argument, such as `tool help commands`, or whenever you would rather run the tool yourself. Lines piped in are joined before they are parsed: winget --help | New-CompleterScript -CommandName winget ` -Path .\winget_completer\winget_completer.ps1 - `-NoProbe` runs nothing and writes an empty table. Use it for a tool without subcommands, or when you will write the table by hand. `-WhatIf` and `-Confirm` name both the file and the program the probe would run, and the probe runs only after the call is confirmed. What the probe runs The probe resolves the first `-CommandName` value with `Get-Command -CommandType Application`, so it never runs a function, an alias, a cmdlet, or a PowerShell script of that name. On Windows it then reads the file's program header, without starting it, and runs only a `.exe` whose header marks a console program. It does not run a GUI program, a `.cmd` or `.bat` shim such as `npm.cmd`, or an app execution alias such as `winget.exe` under `WindowsApps`, whose header cannot be read. On Linux and macOS any resolved application runs, and a script with a shebang runs its own interpreter. When the command is not found as an application, is refused, or fails to start, a warning says why and suggests `-HelpText`, and the script is written with an empty table. The program runs with its one argument and no shell, with standard input closed, so a tool that reads input sees the end of it at once, and with `NO_COLOR=1` added to the session's environment. Its working directory is the temporary directory, `[System.IO.Path]::GetTempPath()`, so a tool that writes into its current directory does not write into your repository. One 5-second limit covers the run and the reading of its output. The output is dropped, with a warning, in two cases: - the program has not exited at the limit, so it and the processes it started are stopped; - the program exited, but a process it started, such as a build server or a device bridge, still holds its output a second later, so the output is not waited for. The exit code decides nothing, because many tools print their help and exit with a nonzero code. The text used is standard output, or standard error when standard output is empty. It is decoded, and terminal colours, links, backspace overstrikes, and carriage-return progress lines are removed. The same cleaning applies to `-HelpText`. How the help is read The parser looks for a commands section and reads one subcommand per line, keeping the help's order: - A section starts at a header line of one to six words, one of which is `command`, `commands`, `subcommand`, or `subcommands`, optionally followed by a colon or written between `<` and `>`. `Commands:`, `CORE COMMANDS`, `Basic Commands (Beginner):`, and `<Commands>` all start a section; `Options:` does not. - An entry is a name, a separator, and a description. The separator is a colon and a space, two or more dashes, ` - `, two or more spaces, or a tab. A name starts and ends with a letter or digit, and aliases after it (`build, b`) are dropped. - The first entry fixes the section's indentation. A line indented deeper continues a description, and any other line that is not an entry is skipped without ending the section. A blank line, another header, or an entry indented less ends it. - A name that appears twice is kept once. Control and formatting characters in a description become spaces, and an empty description becomes the name. `cargo --help` works: its `Commands:` section gives 16 subcommands, `build`, `check`, `clean`, and so on, and its last line, `... See all commands with --list`, is not an entry, because a name starts with a letter or digit. `pip --help` works too and gives all 18, although two of its descriptions wrap back to the first column (`dependencies.`), because a line that is not an entry does not end the section. `git --help` does not work: its header, `These are common Git commands used in various situations:`, has nine words, and its commands are grouped under headings of their own, so the table is empty. An empty table is not an error and draws no warning, because it is also the right result for a tool with options only, such as `rg`. A partial table is the case to watch for. `sc --help` on Windows gives 35 subcommands but misses `qmanagedaccount`, whose line, `qmanagedaccount-Queries ...`, uses a single dash, which is not a separator. The script still passes every check, and the command writes no warning. Compare a seeded table with the tool's help before you rely on it, and add what is missing by hand. Residual risks The probe runs a program you did not write, and some risks remain: - The header check cannot see through a console shim that starts a GUI program. - Some tools treat `--help` as an operand: `tee --help` writes a file named `--help` into the temporary directory, and a BSD `yes` prints until the limit. - On Linux and macOS a child can open `/dev/tty` although its standard input is closed. - A process the program started and left running after it exited is no longer in the process tree the probe stops, so it can outlive the probe. `-NoProbe` and `-HelpText` avoid all of them. The skeleton This is the cargo script, with the table cut short: # cargo tab completion for PowerShell # Help-seeded native completer: the subcommand table was read from 'cargo --help' when the script was generated. Set-StrictMode -Version 2.0 if (-not (Get-Variable -Name CargoCompletionCatalog -Scope Script -ErrorAction Ignore)) { $script:CargoCompletionCatalog = @{ Subcommands = @( @{ Name = 'build'; Description = 'Compile the current package' } @{ Name = 'check'; Description = 'Analyze the current package and report errors, but don''t build object files' } ) } } function Complete-Cargo { param( [string]$wordToComplete, [System.Management.Automation.Language.CommandAst]$commandAst, [int]$cursorPosition ) # Offer subcommands in the first argument position only; extend this function for options and values. $precedingElements = @($commandAst.CommandElements | Where-Object { $_.Extent.EndOffset -lt $cursorPosition }) if ($precedingElements.Count -gt 1) { return } foreach ($subcommand in $script:CargoCompletionCatalog.Subcommands) { if ($subcommand.Name.StartsWith($wordToComplete, [System.StringComparison]::OrdinalIgnoreCase)) { [System.Management.Automation.CompletionResult]::new($subcommand.Name, $subcommand.Name, 'ParameterValue', $subcommand.Description) } } } Register-ArgumentCompleter -Native -CommandName 'cargo', 'cargo.exe' -ScriptBlock { param($wordToComplete, $commandAst, $cursorPosition) Complete-Cargo -wordToComplete $wordToComplete -commandAst $commandAst -cursorPosition $cursorPosition } Read against the three shapes below: - The two comment lines name the command and the source of the table. From `-HelpText` the second reads `... read from help text passed to New-CompleterScript.`, and an empty table gives `# Native completer skeleton: add subcommands to the table and options to Complete-Cargo.` No date, version, or machine path appears, so the same input always gives the same bytes. - The guarded `$script:CargoCompletionCatalog` block is shape 1: one `$script:` assignment of literals behind a `Get-Variable` guard. Every string is single-quoted, with each quote character doubled, as in `don''t`. An empty table is written `Subcommands = @()`. - `Complete-Cargo` is where shape 2's work goes. It offers the table in the first argument position only, and returning nothing elsewhere lets PowerShell fall back to path completion. Extend it for options and values, and put anything computed, such as tool discovery or a cache filled from live output, in a function it calls lazily. The skeleton has no initializer of its own, because a static table needs none. - The last statement is shape 3: one bare script-scope `Register-ArgumentCompleter` with `-Native`, a literal `-CommandName` list, and a literal `-ScriptBlock`. Each name is written as given and followed by its `.exe` form unless it already ends in `.exe`, `.cmd`, `.bat`, `.ps1`, or `.com`, so `-CommandName rg` registers `'rg', 'rg.exe'` and `-CommandName npm.cmd` registers `'npm.cmd'`. - The first name gives the stem of the state variable and the function: `cargo` gives `Cargo`, `cargo-binstall` gives `CargoBinstall`, and `oh-my-posh` gives `OhMyPosh`. The script uses four-space indentation and opening braces on the same line, as completer repositories usually do. The guarantee Before the file is put in place, the command writes the script to a temporary file beside it, checks that file with the strict grammar `Test-CompleterScript` applies, and derives its targets as `Register-Completer -Lazy` does. Only a file with no finding that registers exactly the target list is moved into place, so every script the command writes passes `Test-CompleterScript`, whatever help it was seeded from. If the check fails, the command writes nothing and asks you to report the defect. The file is UTF-8 without a byte-order mark, with the platform's newline and a final newline. Once you edit the script, keeping it inside the grammar is your responsibility. Run `Test-CompleterScript` after every change, as the BASIC WORKFLOW below does. THE STRICT GRAMMAR To import through the strict tier, a standalone completer script must follow these rules: - Keep the script self-contained. Do not dot-source other scripts, and do not use `#requires -Modules`, `#requires -Assembly`, `using module`, or `using assembly`; they load code when the script is dot-sourced. `#requires -Version` and `using namespace` are fine. - Keep script-scope statements limited to `Set-StrictMode`, function definitions, guarded `if` statements, and script-scope `Register-ArgumentCompleter` calls. `Get-Variable` is allowed so the guard can test for existing state. - Inside a guarded `if`, assign only to unqualified or `$script:` variables, and assign only literal values: strings, numbers, arrays, and hashtables of those. No `[pscustomobject]` or other type casts, no `[type]::Member` access, no method calls, no subexpressions, no command output. - Call the built-in `Register-ArgumentCompleter` command directly, at script scope. Do not wrap it in a function, do not call it from inside a function or script block, and do not put it inside `try`/`catch`. - Do not define a function named `Get-Variable`, `Register-ArgumentCompleter`, or `Set-StrictMode`, with or without a scope qualifier such as `script:`, `local:`, or `global:`. A definition by any of those names shadows the built-in the grammar allows, and its body would run at import time. - Use only `-CommandName`, `-ParameterName`, the bare `-Native` switch, and `-ScriptBlock`, always by name. Positional arguments and splatting are not supported. - Keep `-CommandName` and `-ParameterName` literal: a literal string, a literal string array, or a literal `@('tool', 'tool.exe')` expression. - Keep `-ScriptBlock` a literal `{ ... }` block, not a variable. - Move cache initialization, alias bootstrap, tool discovery, generated completion loading, and every other piece of setup into functions that run lazily from inside the registered script block. The three shapes below cover what almost every completer needs at script scope. Together they are the whole vocabulary. SHAPE 1: LITERAL-ONLY GUARDED SCRIPT STATE A completer often keeps a small table of values or a cache at script scope. The import-safe way to declare it is an `if` guard around a `$script:` assignment whose value is made only of literals. This is rejected, because the `[pscustomobject]` cast and the `[Environment]::GetEnvironmentVariable` call both run at import time: if (-not (Get-Variable -Name DemoState -Scope Script ` -ErrorAction SilentlyContinue)) { $script:DemoState = [pscustomobject] @{ Values = @('alpha', 'beta') Home = [Environment]::GetEnvironmentVariable('HOME') } } `Test-CompleterScript` reports the cast, which is the outermost unsupported construct: Line : 4 Column : 25 Severity : Error Construct : ConvertExpressionAst Message : The script contains unsupported top-level expression 'ConvertExpressionAst'. Hint : A type cast runs at import time. Move the [pscustomobject] literal into a lazy initializer inside a function. Removing the cast lets the grammar look inside the hashtable, and the next run reports the method call on line 6 as `InvokeMemberExpressionAst` with the hint to move it into a lazy initializer. This is the same state expressed inside the grammar. The hashtable holds literals only, and the value that needs a call moves to where it is read: if (-not (Get-Variable -Name DemoState -Scope Script ` -ErrorAction SilentlyContinue)) { $script:DemoState = @{ Values = @('alpha', 'beta') Home = $null } } function Get-DemoHome { if ($null -eq $script:DemoState.Home) { $script:DemoState.Home = [Environment]::GetEnvironmentVariable('HOME') } $script:DemoState.Home } A hashtable is enough for almost every cache. If the completer needs a typed object, build it in a function, as shape 2 shows. SHAPE 2: LAZY INITIALIZERS INSIDE FUNCTIONS Anything that computes, discovers, or calls out belongs in a function that the completer calls on first use. The function owns the guard, so script scope never has to run the expensive part. This is rejected, because tool discovery and object construction run at import time, on every session start, whether or not the completer is ever used: $script:DemoTools = Get-Command -Name demo* -CommandType Application | ForEach-Object { [pscustomobject] @{ Name = $_.Name; Path = $_.Source } } This is the same work inside the grammar. Nothing runs until the first tab press, and the result is cached for the rest of the session: function Get-DemoTool { if (-not (Get-Variable -Name DemoTools -Scope Script ` -ErrorAction SilentlyContinue)) { $script:DemoTools = @( Get-Command -Name demo* -CommandType Application | ForEach-Object { [pscustomobject] @{ Name = $_.Name Path = $_.Source } } ) } $script:DemoTools } function Complete-DemoTool { param( [string] $WordToComplete ) foreach ($tool in Get-DemoTool) { if ($tool.Name -like "$WordToComplete*") { [System.Management.Automation.CompletionResult]::new( $tool.Name, $tool.Name, 'ParameterValue', $tool.Path ) } } } The same pattern covers alias bootstrap, reading generated completion files from disk, probing a tool's `--help` output, and importing a module the completer depends on: put the work behind a function, guard it, and call it from the registered script block. SHAPE 3: BARE SCRIPT-SCOPE REGISTER-ARGUMENTCOMPLETER The registration call itself must be plain: at script scope, calling the built-in command by name, with every argument named and literal. This is rejected twice over: `Test-CompleterScript` reports the `try`/`catch` as an unsupported `TryStatementAst` at script scope, and the `Register-ArgumentCompleter` call inside the wrapper as a `CommandAst` registered from a nested function. Moving the call to script scope would expose the third problem, the splatted arguments, which the grammar cannot analyze: function Register-DemoCompleter { param($Names) $registration = @{ Native = $true CommandName = $Names ScriptBlock = { param($wordToComplete) ... } } Register-ArgumentCompleter @registration } try { Register-DemoCompleter -Names @('demo', 'demo.exe') } catch { Write-Warning "demo completer failed: $_" } This is the same registration inside the grammar. The `try`/`catch` goes away because a bare `Register-ArgumentCompleter` with literal arguments has nothing left to fail on, and the wrapper goes away because there is nothing left to wrap: Register-ArgumentCompleter ` -Native ` -CommandName @('demo', 'demo.exe') ` -ScriptBlock { param($wordToComplete, $commandAst, $cursorPosition) $null = $commandAst, $cursorPosition Complete-DemoTool -WordToComplete $wordToComplete } The helper functions and `$script:` state remain available to the script block because the imported script block keeps the temporary module context created during import. The rejected version is not broken, only unanalyzable. Imported with `-Trusted`, the wrapper runs, the shadowed `Register-ArgumentCompleter` captures both targets, and the result is two records for `demo` and `demo.exe` with `Trusted` set to `$true`. A COMPLETE STRICT-TIER SCRIPT Putting the three shapes together, this native completer imports under the strict tier: Set-StrictMode -Version 2.0 if (-not (Get-Variable -Name DemoState -Scope Script ` -ErrorAction SilentlyContinue)) { $script:DemoState = @{ Values = @('alpha', 'beta') } } function Complete-DemoTool { param( [string] $WordToComplete ) foreach ($value in $script:DemoState.Values) { if ($value -like "$WordToComplete*") { [System.Management.Automation.CompletionResult]::new( $value, $value, 'ParameterValue', $value ) } } } Register-ArgumentCompleter ` -Native ` -CommandName @('demo', 'demo.exe') ` -ScriptBlock { param($wordToComplete, $commandAst, $cursorPosition) $null = $commandAst, $cursorPosition Complete-DemoTool -WordToComplete $wordToComplete } A parameter completer is the same shape with `-ParameterName` in place of `-Native` and the five-argument `param` block that parameter completers receive: Register-ArgumentCompleter ` -CommandName 'Invoke-DemoTool' ` -ParameterName 'Name' ` -ScriptBlock { param( $commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters ) $null = $commandName, $parameterName, $commandAst, $fakeBoundParameters Complete-DemoTool -WordToComplete $wordToComplete } CHECKING CONFORMANCE WITH TEST-COMPLETERSCRIPT `Test-CompleterScript` runs the strict grammar over a script without executing it and returns one `CompleterActions.CompleterScriptFinding` per unsupported construct. It takes the same `-Path` and `-LiteralPath` parameters as `Import-CompleterScript`, so `Get-ChildItem` output binds directly: Test-CompleterScript -Path .\demo_completer.ps1 Get-ChildItem -Path ~\Completers -Recurse -Filter *.ps1 | Test-CompleterScript Each finding has: - `Path`, `Line`, and `Column` Where the construct starts. - `Severity` `Error` for every construct the strict tier rejects. - `Construct` The offending construct type, usually the AST node type such as `AssignmentStatementAst`, `ConvertExpressionAst`, or `CommandAst`, or `ParseError` when the file does not parse. - `Message` What is wrong. - `Hint` How to move the construct inside the grammar. A conforming script produces no output, so the conformance check for a whole repository is one pipeline that should stay empty: Get-ChildItem -Path ~\Completers -Recurse -Filter *.ps1 | Test-CompleterScript | Where-Object Severity -eq Error The same check works as a Pester test: It 'imports under the strict tier: <Name>' -TestCases $cases { param($Path) Test-CompleterScript -Path $Path | Should -BeNullOrEmpty } `Import-CompleterScript` builds its strict-tier error text from these same findings, so a script that passes `Test-CompleterScript` imports, and a script that fails to import shows the findings in its error. VERIFYING REGISTRATIONS WITH TEST-COMPLETERREGISTRATION Importing and registering a completer proves that the script ran. It does not prove that pressing Tab produces the right answers. `Test-CompleterRegistration` closes that gap: it resolves a target the same way `Get-Completer` does, checks that a live runtime registration exists for it, runs `TabExpansion2` for the input you give it, and returns the matches as `CompleterActions.CompletionMatch` objects: Test-CompleterRegistration -CommandName demo -Native -InputText 'demo a' RuntimeKey: demo CompletionText ListItemText ResultType ToolTip -------------- ------------ ---------- ------- alpha alpha ParameterValue alpha That is the complete strict-tier script from above, imported, registered, and completing `demo a` to `alpha`. Against a real completer the table lists every match; for a registered git completer, `-InputText 'git che'` returns `checkout`, `cherry`, `cherry-pick`, and the other `che` subcommands. Registration records pipe in directly, and `-CursorPosition` moves the cursor away from the end of the input when the completion depends on it: Get-Completer -CommandName Invoke-DemoTool ` -ParameterName Name | Test-CompleterRegistration -InputText 'Invoke-DemoTool -Name a' Test-CompleterRegistration -CommandName demo -Native ` -InputText 'demo a --verbose' -CursorPosition 6 One input text invokes one completer, so each call tests exactly one target: the command throws, naming the targets, when an array or a piped set of records resolves to more than one. The command also throws when the target has no runtime registration, returns nothing when the completer yields no matches, and never changes any registration or PSReadLine state. Completion runs from the module's scope, so the commands in `-InputText` must be visible from the global scope, as they are in an interactive session. Verifying the registered runtime behavior this way is more useful than invoking the imported script block in isolation, because it exercises the same path a Tab press does. BASIC WORKFLOW The normal workflow is: 0. For a new script, start from `New-CompleterScript`. 1. Import the module. 2. Run `Test-CompleterScript` over the completer scripts. Fix the findings using the three shapes, or decide to import with `-Trusted`. 3. Import the scripts with `Import-CompleterScript`. 4. Pipe the results into `Register-Completer`. 5. Verify with `Test-CompleterRegistration` and inspect with `Get-Completer`. 6. Add the script to the repository's set with `Export-CompleterSet`, then check the set with `Test-CompleterSet`, which returns nothing when the set matches its folder. DISTRIBUTING COMPLETERS AS A MODULE A completer repository can be published as an ordinary PowerShell module whose manifest names its set file in `PrivateData.CompleterSet`. Once it is installed with `Install-PSResource`, a profile line that names the module imports it: `Import-CompleterSet -Name <module>`. The package layout, the manifest, the rules `Import-CompleterSet -Name` applies to a package, and how to stage and publish one are in `about_Completer_Sets`, under COMPLETER SETS AS MODULES. EXAMPLE 1 Import a single parameter completer script and register it: Import-CompleterScript -Path .\MyTool.Completer.ps1 | Register-Completer -PassThru `-PassThru` returns the managed registration records so you can inspect the final registrations that were created. EXAMPLE 2 Inspect imported definitions before registering them: $imported = Import-CompleterScript -Path .\MyTool.Completer.ps1 $imported | Format-List CommandName, ParameterName, CompleterType, Trusted, Path $imported.ScriptText This is useful when you want to verify the discovered targets and the imported script text before you mutate the runtime. EXAMPLE 3 Import a whole repository through the trusted tier from a profile: Get-ChildItem -Path ~\Completers -Recurse -Filter *_completer.ps1 | Import-CompleterScript -Trusted | Register-Completer `Import-CompleterScript` accepts property-name binding from `FullName`, so `FileInfo` objects from `Get-ChildItem` work directly. EXAMPLE 4 Check one script, then import it strictly once it conforms: Test-CompleterScript -Path .\demo_completer.ps1 # ...fix the findings... Import-CompleterScript -Path .\demo_completer.ps1 | Register-Completer Test-CompleterRegistration -CommandName demo -Native -InputText 'demo a' EXAMPLE 5 Use `-LiteralPath` when the file name contains wildcard characters: Import-CompleterScript -LiteralPath '.\Completers\[demo].ps1' `-LiteralPath` treats the path exactly as written and does not expand `[]`, `*`, or `?`. COMMON FINDINGS Strict import fails when the script uses patterns the grammar cannot analyze. The finding's `Construct` names the pattern and its `Hint` names the shape that fixes it. The common ones: - `CommandAst` with an unsupported top-level command such as `Get-Date` or `Import-Module`: move the call into a function (shape 2). - `AssignmentStatementAst` at script scope: wrap it in a `Get-Variable` guard (shape 1) or move it into a function (shape 2). - `ConvertExpressionAst`, `InvokeMemberExpressionAst`, `MemberExpressionAst`, or `SubExpressionAst` inside a guarded assignment: keep the literal parts and move the computed value into a lazy initializer (shapes 1 and 2). - `TryStatementAst`, `ForEachStatementAst`, `SwitchStatementAst`, or any other control flow at script scope: move it into a function (shape 2). - `FunctionDefinitionAst` named `Register-ArgumentCompleter`, `Get-Variable`, or `Set-StrictMode`, with or without a scope qualifier such as `script:` or `global:`: rename the wrapper and call the built-in directly (shape 3). - `VariableExpressionAst` as a `-ScriptBlock`, `-CommandName`, or `-ParameterName` value, or as a splatted argument: make the argument a literal (shape 3). - `CommandParameterAst` for a parameter other than `-CommandName`, `-ParameterName`, `-Native`, or `-ScriptBlock`: drop it (shape 3). - `UsingStatementAst` or `ScriptRequirements` for modules or assemblies: remove the directive and load the dependency lazily (shape 2). - `ParseError`: the file does not parse; fix the syntax first. Any of these can also be resolved by importing the script with `-Trusted`, which is the right answer when the script is yours and the shape is deliberate. SEE ALSO Get-Help New-CompleterScript -Full Get-Help Import-CompleterScript -Full Get-Help Test-CompleterScript -Full Get-Help Test-CompleterRegistration -Full Get-Help Register-Completer -Full Get-Help Get-Completer -Full about_CompleterActions_Migration Register-ArgumentCompleter |