PSCompletion.psm1

Add-Type @"
using System;
using System.Management.Automation;
 
public class NativeCommandTreeNode
{
    private NativeCommandTreeNode(NativeCommandTreeNode[] subCommands)
    {
        SubCommands = subCommands;
    }
 
    public NativeCommandTreeNode(string command, NativeCommandTreeNode[] subCommands)
        : this(command, null, subCommands)
    {
    }
 
    public NativeCommandTreeNode(string command, string tooltip, NativeCommandTreeNode[] subCommands)
        : this(subCommands)
    {
        this.Command = command;
        this.Tooltip = tooltip;
    }
 
    public NativeCommandTreeNode(string command, string tooltip, bool argument)
        : this(null)
    {
        this.Command = command;
        this.Tooltip = tooltip;
        this.Argument = true;
    }
 
    public NativeCommandTreeNode(ScriptBlock completionGenerator, NativeCommandTreeNode[] subCommands)
        : this(subCommands)
    {
        this.CompletionGenerator = completionGenerator;
    }
 
    public string Command { get; private set; }
    public string Tooltip { get; private set; }
    public bool Argument { get; private set; }
    public ScriptBlock CompletionGenerator { get; private set; }
    public NativeCommandTreeNode[] SubCommands { get; private set; }
}
"@


function New-CompletionResult
{
    param([Parameter(Position=0, ValueFromPipelineByPropertyName, Mandatory, ValueFromPipeline)]
          [ValidateNotNullOrEmpty()]
          [string]
          $CompletionText,

          [Parameter(Position=1, ValueFromPipelineByPropertyName)]
          [string]
          $ToolTip,

          [Parameter(Position=2, ValueFromPipelineByPropertyName)]
          [string]
          $ListItemText,

          [System.Management.Automation.CompletionResultType]
          $CompletionResultType = [System.Management.Automation.CompletionResultType]::Command,
 
          [Parameter(Mandatory = $false)]
          [switch] $NoQuotes = $false
          )

    process
    {
        $toolTipToUse = if ($ToolTip -eq '') { $CompletionText } else { $ToolTip }
        $listItemToUse = if ($ListItemText -eq '') { $CompletionText } else { $ListItemText }

        # If the caller explicitly requests that quotes
        # not be included, via the -NoQuotes parameter,
        # then skip adding quotes.

        if ($CompletionResultType -eq [System.Management.Automation.CompletionResultType]::Command -and -not $NoQuotes)
        {
            # Add single quotes for the caller in case they are needed.
            # We use the parser to robustly determine how it will treat
            # the argument. If we end up with too many tokens, or if
            # the parser found something expandable in the results, we
            # know quotes are needed.

            $tokens = $null
            $null = [System.Management.Automation.Language.Parser]::ParseInput("echo $CompletionText", [ref]$tokens, [ref]$null)
            if ($tokens.Length -ne 3 -or
                ($tokens[1] -is [System.Management.Automation.Language.StringExpandableToken] -and
                 $tokens[1].Kind -eq [System.Management.Automation.Language.TokenKind]::Generic))
            {
                $CompletionText = "'$CompletionText'"
            }
        }
        return New-Object System.Management.Automation.CompletionResult `
            ($CompletionText,$listItemToUse,$CompletionResultType,$toolTipToUse.Trim())
    }

}

function New-CommandTree
{
    [CmdletBinding(DefaultParameterSetName='Default')]
    param(
        [Parameter(Position=0, Mandatory, ParameterSetName='Default')]
        [Parameter(Position=0, Mandatory, ParameterSetName='Argument')]
        [ValidateNotNullOrEmpty()]
        [string]
        $Completion,

        [Parameter(Position=1, Mandatory, ParameterSetName='Default')]
        [Parameter(Position=1, Mandatory, ParameterSetName='Argument')]
        [string]
        $Tooltip,

        [Parameter(ParameterSetName='Argument')]
        [switch]
        $Argument,

        [Parameter(Position=2, ParameterSetName='Default')]
        [Parameter(Position=1, ParameterSetName='ScriptBlockSet')]
        [scriptblock]
        $SubCommands,

        [Parameter(Position=0, Mandatory, ParameterSetName='ScriptBlockSet')]
        [scriptblock]
        $CompletionGenerator
    )

    $actualSubCommands = $null
    if ($null -ne $SubCommands)
    {
        $actualSubCommands = [NativeCommandTreeNode[]](& $SubCommands)
    }

    switch ($PSCmdlet.ParameterSetName)
    {
        'Default' {
            New-Object NativeCommandTreeNode $Completion,$Tooltip,$actualSubCommands
            break
        }
        'Argument' {
            New-Object NativeCommandTreeNode $Completion,$Tooltip,$true
        }
        'ScriptBlockSet' {
            New-Object NativeCommandTreeNode $CompletionGenerator,$actualSubCommands
            break
        }
    }
}

function Get-CommandTreeCompletion
{
    param($wordToComplete, $commandAst, [NativeCommandTreeNode[]]$CommandTree)

    $commandElements = $commandAst.CommandElements

    # Skip the first command element - it's the command name
    # Iterate through the remaining elements, stopping early
    # if we find the element that matches $wordToComplete.
    for ($i = 1; $i -lt $commandElements.Count; $i++)
    {
        if (!($commandElements[$i] -is [System.Management.Automation.Language.StringConstantExpressionAst]))
        {
            # Ignore arguments that are expressions. In some rare cases this
            # could cause strange completions because the context is incorrect, e.g.:
            # $c = 'advfirewall'
            # netsh $c firewall
            # Here we would be in advfirewall firewall context, but we'd complete as
            # though we were in firewall context.
            continue
        }

        if ($commandElements[$i].Value -eq $wordToComplete)
        {
            $CommandTree = $CommandTree |
                Where-Object { $_.Command -like "$wordToComplete*" -or $_.CompletionGenerator -ne $null }
            break
        }

        foreach ($subCommand in $CommandTree)
        {
            if ($subCommand.Command -eq $commandElements[$i].Value)
            {
                if (!$subCommand.Argument)
                {
                    $CommandTree = $subCommand.SubCommands
                }
                break
            }
        }
    }

    if ($null -ne $CommandTree)
    {
        $CommandTree | ForEach-Object {
            if ($_.Command)
            {
                $toolTip = if ($_.Tooltip) { $_.Tooltip } else { $_.Command }
                $type = if ($_.Argument) { [System.Management.Automation.CompletionResultType]::ParameterValue } else { [System.Management.Automation.CompletionResultType]::Command }
                New-CompletionResult -CompletionText $_.Command -ToolTip $toolTip -CompletionResultType $type
            }
            else
            {
                & $_.CompletionGenerator $wordToComplete $commandAst
            }
        }
    }
}

New-Alias -Name 'nct' -Value 'New-CommandTree'
Export-ModuleMember -Function @('New-CommandTree', 'Get-CommandTreeCompletion') -Alias @('nct')