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]$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 $CurrentWord -ResultType 'ParameterValue' -ToolTip $ToolTip)
}

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

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

    if ($Path -match '[\\/]+$') {
        return [pscustomobject]@{ Parent = $Path; Leaf = '' }
    }

    $leaf = Split-Path -Path $Path -Leaf
    if ($leaf -in @('.', '..')) {
        return [pscustomobject]@{ Parent = $Path; Leaf = '' }
    }

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

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

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)
}

function Get-PsExecCurrentToken {
    param([string]$Line, [int]$CursorPosition, [string]$Fallback)

    if ([string]::IsNullOrWhiteSpace($Line)) { return $Fallback }
    $safeCursor = [Math]::Min([Math]::Max($CursorPosition, 0), $Line.Length)
    $prefix = $Line.Substring(0, $safeCursor)
    if ($prefix -match '\s$') { return '' }
    $parts = @([regex]::Matches($prefix, '"[^"]*"|\S+') | ForEach-Object { $_.Value })
    if ($parts.Count -gt 0) { return $parts[-1] }
    $Fallback
}

function Remove-PsExecOuterQuotes {
    param([string]$Value)
    if ([string]::IsNullOrEmpty($Value)) { return '' }
    if ($Value.Length -ge 2 -and $Value.StartsWith('"') -and $Value.EndsWith('"')) {
        return $Value.Substring(1, $Value.Length - 2)
    }
    $Value.TrimStart('"')
}

function ConvertTo-PsExecQuotedValue {
    param([string]$Value, [bool]$AlwaysQuote = $false)
    if ([string]::IsNullOrWhiteSpace($Value)) { return $Value }
    if (($AlwaysQuote -or $Value -match '\s') -and -not ($Value.StartsWith('"') -and $Value.EndsWith('"'))) {
        return '"' + $Value.Replace('`', '``').Replace('"', '`"') + '"'
    }
    $Value
}

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

    $currentWord = if ([string]::IsNullOrEmpty($WordToComplete)) {
        ''
    } else {
        Get-PsExecCurrentToken -Line $CommandAst.Extent.Text -CursorPosition ($CursorPosition - $CommandAst.Extent.StartOffset) -Fallback $WordToComplete
    }

    # 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 = @($CommandAst.CommandElements |
        Select-Object -Skip 1 |
        Where-Object { $_.Extent.EndOffset -le $CursorPosition } |
        ForEach-Object { $_.Extent.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
        TokensBeforeCurrent = $tokensBeforeCurrent
    }
}

function Get-PsExecAtFileCompletions {
    param([string]$CurrentWord)

    $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 -Path $parent -Filter $filter -ErrorAction SilentlyContinue)
    $alwaysQuote = -not [string]::IsNullOrEmpty($CurrentWord) -and $CurrentWord.StartsWith('"')
    $results = New-Object System.Collections.Generic.List[object]

    foreach ($item in $items) {
        $completionPath = if ($pathPortion -and -not [System.IO.Path]::IsPathRooted($pathPortion) -and $parent -ne '.') {
            Join-Path -Path $parent -ChildPath $item.Name
        } elseif ($parent -eq '.') {
            $item.Name
        } else {
            $item.FullName
        }

        if ($item.PSIsContainer -and -not $completionPath.EndsWith([System.IO.Path]::DirectorySeparatorChar)) {
            $completionPath += [System.IO.Path]::DirectorySeparatorChar
        }

        [void]$results.Add((New-PsExecCompletionResult -CompletionText (ConvertTo-PsExecQuotedValue -Value ('@' + $completionPath) -AlwaysQuote:$alwaysQuote) -ListItemText ('@' + $item.Name) -ResultType 'ParameterValue' -ToolTip 'File containing remote computer names for @file syntax.'))
    }

    if ($results.Count -eq 0) {
        $placeholder = if ([string]::IsNullOrWhiteSpace($CurrentWord)) { '@file' } else { $CurrentWord }
        return @(
            New-PsExecCompletionResult -CompletionText $placeholder -ResultType 'ParameterValue' -ToolTip 'File containing remote computer names for @file syntax.'
        )
    }

    @($results.ToArray())
}

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

    $trimmed = Remove-PsExecOuterQuotes -Value $CurrentWord
    $alwaysQuote = -not [string]::IsNullOrEmpty($CurrentWord) -and $CurrentWord.StartsWith('"')
    $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 $sample -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 $CurrentWord -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 -Path $parent -Filter $filter -ErrorAction SilentlyContinue)) {
            $completionPath = if ($trimmed -and -not [System.IO.Path]::IsPathRooted($trimmed) -and $parent -ne '.') {
                Join-Path -Path $parent -ChildPath $item.Name
            } elseif ($parent -eq '.') {
                $item.Name
            } else {
                $item.FullName
            }

            if ($item.PSIsContainer -and -not $completionPath.EndsWith([System.IO.Path]::DirectorySeparatorChar)) {
                $completionPath += [System.IO.Path]::DirectorySeparatorChar
            }

            [void]$results.Add((New-PsExecCompletionResult -CompletionText (ConvertTo-PsExecQuotedValue -Value $completionPath -AlwaysQuote:$alwaysQuote) -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 $command.Name -ResultType 'ParameterValue' -ToolTip ($command.Source ? $command.Source : $command.Name)))
        }
    }

    if ($results.Count -eq 0) {
        $completion = if ([string]::IsNullOrWhiteSpace($CurrentWord)) { '<local-command>' } else { $CurrentWord }
        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 {
    param(
        [string]$WordToComplete,
        [System.Management.Automation.Language.CommandAst]$CommandAst,
        [int]$CursorPosition
    )

    Initialize-PsExecCompletionCatalog

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

    if ($currentWord.StartsWith('@') -or $currentWord.StartsWith('"@')) {
        return Get-PsExecAtFileCompletions -CurrentWord $currentWord
    }

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

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

        $argumentValue = if ([string]::IsNullOrWhiteSpace($currentWord)) { '<argument>' } else { $currentWord }
        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
        }

        return @(
            New-PsExecCompletionResult -CompletionText $currentWord -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))
    }

    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 $currentWord -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
}