completers/psexec_completer/psexec_completer.ps1

# psexec tab completion for PowerShell
# Static-first native completer for PsExec with safe remote placeholders and conservative command-tail handling.

Set-StrictMode -Version 2.0

function Initialize-PsExecCompletionCatalog {
    if (Get-Variable -Name PsExecCompletionCatalog -Scope Script -ErrorAction Ignore) { return }

    $script:PsExecCompletionCatalog = @{
        Switches = @(
            [pscustomobject]@{ Token = '-u'; Description = 'Optional user name for login to the remote computer.'; TakesValue = $true; ValueKind = 'User' }
            [pscustomobject]@{ Token = '-p'; Description = 'Optional password for the remote user name.'; TakesValue = $true; ValueKind = 'Password' }
            [pscustomobject]@{ Token = '-n'; Description = 'Timeout in seconds connecting to remote computers.'; TakesValue = $true; ValueKind = 'Timeout' }
            [pscustomobject]@{ Token = '-r'; Description = 'Name of the remote service to create or interact with.'; TakesValue = $true; ValueKind = 'ServiceName' }
            [pscustomobject]@{ Token = '-h'; Description = 'Run with the account''s elevated token when available.'; TakesValue = $false }
            [pscustomobject]@{ Token = '-l'; Description = 'Run process as a limited user.'; TakesValue = $false }
            [pscustomobject]@{ Token = '-s'; Description = 'Run the remote process in the System account.'; TakesValue = $false }
            [pscustomobject]@{ Token = '-e'; Description = 'Do not load the specified account''s profile.'; TakesValue = $false }
            [pscustomobject]@{ Token = '-x'; Description = 'Display UI on the Winlogon secure desktop (local only).' ; TakesValue = $false }
            [pscustomobject]@{ Token = '-i'; Description = 'Run the program interactively in the specified session. The session id is optional.'; TakesValue = $true; ValueKind = 'Session'; OptionalValue = $true }
            [pscustomobject]@{ Token = '-c'; Description = 'Copy the specified program to the remote system for execution.'; TakesValue = $false }
            [pscustomobject]@{ Token = '-f'; Description = 'Copy the specified program even if it already exists remotely.'; TakesValue = $false }
            [pscustomobject]@{ Token = '-v'; Description = 'Copy only if the local file is newer or a higher version.'; TakesValue = $false }
            [pscustomobject]@{ Token = '-w'; Description = 'Set the remote working directory for the process.'; TakesValue = $true; ValueKind = 'RemoteDirectory' }
            [pscustomobject]@{ Token = '-d'; Description = 'Do not wait for process termination.'; TakesValue = $false }
            [pscustomobject]@{ Token = '-g'; Description = 'Set the primary thread processor group.'; TakesValue = $true; ValueKind = 'ProcessorGroup' }
            [pscustomobject]@{ Token = '-a'; Description = 'Restrict the application to the specified CPUs.'; TakesValue = $true; ValueKind = 'Affinity' }
            [pscustomobject]@{ Token = '-arm'; Description = 'Indicate that the remote computer is ARM architecture.'; TakesValue = $false }
            [pscustomobject]@{ Token = '-verbose'; Description = 'Enable verbose PsExec status output.'; TakesValue = $false }
            [pscustomobject]@{ Token = '-accepteula'; Description = 'Suppress the Sysinternals license dialog.'; TakesValue = $false }
            [pscustomobject]@{ Token = '-nobanner'; Description = 'Do not display the startup banner and copyright message.'; TakesValue = $false }
            [pscustomobject]@{ Token = '-low'; Description = 'Run the process at low priority.'; TakesValue = $false; Group = 'Priority' }
            [pscustomobject]@{ Token = '-belownormal'; Description = 'Run the process below normal priority.'; TakesValue = $false; Group = 'Priority' }
            [pscustomobject]@{ Token = '-abovenormal'; Description = 'Run the process above normal priority.'; TakesValue = $false; Group = 'Priority' }
            [pscustomobject]@{ Token = '-high'; Description = 'Run the process at high priority.'; TakesValue = $false; Group = 'Priority' }
            [pscustomobject]@{ Token = '-realtime'; Description = 'Run the process at realtime priority.'; TakesValue = $false; Group = 'Priority' }
            [pscustomobject]@{ Token = '-background'; Description = 'Run the process at low I/O and memory priority.'; TakesValue = $false; Group = 'Priority' }
            [pscustomobject]@{ Token = '-?'; Description = 'Display PsExec help.'; TakesValue = $false; Terminal = $true }
            [pscustomobject]@{ Token = '/?'; Description = 'Display PsExec help.'; TakesValue = $false; Terminal = $true }
        )
        TimeoutHints = @('1', '2', '5', '10', '30', '60')
        SessionHints = @('0', '1', '2', '<session-id>')
        GroupHints   = @('0', '1', '<group-id>')
        # Help: 'Separate processors ... with commas where 1 is the lowest
        # numbered CPU. For example, to run the application on CPU 2 and CPU 4,
        # enter: "-a 2,4"'. CPU 0 does not exist in that numbering.
        AffinityHints = @(@(1..([Math]::Min([Environment]::ProcessorCount, 8)) | ForEach-Object { [string]$_ }) + @('2,4', '<cpu-list>'))
    }
}

function Expand-PsExecHint {
    param([string[]]$Hints, [string]$CurrentWord, [string]$SpanText, [string]$ToolTip)

    $typed = Remove-PsExecOuterQuotes -Value $CurrentWord
    $matched = @(
        foreach ($hint in $Hints) {
            if ([string]::IsNullOrWhiteSpace($typed) -or $hint.StartsWith($typed, [System.StringComparison]::OrdinalIgnoreCase)) {
                New-PsExecCompletionResult -CompletionText $hint -ResultType 'ParameterValue' -ToolTip $ToolTip
            }
        }
    )

    if ($matched.Count -gt 0) { return $matched }

    # Never clobber what the user already typed with an unrelated hint.
    @(New-PsExecCompletionResult -CompletionText $SpanText -ResultType 'ParameterValue' -ToolTip $ToolTip)
}

function Split-PsExecPath {
    param([string]$Path)

    if ([string]::IsNullOrWhiteSpace($Path)) {
        return [pscustomobject]@{ Parent = '.'; Leaf = '' }
    }

    # Split-Path -Leaf resolves a '.' or '..' leaf to the folder's own name,
    # so those leaves are matched on the text instead.
    if ($Path -match '[\\/]+$|(^|[\\/])\.\.?$') {
        return [pscustomobject]@{ Parent = $Path; Leaf = '' }
    }

    # A drive-relative 'C:name' lives in that drive's current folder.
    if ($Path -match '^([A-Za-z]:)([^\\/]*)$') {
        return [pscustomobject]@{ Parent = $Matches[1]; Leaf = $Matches[2] }
    }

    $leaf = Split-Path -Path $Path -Leaf
    $parent = Split-Path -Path $Path -Parent
    if ([string]::IsNullOrWhiteSpace($parent)) { $parent = '.' }

    [pscustomobject]@{ Parent = $parent; Leaf = $leaf }
}

# The completed path keeps the folder text exactly as typed ('.\', '../', 'C:');
# a bare '.' or '..' gains a separator. A whole-word name with no folder that
# starts with a dash gets '.\', as PowerShell's own file completion does: a
# word starting with a dash is read as a parameter ('@-name' is not one).
function Get-PsExecCompletionPath {
    param([string]$Typed, [string]$Leaf, [System.IO.FileSystemInfo]$Item, [switch]$WholeWord)

    $sep = [System.IO.Path]::DirectorySeparatorChar
    $prefix = if ($Typed -match '^(.*[\\/]|[A-Za-z]:)') {
        $Matches[1]
    } elseif ($Typed -and -not $Leaf) {
        $Typed + $sep
    } elseif ($WholeWord -and $Item.Name -match '^[-\u2013-\u2015]') {
        '.' + $sep
    } else {
        ''
    }

    $completionPath = $prefix + $Item.Name
    if ($Item.PSIsContainer) { $completionPath += $sep }
    $completionPath
}

function New-PsExecCompletionResult {
    param(
        [string]$CompletionText,
        [string]$ResultType,
        [string]$ToolTip,
        [string]$ListItemText
    )

    if ([string]::IsNullOrWhiteSpace($ListItemText)) { $ListItemText = $CompletionText }
    if ([string]::IsNullOrWhiteSpace($ToolTip)) { $ToolTip = $CompletionText }

    [System.Management.Automation.CompletionResult]::new($CompletionText, $ListItemText, $ResultType, $ToolTip)
}

# PowerShell treats U+2018-U+201B as single quotes and U+201C-U+201E as double
# quotes, exactly like ' and ", so every quote test below covers both.
function Get-PsExecTypedQuote {
    param([string]$Value)
    if ($Value -match '^([''"\u2018-\u201E])') { return $Matches[1] }
    ''
}

function Remove-PsExecOuterQuotes {
    param([string]$Value)
    $quote = Get-PsExecTypedQuote -Value $Value
    if (-not $quote) { return $Value -replace '`(.)', '$1' }
    # An unterminated quote is one token running to the cursor: read the string
    # up to its closing quote, when present, and undo that quote style's escapes
    # (a doubled quote stands for its second character).
    $inner = $Value.Substring(1)
    if ($quote -match "['\u2018-\u201B]") {
        return [regex]::Match($inner, "^(?:[^'\u2018-\u201B]|['\u2018-\u201B]{2})*").Value -replace "['\u2018-\u201B](['\u2018-\u201B])", '$1'
    }
    [regex]::Match($inner, '^(?:[^`"\u201C-\u201E]|`.|["\u201C-\u201E]{2})*').Value -replace '`(.)|["\u201C-\u201E](["\u201C-\u201E])', '$1$2'
}

function ConvertTo-PsExecQuotedValue {
    param([string]$Value, [string]$Quote = '')
    if ([string]::IsNullOrWhiteSpace($Value)) { return $Value }
    if (-not $Quote -and $Value -notmatch '[\s{}();,|&<>''"`$@#\u2018-\u201E]|^[-\u2013-\u2015]') { return $Value }
    # Keep the quote the user typed (single by default). Inside single quotes
    # every single-quote character is doubled; inside double quotes ` " $ and
    # the typographic double quotes take a backtick.
    if (-not $Quote) { $Quote = "'" }
    if ($Quote -match "['\u2018-\u201B]") {
        return $Quote + ($Value -replace "(['\u2018-\u201B])", '$1$1') + $Quote
    }
    $Quote + ($Value -replace '([`$"\u201C-\u201E])', '`$1') + $Quote
}

function Get-PsExecArgumentState {
    param([System.Management.Automation.Language.CommandAst]$CommandAst, [int]$CursorPosition)

    # A bare '@' is an unrecognized token, so the parser splits '@.\hosts.txt'
    # into '@' and '.\hosts.txt'; glue it back onto the element it touches so
    # the word is the one psexec will see.
    $words = New-Object System.Collections.Generic.List[object]
    foreach ($element in @($CommandAst.CommandElements | Select-Object -Skip 1)) {
        $previous = if ($words.Count -gt 0) { $words[$words.Count - 1] } else { $null }
        if ($previous -and $previous.Text -eq '@' -and -not $previous.At -and $previous.End -eq $element.Extent.StartOffset) {
            $previous.Text = '@' + $element.Extent.Text
            $previous.End = $element.Extent.EndOffset
            $previous.At = $true
            continue
        }
        [void]$words.Add([pscustomobject]@{ Start = $element.Extent.StartOffset; End = $element.Extent.EndOffset; Text = $element.Extent.Text; At = $false })
    }

    # The word under the cursor is read from the parser, never from
    # $wordToComplete (a registered completer receives it with '$name'
    # expanded): the element that contains the cursor, cut at the cursor. An
    # unterminated quote is one element. SpanText is the text PowerShell's
    # replacement span covers: the whole element, even when the cursor is
    # mid-word, so an echo of it changes nothing. For a glued '@' the span is
    # only the path after it (StrandedAt, results must keep that '@' valid), or
    # only the '@' itself when the cursor sits right after it (AtOnly).
    $currentWord = ''
    $spanText = ''
    $strandedAt = $false
    $atOnly = $false
    foreach ($word in $words) {
        if ($word.Start -lt $CursorPosition -and $word.End -ge $CursorPosition) {
            $currentWord = $word.Text.Substring(0, $CursorPosition - $word.Start)
            $spanText = $word.Text
            if ($word.At) {
                $atOnly = $CursorPosition -eq $word.Start + 1
                $strandedAt = -not $atOnly
                $spanText = if ($atOnly) { '@' } else { $word.Text.Substring(1) }
            }
            break
        }
    }

    # Only elements that end at or before the cursor have actually been typed
    # to the left of it; anything further right must not classify this slot.
    $tokens = @($words |
        Where-Object { $_.End -le $CursorPosition } |
        ForEach-Object { $_.Text })
    $tokensBeforeCurrent = @($tokens)
    if (-not [string]::IsNullOrEmpty($currentWord) -and $tokensBeforeCurrent.Count -gt 0 -and $tokensBeforeCurrent[-1] -eq $currentWord) {
        $tokensBeforeCurrent = @($tokensBeforeCurrent | Select-Object -First ($tokensBeforeCurrent.Count - 1))
    }

    [pscustomobject]@{
        CurrentWord         = $currentWord
        SpanText            = $spanText
        StrandedAt          = $strandedAt
        AtOnly              = $atOnly
        TokensBeforeCurrent = $tokensBeforeCurrent
    }
}

function Get-PsExecAtFileCompletions {
    param([string]$CurrentWord, [string]$SpanText, [bool]$StrandedAt = $false)

    $trimmed = Remove-PsExecOuterQuotes -Value $CurrentWord
    if (-not $trimmed.StartsWith('@')) { return @() }
    $pathPortion = $trimmed.Substring(1)
    $parts = Split-PsExecPath -Path $pathPortion
    $parent = $parts.Parent
    $leaf = $parts.Leaf
    $filter = if ([string]::IsNullOrWhiteSpace($leaf)) { '*' } else { "$leaf*" }
    $items = @(Get-ChildItem -LiteralPath $parent -Filter $filter -ErrorAction Ignore)
    $quote = Get-PsExecTypedQuote -Value $CurrentWord
    $results = New-Object System.Collections.Generic.List[object]

    foreach ($item in $items) {
        $completionPath = Get-PsExecCompletionPath -Typed $pathPortion -Leaf $leaf -Item $item

        # A stranded '@' stays in the line, and only '@(' keeps it valid: it
        # passes the quoted '@path' to psexec as one argument.
        $completionText = if ($StrandedAt) {
            '(' + (ConvertTo-PsExecQuotedValue -Value ('@' + $completionPath)) + ')'
        } else {
            ConvertTo-PsExecQuotedValue -Value ('@' + $completionPath) -Quote $quote
        }
        [void]$results.Add((New-PsExecCompletionResult -CompletionText $completionText -ListItemText ('@' + $item.Name) -ResultType 'ParameterValue' -ToolTip 'File containing remote computer names for @file syntax.'))
    }

    if ($results.Count -eq 0) {
        return @(
            New-PsExecCompletionResult -CompletionText $SpanText -ResultType 'ParameterValue' -ToolTip 'File containing remote computer names for @file syntax.'
        )
    }

    @($results.ToArray())
}

function Get-PsExecExecutableCompletions {
    param([string]$CurrentWord, [string]$SpanText)

    $trimmed = Remove-PsExecOuterQuotes -Value $CurrentWord
    $quote = Get-PsExecTypedQuote -Value $CurrentWord
    $results = New-Object System.Collections.Generic.List[object]

    if ([string]::IsNullOrWhiteSpace($trimmed)) {
        foreach ($sample in @('cmd.exe', 'powershell.exe', 'pwsh.exe')) {
            [void]$results.Add((New-PsExecCompletionResult -CompletionText (ConvertTo-PsExecQuotedValue -Value $sample -Quote $quote) -ResultType 'ParameterValue' -ToolTip 'Local executable or script to copy and run with -c.'))
        }
    }

    if ($trimmed.StartsWith('\\')) {
        # A UNC copy source is never enumerated: resolving the share blocks the
        # completion thread on SMB name resolution for seconds when the host is
        # unknown, and leaves a Get-ChildItem record in $Error.
        [void]$results.Add((New-PsExecCompletionResult -CompletionText $SpanText -ResultType 'ParameterValue' -ToolTip 'Network path to copy with -c; UNC paths are not enumerated during completion.'))
        return @($results.ToArray())
    }

    if ($trimmed -match '[\\/]|^\.' -or $trimmed -match '^[A-Za-z]:') {
        $parts = Split-PsExecPath -Path $trimmed
        $parent = $parts.Parent
        $leaf = $parts.Leaf
        $filter = if ([string]::IsNullOrWhiteSpace($leaf)) { '*' } else { "$leaf*" }
        foreach ($item in @(Get-ChildItem -LiteralPath $parent -Filter $filter -ErrorAction Ignore)) {
            $completionPath = Get-PsExecCompletionPath -Typed $trimmed -Leaf $leaf -Item $item -WholeWord
            [void]$results.Add((New-PsExecCompletionResult -CompletionText (ConvertTo-PsExecQuotedValue -Value $completionPath -Quote $quote) -ResultType $(if ($item.PSIsContainer) { 'ProviderContainer' } else { 'ParameterValue' }) -ToolTip $item.FullName))
        }
    } else {
        foreach ($command in @(Get-Command -Name "$trimmed*" -CommandType Application -ErrorAction SilentlyContinue | Sort-Object -Property Name -Unique | Select-Object -First 20)) {
            [void]$results.Add((New-PsExecCompletionResult -CompletionText (ConvertTo-PsExecQuotedValue -Value $command.Name -Quote $quote) -ResultType 'ParameterValue' -ToolTip ($command.Source ? $command.Source : $command.Name)))
        }
    }

    if ($results.Count -eq 0) {
        $completion = if ([string]::IsNullOrWhiteSpace($SpanText)) { '<local-command>' } else { $SpanText }
        return @(
            New-PsExecCompletionResult -CompletionText $completion -ResultType 'ParameterValue' -ToolTip 'Local executable or script to copy and run with -c.'
        )
    }

    $seen = @{}
    $unique = foreach ($item in $results) {
        if ($seen.ContainsKey($item.CompletionText)) { continue }
        $seen[$item.CompletionText] = $true
        $item
    }
    @($unique)
}

function Complete-PsExec {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'WordToComplete', Justification = 'The word is cut from the CommandAst element at the cursor; a registered completer receives WordToComplete with $name expanded.')]
    param(
        [string]$WordToComplete,
        [System.Management.Automation.Language.CommandAst]$CommandAst,
        [int]$CursorPosition
    )

    Initialize-PsExecCompletionCatalog

    $state = Get-PsExecArgumentState -CommandAst $CommandAst -CursorPosition $CursorPosition
    # Tab between a bare '@' and the path after it replaces only the '@':
    # anything else would fuse with the typed path, so keep the line as it is.
    if ($state.AtOnly) {
        return @(New-PsExecCompletionResult -CompletionText '@' -ResultType 'ParameterValue' -ToolTip 'File containing remote computer names for @file syntax.')
    }
    $currentWord = $state.CurrentWord
    $spanText = $state.SpanText
    $tokensBeforeCurrent = @($state.TokensBeforeCurrent)
    $usedSwitches = @{}
    $valueContext = $null
    $remoteTarget = $null
    $commandToken = $null
    $commandTail = @()
    $copyMode = $false
    $prioritySelected = $false

    $switchLookup = @{}
    foreach ($spec in $script:PsExecCompletionCatalog.Switches) {
        $switchLookup[$spec.Token.ToLowerInvariant()] = $spec
    }

    for ($i = 0; $i -lt $tokensBeforeCurrent.Count; $i++) {
        $token = $tokensBeforeCurrent[$i]
        $key = $token.ToLowerInvariant()

        if ($switchLookup.ContainsKey($key)) {
            $usedSwitches[$key] = $true
            $spec = $switchLookup[$key]
            if ($spec.PSObject.Properties['Group'] -and $spec.Group -eq 'Priority') { $prioritySelected = $true }
            if ($key -eq '-c') { $copyMode = $true }
            if ($spec.TakesValue) {
                # '-i [session]' takes an OPTIONAL id, so only a number consumes
                # the next token; a switch, target or program stays what it is.
                $optional = [bool]($spec.PSObject.Properties['OptionalValue'] -and $spec.OptionalValue)
                if ($i -eq ($tokensBeforeCurrent.Count - 1)) {
                    if (-not $optional -or [string]::IsNullOrEmpty($currentWord) -or $currentWord -match '^\d+$') {
                        $valueContext = $spec.ValueKind
                        break
                    }
                    continue
                }

                if (-not $optional -or $tokensBeforeCurrent[$i + 1] -match '^\d+$') {
                    $i++
                }
            }
            continue
        }

        if (-not $remoteTarget -and ((Remove-PsExecOuterQuotes -Value $token).StartsWith('\\') -or (Remove-PsExecOuterQuotes -Value $token).StartsWith('@'))) {
            $remoteTarget = $token
            continue
        }

        if (-not $commandToken) {
            $commandToken = $token
            continue
        }

        $commandTail += $token
    }

    switch ($valueContext) {
        'User' {
            return @(Expand-PsExecHint -Hints @('<username>', '<domain\user>', "$env:USERDOMAIN\$env:USERNAME") -CurrentWord $currentWord -SpanText $spanText -ToolTip 'Remote user name, Domain\User syntax when network access is needed.')
        }
        'Password' {
            $value = if ([string]::IsNullOrWhiteSpace($spanText)) { '<password>' } else { $spanText }
            return @(New-PsExecCompletionResult -CompletionText $value -ResultType 'ParameterValue' -ToolTip 'Remote password value.')
        }
        'Timeout' {
            return @(Expand-PsExecHint -Hints $script:PsExecCompletionCatalog.TimeoutHints -CurrentWord $currentWord -SpanText $spanText -ToolTip 'Remote connection timeout in seconds.')
        }
        'ServiceName' {
            return @(Expand-PsExecHint -Hints @('PSEXESVC', '<service-name>') -CurrentWord $currentWord -SpanText $spanText -ToolTip 'Remote service name for PsExec (PSEXESVC is the default).')
        }
        'Session' {
            return @(Expand-PsExecHint -Hints $script:PsExecCompletionCatalog.SessionHints -CurrentWord $currentWord -SpanText $spanText -ToolTip 'Interactive session number for -i.')
        }
        'RemoteDirectory' {
            $value = if ([string]::IsNullOrWhiteSpace($spanText)) { '<remote-directory>' } else { $spanText }
            return @(New-PsExecCompletionResult -CompletionText $value -ResultType 'ParameterValue' -ToolTip 'Remote working directory path for -w.')
        }
        'ProcessorGroup' {
            return @(Expand-PsExecHint -Hints $script:PsExecCompletionCatalog.GroupHints -CurrentWord $currentWord -SpanText $spanText -ToolTip 'Processor group number for -g.')
        }
        'Affinity' {
            return @(Expand-PsExecHint -Hints $script:PsExecCompletionCatalog.AffinityHints -CurrentWord $currentWord -SpanText $spanText -ToolTip 'Comma-separated CPU list for -a, where 1 is the lowest numbered CPU.')
        }
    }

    if ((Remove-PsExecOuterQuotes -Value $currentWord).StartsWith('@')) {
        return Get-PsExecAtFileCompletions -CurrentWord $currentWord -SpanText $spanText -StrandedAt $state.StrandedAt
    }

    if ($copyMode -and -not $commandToken -and [string]::IsNullOrEmpty($currentWord)) {
        return Get-PsExecExecutableCompletions -CurrentWord $currentWord -SpanText $spanText
    }

    if ($commandToken) {
        if ($copyMode -and -not $commandTail) {
            return Get-PsExecExecutableCompletions -CurrentWord $currentWord -SpanText $spanText
        }

        $argumentValue = if ([string]::IsNullOrWhiteSpace($spanText)) { '<argument>' } else { $spanText }
        return @(New-PsExecCompletionResult -CompletionText $argumentValue -ResultType 'ParameterValue' -ToolTip 'Command tail is intentionally conservative and non-enumerating.')
    }

    if (-not [string]::IsNullOrWhiteSpace($currentWord) -and
        -not $currentWord.StartsWith('-') -and
        -not $currentWord.StartsWith('/') -and
        -not $currentWord.StartsWith('\')) {
        # 'If you omit the computer name PsExec runs the application on the
        # local system', so the local-run command slot is a local program.
        if ($copyMode -or -not $remoteTarget) {
            return Get-PsExecExecutableCompletions -CurrentWord $currentWord -SpanText $spanText
        }

        return @(
            New-PsExecCompletionResult -CompletionText $spanText -ResultType 'ParameterValue' -ToolTip 'Remote command or command path.'
            New-PsExecCompletionResult -CompletionText '<command>' -ResultType 'ParameterValue' -ToolTip 'Remote command or command path.'
        )
    }

    $results = New-Object System.Collections.Generic.List[object]
    $typedTarget = Remove-PsExecOuterQuotes -Value $currentWord
    if (-not $remoteTarget) {
        foreach ($target in @('\\<computer>', "\\$env:COMPUTERNAME", '\\localhost', '\\*', '@file')) {
            if ([string]::IsNullOrWhiteSpace($currentWord) -or $target.StartsWith($typedTarget, [System.StringComparison]::OrdinalIgnoreCase)) {
                [void]$results.Add((New-PsExecCompletionResult -CompletionText $target -ResultType 'ParameterValue' -ToolTip 'PsExec remote target placeholder.'))
            }
        }
    }

    foreach ($spec in $script:PsExecCompletionCatalog.Switches) {
        $key = $spec.Token.ToLowerInvariant()
        $isTerminal = [bool]($spec.PSObject.Properties['Terminal'] -and $spec.Terminal)
        $isPriority = [bool]($spec.PSObject.Properties['Group'] -and $spec.Group -eq 'Priority')
        if ($isTerminal -and $tokensBeforeCurrent.Count -gt 0) { continue }
        if ($usedSwitches.ContainsKey($key)) { continue }
        if (($key -in @('-f', '-v')) -and -not $copyMode) { continue }
        if (($key -in @('-u', '-p')) -and -not $remoteTarget) { continue }
        if (($key -eq '-s' -and $usedSwitches.ContainsKey('-e')) -or ($key -eq '-e' -and $usedSwitches.ContainsKey('-s'))) { continue }
        if ($isPriority -and $prioritySelected) { continue }
        if (-not [string]::IsNullOrWhiteSpace($currentWord) -and -not $spec.Token.StartsWith($currentWord, [System.StringComparison]::OrdinalIgnoreCase)) { continue }
        [void]$results.Add((New-PsExecCompletionResult -CompletionText $spec.Token -ResultType 'ParameterName' -ToolTip $spec.Description))
    }

    if ($copyMode -and -not $currentWord.StartsWith('-')) {
        [void]$results.AddRange(@(Get-PsExecExecutableCompletions -CurrentWord $currentWord -SpanText $spanText))
    }

    if ($results.Count -eq 0 -and ($typedTarget.StartsWith('\') -or $typedTarget.StartsWith('/'))) {
        # A partially typed UNC target or slash form that matches no placeholder:
        # echo it back rather than proposing a command in a slot that cannot
        # hold one.
        $echoToolTip = if ($typedTarget.StartsWith('\')) { 'Remote computer name.' } else { 'PsExec switch or command.' }
        [void]$results.Add((New-PsExecCompletionResult -CompletionText $spanText -ResultType 'ParameterValue' -ToolTip $echoToolTip))
    }

    @($results.ToArray())
}


Register-ArgumentCompleter -Native -CommandName @('psexec', 'psexec.exe') -ScriptBlock {
    param($wordToComplete, $commandAst, $cursorPosition)
    Complete-PsExec -WordToComplete $wordToComplete -CommandAst $commandAst -CursorPosition $cursorPosition
}