Where PowerShell Command Completion Code Lives: Inside the TabExpansion2 Engine

PowerShell's command completion logic resides in the src/System.Management.Automation/engine/CommandCompletion/ directory, where the TabExpansion2 function serves as the entry point that delegates to the CommandCompletion class and CompletionCompleters for generating suggestions.

The PowerShell command completion system powers the interactive tab-completion experience you use every time you press Tab in the terminal. Unlike monolithic shell implementations, PowerShell separates its completion engine into a dedicated subsystem within the open-source PowerShell/PowerShell repository, exposing both internal APIs and extensibility points for custom completers.

The Command Completion Pipeline Architecture

The completion flow follows a strict pipeline from user input to suggestion display:

  1. User presses Tab → The host invokes the built-in function TabExpansion2
  2. CommandCompletion.CompleteInput creates a completion context and initiates analysis
  3. CompletionAnalysis parses the input into AST nodes and tokens, building a CompletionContext
  4. CompletionCompleters dispatches to specialized completers based on token type (commands, parameters, members, variables)
  5. CompletionHelpers handles quote normalization and wildcard matching
  6. CompletionResult objects return the final suggestions to the UI

Entry Point: The TabExpansion2 Function

The TabExpansion2 function is defined as a hidden script function in src/System.Management.Automation/engine/InitialSessionState.cs. During engine startup, the following script is registered via SessionStateFunctionEntry.GetDelayParsedFunctionEntry("TabExpansion2", …):

private const string s_tabExpansionFunctionText = @"
    param($commandName, $wordToComplete, $cursorPosition, $options)
    # Calls the CommandCompletion engine

    [System.Management.Automation.CommandCompletion]::CompleteInput($null,$commandName,$wordToComplete,$cursorPosition,$options,$null,$null)
";

This function acts as the bridge between the host application (PowerShell console, VS Code, etc.) and the .NET completion engine.

Core Engine: CommandCompletion and CompletionAnalysis

CommandCompletion.cs

The primary API surface lives in src/System.Management.Automation/engine/CommandCompletion/CommandCompletion.cs. The static CompleteInput method serves as the external entry point:

public static List<CompletionResult> CompleteInput(
    PowerShell powershell,
    string input,
    int cursor,
    Hashtable options,
    out int replacementIndex,
    out int replacementLength)
{
    // …parse the input into AST & tokens…
    var analysis = new CompletionAnalysis(ast, tokens, cursorPos, options);
    return analysis.GetResults(powershell, out replacementIndex, out replacementLength);
}

CompletionAnalysis.cs

The CompletionAnalysis class in src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs inspects the AST (Abstract Syntax Tree) to determine the completion context. The ExtractAstContext method identifies the token under the cursor and creates a CompletionContext object containing:

  • The AST node at the cursor position
  • Token stream from the parser
  • Word to complete (partial string)
  • Execution context for invoking sub-commands

Concrete Completers: CompletionCompleters

All domain-specific completion logic lives in src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs. This file contains the dispatch logic that routes to specialized completers based on token kind:

switch (tokenAtCursor.Kind)
{
    case TokenKind.Identifier:
        result = GetResultForIdentifier(completionContext, ref replacementIndex, ref replacementLength);
        break;

    case TokenKind.Parameter:
        result = CompletionCompleters.CompleteCommandParameter(completionContext);
        break;

    case TokenKind.Dot:
    case TokenKind.ColonColon:
        result = CompletionCompleters.CompleteMember(completionContext,
                     @static: tokenAtCursor.Kind == TokenKind.ColonColon,
                     ref replacementLength);
        break;
    // …many other cases (variables, strings, operators, etc.)…
}

The CompletionCompleters class implements static methods like CompleteCommandParameter, CompleteMember, and CompleteVariable, each handling a specific completion scenario.

Helper Utilities and Result Types

CompletionHelpers.cs

Utility functions in src/System.Management.Automation/engine/CommandCompletion/CompletionHelpers.cs handle cross-cutting concerns:

  • HandleDoubleAndSingleQuote and QuoteCompletionText manage quote insertion and escaping
  • LiteralMatchOrdinalIgnoreCase and WildcardPatternMatchIgnoreCase provide matching logic for filename completion
  • Helper methods create properly formatted CompletionResult instances

CompletionResult.cs

The CompletionResult class defined in src/System.Management.Automation/engine/CommandCompletion/CompletionResult.cs encapsulates the final suggestion data:

  • CompletionText: The actual text inserted into the command line
  • ListItemText: The display text shown in dropdown menus
  • ToolTip: Descriptive text (parameter help, type info)
  • ResultType: Enum indicating Command, Parameter, Property, Method, etc.

Programmatic Access Examples

Simulating Tab Completion from PowerShell

You can invoke the completion engine directly without pressing Tab:


# Emulate pressing Tab after typing "Get-Ser"

$input = 'Get-Ser'
$cursor = $input.Length          # cursor at end of line

$repIdx = 0
$repLen = 0
[System.Management.Automation.CommandCompletion]::CompleteInput(
    $null, $input, $cursor, @{}, [ref]$repIdx, [ref]$repLen)

This returns a collection of CompletionResult objects containing suggestions like Get-Service.

Registering a Custom Argument Completer

Custom completers integrate with the engine via Register-ArgumentCompleter:


# Register a script block that provides completions for a fake command

Register-ArgumentCompleter -CommandName 'Invoke-MyCmd' -ParameterName 'Path' -ScriptBlock {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)

    # Return any .txt files in the current directory that start with the word typed

    Get-ChildItem -Path . -Filter "$wordToComplete*.txt" |
        ForEach-Object {
            [System.Management.Automation.CompletionResult]::new(
                $_.Name, $_.Name, 'ParameterValue', $_.FullName)
        }
}

The engine stores these script blocks in the CustomArgumentCompleters dictionary within CompletionContext and invokes them when completing the specified command parameters.

Accessing Completion from C#

For tools and editors embedding PowerShell, access completions via the public API:

using System.Management.Automation;
using System.Management.Automation.Language;

// Build a PowerShell instance
PowerShell ps = PowerShell.Create(RunspaceMode.CurrentRunspace);

// Request completions for "Get-P*" at cursor position 7
var results = CommandCompletion.CompleteInput(
                ps,
                "Get-P*",
                7,
                new Hashtable(),
                out int replIdx,
                out int replLen);

foreach (var r in results)
{
    Console.WriteLine($"{r.CompletionText}   // {r.ToolTip}");
}

Summary

  • Entry Point: The TabExpansion2 function in InitialSessionState.cs forwards Tab key presses to the completion engine.
  • Core Logic: CommandCompletion.CompleteInput in CommandCompletion.cs orchestrates the analysis and result generation.
  • Context Building: CompletionAnalysis parses the command line AST to determine what type of completion is needed.
  • Specialized Completers: CompletionCompleters contains the concrete implementations for commands, parameters, members, and variables.
  • Utilities: CompletionHelpers handles quote processing and wildcard matching, while CompletionResult defines the data contract for suggestions.
  • Extensibility: The Register-ArgumentCompleter command stores custom completers in CompletionContext.CustomArgumentCompleters for invocation during completion.

Frequently Asked Questions

What is the difference between TabExpansion and TabExpansion2?

TabExpansion2 is the modern completion entry point that replaced the legacy TabExpansion function. According to the PowerShell source code in InitialSessionState.cs, TabExpansion2 calls the managed [System.Management.Automation.CommandCompletion]::CompleteInput API, whereas the older TabExpansion relied primarily on script-based completion logic. All modern PowerShell hosts use TabExpansion2 to leverage the full AST-based completion engine.

How do I create a custom argument completer in PowerShell?

Use the Register-ArgumentCompleter cmdlet to bind a script block to specific commands or parameters. Your script block receives parameters including $wordToComplete, $commandAst, and $fakeBoundParameter, and must return CompletionResult objects. The engine caches these completers in the CustomArgumentCompleters dictionary and invokes them automatically when the user requests completion for registered commands.

Where are the built-in completers for commands and parameters defined?

Built-in completers live in the CompletionCompleters class within src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs. This file contains static methods like CompleteCommandParameter for parameter names, CompleteMember for properties and methods, and specialized handlers for variables and filenames. The class uses a switch statement on TokenKind to dispatch to the appropriate completer based on the cursor context.

Can I invoke PowerShell's completion engine from C# without running a script?

Yes, by calling CommandCompletion.CompleteInput statically from the System.Management.Automation namespace. Pass a PowerShell instance, the input string, cursor position, and optional completion options. The method returns a List<CompletionResult> containing the suggestions and outputs the replacementIndex and replacementLength to indicate which portion of the input should be replaced.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →