completers/psinfo_completer/psinfo_completer.ps1

# psinfo tab completion for PowerShell
# Static-first native completer for PsInfo with safe filter and remote-target hints.

Set-StrictMode -Version 2.0

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

    $script:PsInfoCompletionCatalog = @{
        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 computer user name.'; TakesValue = $true; ValueKind = 'Password' }
            [pscustomobject]@{ Token = '-h'; Description = 'Show installed hotfixes.'; TakesValue = $false }
            [pscustomobject]@{ Token = '-s'; Description = 'Show installed software.'; TakesValue = $false }
            [pscustomobject]@{ Token = '-d'; Description = 'Show disk volume information.'; TakesValue = $false }
            [pscustomobject]@{ Token = '-c'; Description = 'Print in CSV format.'; TakesValue = $false }
            [pscustomobject]@{ Token = '-t'; Description = 'Delimiter used with -c. Use "\t" for tab.'; TakesValue = $true; ValueKind = 'Delimiter' }
            [pscustomobject]@{ Token = '-nobanner'; Description = 'Do not display the startup banner and copyright message.'; TakesValue = $false }
            [pscustomobject]@{ Token = '-accepteula'; Description = 'Suppress the Sysinternals license dialog.'; TakesValue = $false }
            [pscustomobject]@{ Token = '-?'; Description = 'Display PsInfo help.'; TakesValue = $false; Terminal = $true }
            [pscustomobject]@{ Token = '/?'; Description = 'Display PsInfo help.'; TakesValue = $false; Terminal = $true }
        )
        # PsInfo filters by field-label prefix; these are the labels PsInfo v1.79 prints.
        FilterHints = @(
            'uptime', 'kernel version', 'product type', 'product version', 'service pack', 'kernel build number',
            'registered organization', 'registered owner', 'ie version', 'system root',
            'processors', 'processor speed', 'processor type', 'physical memory', 'video driver'
        )
        # ',' ';' and '|' are PowerShell syntax, so every delimiter except \t must reach PsInfo quoted.
        DelimiterHints = @(',', ';', '|', ':', '\t')
    }
}

function New-PsInfoCompletionResult {
    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-PsInfoTypedQuote {
    param([string]$Value)
    if ($Value -match '^([''"\u2018-\u201E])') { return $Matches[1] }
    ''
}

function Remove-PsInfoOuterQuotes {
    # The value of a typed word. A word opened with a quote is read by the PowerShell
    # tokenizer, which drops the quotes and undoes that quote style's escapes; an
    # unterminated quote reads up to the cursor.
    param([string]$Value)
    if (-not (Get-PsInfoTypedQuote -Value $Value)) { return $Value }
    $tokens = $null
    $parseErrors = $null
    [void][System.Management.Automation.Language.Parser]::ParseInput($Value, [ref]$tokens, [ref]$parseErrors)
    [string]$tokens[0].Value
}

function ConvertTo-PsInfoQuotedValue {
    # Renders a value as one PowerShell argument: bare when safe and no quote is given,
    # otherwise in the given quote (single by default). Inside single quotes every
    # single-quote character is doubled; inside double quotes ` " $ and the typographic
    # double quotes take a backtick.
    param([string]$Value, [string]$Quote = '')
    if ([string]::IsNullOrEmpty($Value)) { return $Value }
    if (-not $Quote) {
        if ($Value -notmatch '[\s{}();,|&<>''"`$@#\u2018-\u201E]') { return $Value }
        $Quote = "'"
    }
    if ($Quote -match '^[''\u2018-\u201B]$') {
        return $Quote + ($Value -replace '([''\u2018-\u201B])', '$1$1') + $Quote
    }
    $Quote + ($Value -replace '([`"$\u201C-\u201E])', '`$1') + $Quote
}

function Get-PsInfoArgumentState {
    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 psinfo 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, 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 before the cursor are consumed; the token under the cursor and anything after it are not.
    $tokensBeforeCurrent = @($words | Where-Object { $_.End -lt $CursorPosition } | ForEach-Object { $_.Text })

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

function Get-PsInfoAtFileCompletions {
    param([string]$CurrentWord, [string]$SpanText, [bool]$StrandedAt = $false)
    $trimmed = Remove-PsInfoOuterQuotes -Value $CurrentWord
    if (-not $trimmed.StartsWith('@')) { return @() }
    $pathPortion = $trimmed.Substring(1)
    # Candidates keep the typed directory text verbatim ('.\', './', '..\', 'C:\x\'),
    # so nothing the user typed is dropped.
    $leaf = if ([string]::IsNullOrWhiteSpace($pathPortion) -or $pathPortion -match '[\\/]$') { '' } else { Split-Path -Path $pathPortion -Leaf }
    $typedDirectory = $pathPortion.Substring(0, $pathPortion.Length - $leaf.Length)
    $parent = if ([string]::IsNullOrWhiteSpace($typedDirectory)) { '.' } else { $typedDirectory }
    $quote = Get-PsInfoTypedQuote -Value $CurrentWord
    # The typed leaf is matched literally: '[' or '`' in it is not a wildcard.
    $items = @(
        if (Test-Path -LiteralPath $parent -PathType Container -ErrorAction Ignore) {
            Get-ChildItem -LiteralPath $parent -ErrorAction Ignore |
                Where-Object { $_.Name -like ([System.Management.Automation.WildcardPattern]::Escape($leaf) + '*') }
        }
    )

    $results = foreach ($item in $items) {
        $completionPath = $typedDirectory + $item.Name

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

        # The '@' is part of the value, so the word is always quoted ('@path'). A
        # stranded '@' stays in the line, and only '@(' keeps it valid: it passes
        # the quoted '@path' to psinfo as one argument.
        $completionText = if ($StrandedAt) {
            '(' + (ConvertTo-PsInfoQuotedValue -Value ('@' + $completionPath)) + ')'
        } else {
            ConvertTo-PsInfoQuotedValue -Value ('@' + $completionPath) -Quote $quote
        }
        New-PsInfoCompletionResult -CompletionText $completionText -ListItemText ('@' + $item.Name) -ResultType 'ParameterValue' -ToolTip 'File containing remote computer names for @file syntax.'
    }

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

    @($results)
}

function Complete-PsInfo {
    [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-PsInfoCompletionCatalog

    $state = Get-PsInfoArgumentState -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-PsInfoCompletionResult -CompletionText '@' -ResultType 'ParameterValue' -ToolTip 'File containing remote computer names for @file syntax.')
    }
    $currentWord = $state.CurrentWord
    $spanText = $state.SpanText
    $tokensBeforeCurrent = @($state.TokensBeforeCurrent)
    $switchLookup = @{}
    foreach ($spec in $script:PsInfoCompletionCatalog.Switches) { $switchLookup[$spec.Token.ToLowerInvariant()] = $spec }
    $used = @{}
    $valueContext = $null
    $remoteTarget = $null
    $filter = $null

    for ($i = 0; $i -lt $tokensBeforeCurrent.Count; $i++) {
        $token = $tokensBeforeCurrent[$i]
        $key = $token.ToLowerInvariant()
        if ($switchLookup.ContainsKey($key)) {
            $used[$key] = $true
            $spec = $switchLookup[$key]
            if ($spec.TakesValue) {
                if ($i -eq ($tokensBeforeCurrent.Count - 1)) { $valueContext = $spec.ValueKind; break }
                $i++
            }
            continue
        }

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

        if (-not $filter) { $filter = $token }
    }

    switch ($valueContext) {
        'User' {
            if (-not [string]::IsNullOrWhiteSpace($currentWord)) {
                return @(New-PsInfoCompletionResult -CompletionText $spanText -ResultType 'ParameterValue' -ToolTip 'Remote user name.')
            }
            return @(
                New-PsInfoCompletionResult -CompletionText '<username>' -ResultType 'ParameterValue' -ToolTip 'Remote user name.'
                New-PsInfoCompletionResult -CompletionText '<domain\user>' -ResultType 'ParameterValue' -ToolTip 'Remote user name in Domain\User syntax.'
            )
        }
        'Password' {
            return @(New-PsInfoCompletionResult -CompletionText $(if ([string]::IsNullOrWhiteSpace($currentWord)) { '<password>' } else { $spanText }) -ResultType 'ParameterValue' -ToolTip 'Remote password value.')
        }
        'Delimiter' {
            # Keep the quote the user typed; an untyped delimiter keeps this completer's double quotes.
            $delimiterQuote = Get-PsInfoTypedQuote -Value $currentWord
            # Only hints that extend the typed delimiter; any other typed delimiter is echoed, never replaced.
            $typedDelimiter = Remove-PsInfoOuterQuotes -Value $currentWord
            $delimiterHints = @($script:PsInfoCompletionCatalog.DelimiterHints | Where-Object { $_.StartsWith($typedDelimiter, [System.StringComparison]::Ordinal) })
            if ($delimiterHints.Count -eq 0) {
                return @(New-PsInfoCompletionResult -CompletionText $spanText -ResultType 'ParameterValue' -ToolTip 'Delimiter used with -c.')
            }
            return @($delimiterHints | ForEach-Object {
                $text = if ($_ -eq '\t' -and -not $delimiterQuote) { $_ } else { ConvertTo-PsInfoQuotedValue -Value $_ -Quote $(if ($delimiterQuote) { $delimiterQuote } else { '"' }) }
                New-PsInfoCompletionResult -CompletionText $text -ListItemText $_ -ResultType 'ParameterValue' -ToolTip 'Delimiter used with -c.'
            })
        }
    }

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

    $results = New-Object System.Collections.Generic.List[object]

    if (-not $remoteTarget) {
        foreach ($target in @('\\<computer>', '\\localhost', '\\*', '@file')) {
            if ([string]::IsNullOrWhiteSpace($currentWord) -or $target.StartsWith((Remove-PsInfoOuterQuotes -Value $currentWord), [System.StringComparison]::OrdinalIgnoreCase)) {
                [void]$results.Add((New-PsInfoCompletionResult -CompletionText $target -ResultType 'ParameterValue' -ToolTip 'Remote target placeholder for PsInfo.'))
            }
        }
    }

    foreach ($spec in $script:PsInfoCompletionCatalog.Switches) {
        $key = $spec.Token.ToLowerInvariant()
        if ($used.ContainsKey($key)) { continue }
        $isTerminal = [bool]($spec.PSObject.Properties['Terminal'] -and $spec.Terminal)
        if ($isTerminal -and $tokensBeforeCurrent.Count -gt 0) { continue }
        if (($key -in @('-u', '-p')) -and -not $remoteTarget) { continue }
        if ($key -eq '-t' -and -not $used.ContainsKey('-c')) { continue }
        if (-not [string]::IsNullOrWhiteSpace($currentWord) -and -not $spec.Token.StartsWith($currentWord, [System.StringComparison]::OrdinalIgnoreCase)) { continue }
        [void]$results.Add((New-PsInfoCompletionResult -CompletionText $spec.Token -ResultType 'ParameterName' -ToolTip $spec.Description))
    }

    if (-not $filter) {
        foreach ($hint in $script:PsInfoCompletionCatalog.FilterHints) {
            if ([string]::IsNullOrWhiteSpace($currentWord) -or $hint.StartsWith((Remove-PsInfoOuterQuotes -Value $currentWord), [System.StringComparison]::OrdinalIgnoreCase)) {
                # Keep the quote the user typed; an untyped multi-word label keeps this completer's double quotes.
                $hintQuote = Get-PsInfoTypedQuote -Value $currentWord
                if (-not $hintQuote -and $hint -match '\s') { $hintQuote = '"' }
                [void]$results.Add((New-PsInfoCompletionResult -CompletionText (ConvertTo-PsInfoQuotedValue -Value $hint -Quote $hintQuote) -ListItemText $hint -ResultType 'ParameterValue' -ToolTip 'PsInfo field label prefix; only the matching field is printed.'))
            }
        }
    }

    @($results.ToArray())
}


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