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