completers/attrib_completer/attrib_completer.ps1

<#
.SYNOPSIS
    Registers a native PowerShell argument completer for attrib.

.DESCRIPTION
    Provides a static-first native completer for `attrib` and `attrib.exe`.

    The completer covers:
    - whole-token attribute toggles such as `+R` and `-H`
    - slash-style switches `/S`, `/D`, `/L`, and `/?`
    - local filesystem path and wildcard operand completion

    The script keeps its top level compatible with `Import-CompleterScript`.
#>


Set-StrictMode -Version 2.0

function New-AttribCompletionResult {
    param(
        [string]$CompletionText,
        [string]$ResultType = 'ParameterValue',
        [string]$ToolTip,
        [string]$ListItemText
    )

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

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

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

function Test-AttribStartsWith {
    param(
        [string]$Candidate,
        [string]$Prefix
    )

    [string]::IsNullOrEmpty($Prefix) -or $Candidate.StartsWith($Prefix, [System.StringComparison]::OrdinalIgnoreCase)
}

function Get-AttribAttributeSpecs {
    @(
        [pscustomobject]@{ Token = '+R'; Description = 'Sets the read-only file attribute.' }
        [pscustomobject]@{ Token = '-R'; Description = 'Clears the read-only file attribute.' }
        [pscustomobject]@{ Token = '+A'; Description = 'Sets the archive file attribute.' }
        [pscustomobject]@{ Token = '-A'; Description = 'Clears the archive file attribute.' }
        [pscustomobject]@{ Token = '+S'; Description = 'Sets the system file attribute.' }
        [pscustomobject]@{ Token = '-S'; Description = 'Clears the system file attribute.' }
        [pscustomobject]@{ Token = '+H'; Description = 'Sets the hidden file attribute.' }
        [pscustomobject]@{ Token = '-H'; Description = 'Clears the hidden file attribute.' }
        [pscustomobject]@{ Token = '+O'; Description = 'Sets the offline attribute.' }
        [pscustomobject]@{ Token = '-O'; Description = 'Clears the offline attribute.' }
        [pscustomobject]@{ Token = '+I'; Description = 'Sets the not-content-indexed file attribute.' }
        [pscustomobject]@{ Token = '-I'; Description = 'Clears the not-content-indexed file attribute.' }
        [pscustomobject]@{ Token = '+X'; Description = 'Sets the no-scrub file attribute.' }
        [pscustomobject]@{ Token = '-X'; Description = 'Clears the no-scrub file attribute.' }
        [pscustomobject]@{ Token = '+P'; Description = 'Sets the pinned attribute.' }
        [pscustomobject]@{ Token = '-P'; Description = 'Clears the pinned attribute.' }
        [pscustomobject]@{ Token = '+U'; Description = 'Sets the unpinned attribute.' }
        [pscustomobject]@{ Token = '-U'; Description = 'Clears the unpinned attribute.' }
        [pscustomobject]@{ Token = '+B'; Description = 'Sets the SMR blob attribute.' }
        [pscustomobject]@{ Token = '-B'; Description = 'Clears the SMR blob attribute.' }
        [pscustomobject]@{ Token = '+V'; Description = 'Sets the integrity attribute.' }
        [pscustomobject]@{ Token = '-V'; Description = 'Clears the integrity attribute.' }
    )
}

function Get-AttribSlashSwitchSpecs {
    @(
        [pscustomobject]@{ Token = '/S'; Description = 'Processes matching files in the current folder and all subfolders.' }
        [pscustomobject]@{ Token = '/D'; Description = 'Processes folders as well.' }
        [pscustomobject]@{ Token = '/L'; Description = 'Works on symbolic link attributes instead of the target.' }
        [pscustomobject]@{ Token = '/?'; Description = 'Displays attrib help.' }
    )
}

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

    if ($null -eq $Line) {
        $Line = ''
    }

    $safeCursor = [Math]::Min([Math]::Max($CursorPosition, 0), $Line.Length)
    $prefix = $Line.Substring(0, $safeCursor)
    $tokens = New-Object System.Collections.Generic.List[string]
    $builder = New-Object System.Text.StringBuilder
    $quoteChar = [char]0

    foreach ($character in $prefix.ToCharArray()) {
        if (($character -eq [char]34) -or ($character -eq [char]39)) {
            if ($quoteChar -eq [char]0) {
                $quoteChar = $character
            } elseif ($quoteChar -eq $character) {
                $quoteChar = [char]0
            }

            [void]$builder.Append($character)
            continue
        }

        if ([char]::IsWhiteSpace($character) -and $quoteChar -eq [char]0) {
            if ($builder.Length -gt 0) {
                $tokens.Add($builder.ToString())
                [void]$builder.Clear()
            }

            continue
        }

        [void]$builder.Append($character)
    }

    $hasTrailingSpace = ($quoteChar -eq [char]0) -and ($prefix -match '\s$')
    if ($builder.Length -gt 0) {
        $tokens.Add($builder.ToString())
    }

    if ($hasTrailingSpace) {
        return [pscustomobject]@{
            CurrentToken = ''
        }
    }

    return [pscustomobject]@{
        CurrentToken = if ($tokens.Count -gt 0) { $tokens[$tokens.Count - 1] } else { '' }
    }
}

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

    if ($null -eq $CommandAst) {
        return [pscustomobject]@{
            Line           = ''
            CursorPosition = 0
        }
    }

    $line = if ($null -ne $CommandAst.Extent -and $null -ne $CommandAst.Extent.Text) {
        $CommandAst.Extent.Text
    } else {
        $CommandAst.ToString()
    }

    if ($null -eq $line) {
        $line = ''
    }

    $startOffset = if ($null -ne $CommandAst.Extent) { $CommandAst.Extent.StartOffset } else { 0 }
    $relativeCursor = [Math]::Max($CursorPosition - $startOffset, 0)
    $trailingWhitespaceLength = $relativeCursor - $line.Length

    if ($trailingWhitespaceLength -gt 0) {
        $line += ' ' * $trailingWhitespaceLength
    }

    [pscustomobject]@{
        Line           = $line
        CursorPosition = $relativeCursor
    }
}

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

    $results = New-Object System.Collections.Generic.List[System.Management.Automation.CompletionResult]
    $sign = if ($CurrentWord.StartsWith('-')) { '-' } else { '+' }

    foreach ($spec in Get-AttribAttributeSpecs) {
        if (-not $spec.Token.StartsWith($sign, [System.StringComparison]::OrdinalIgnoreCase)) {
            continue
        }

        if (Test-AttribStartsWith -Candidate $spec.Token -Prefix $CurrentWord) {
            [void]$results.Add((New-AttribCompletionResult -CompletionText $spec.Token -ResultType 'ParameterName' -ToolTip $spec.Description))
        }
    }

    if ($results.Count -eq 0) {
        [void]$results.Add((New-AttribCompletionResult -CompletionText $CurrentWord -ResultType 'ParameterName' -ToolTip 'Attribute toggle.'))
    }

    @($results.ToArray())
}

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

    $results = New-Object System.Collections.Generic.List[System.Management.Automation.CompletionResult]

    foreach ($spec in Get-AttribSlashSwitchSpecs) {
        if (Test-AttribStartsWith -Candidate $spec.Token -Prefix $CurrentWord) {
            [void]$results.Add((New-AttribCompletionResult -CompletionText $spec.Token -ResultType 'ParameterName' -ToolTip $spec.Description))
        }
    }

    if ($results.Count -eq 0) {
        [void]$results.Add((New-AttribCompletionResult -CompletionText $CurrentWord -ResultType 'ParameterName' -ToolTip 'attrib slash switch.'))
    }

    @($results.ToArray())
}

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

    $results = New-Object System.Collections.Generic.List[System.Management.Automation.CompletionResult]
    $inputPath = if ($null -eq $CurrentWord) { '' } else { $CurrentWord }

    foreach ($item in [System.Management.Automation.CompletionCompleters]::CompleteFilename($inputPath)) {
        [void]$results.Add([System.Management.Automation.CompletionResult]::new(
                $item.CompletionText,
                $item.CompletionText,
                $item.ResultType,
                $item.ToolTip
            ))
    }

    if ($results.Count -eq 0) {
        $fallback = if ([string]::IsNullOrWhiteSpace($inputPath)) { '<path>' } else { $inputPath }
        [void]$results.Add((New-AttribCompletionResult -CompletionText $fallback -ResultType 'ParameterValue' -ToolTip 'File or directory path or wildcard operand.'))
    }

    @($results.ToArray())
}

function Complete-Attrib {
    param(
        [string]$wordToComplete,
        [System.Management.Automation.Language.CommandAst]$commandAst,
        [int]$cursorPosition
    )

    $commandState = Get-AttribCommandTextForCompletion -CommandAst $commandAst -CursorPosition $cursorPosition
    $tokenState = Get-AttribTokenState -Line $commandState.Line -CursorPosition $commandState.CursorPosition
    $currentWord = if ($null -eq $tokenState.CurrentToken) { '' } else { $tokenState.CurrentToken }

    if ([string]::IsNullOrEmpty($currentWord)) {
        return @(Get-AttribPathCompletions -CurrentWord '')
    }

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

    if ($currentWord.StartsWith('/')) {
        return @(Get-AttribSlashCompletions -CurrentWord $currentWord)
    }

    @(Get-AttribPathCompletions -CurrentWord $currentWord)
}

Register-ArgumentCompleter -Native -CommandName @('attrib', 'attrib.exe') -ScriptBlock {
    param($wordToComplete, $commandAst, $cursorPosition)

    Complete-Attrib -wordToComplete $wordToComplete -commandAst $commandAst -cursorPosition $cursorPosition
}