completers/accesschk_completer/accesschk_completer.ps1

# accesschk tab completion for PowerShell
# Native completer for AccessChk with safe mode-aware values and local hints.

Set-StrictMode -Version 2.0

if (-not (Get-Variable -Name AccessChkCompletionCatalog -Scope Script -ErrorAction Ignore)) {
    $script:AccessChkCompletionCatalog = @{
        Switches            = @(
            @{ Token = '-a'; Description = 'Treat the name as a Windows account right.' }
            @{ Token = '-c'; Description = 'Treat the name as a Windows service.' }
            @{ Token = '-d'; Description = 'Only process directories or top-level keys.' }
            @{ Token = '-e'; Description = 'Only show explicitly set integrity levels.' }
            @{ Token = '-f'; Description = 'After -p, show full token details; otherwise filter by comma-separated accounts.' }
            @{ Token = '-h'; Description = 'Treat the name as a file or printer share.' }
            @{ Token = '-i'; Description = 'Ignore inherited ACE-only objects when showing full descriptors.' }
            @{ Token = '-k'; Description = 'Treat the name as a registry key.' }
            @{ Token = '-l'; Description = 'Show the full security descriptor.' }
            @{ Token = '-L'; Description = 'Show the full security descriptor in SDDL format.' }
            @{ Token = '-m'; Description = 'Treat the name as an event log.' }
            @{ Token = '-n'; Description = 'Show only objects that have no access.' }
            @{ Token = '-o'; Description = 'Treat the name as an Object Manager namespace object.' }
            @{ Token = '-p'; Description = 'Treat the name as a process name or PID.' }
            @{ Token = '-nobanner'; Description = 'Do not display the startup banner.' }
            @{ Token = '-r'; Description = 'Show only objects that have read access.' }
            @{ Token = '-s'; Description = 'Recurse.' }
            @{ Token = '-t'; Description = 'After -o, filter by object type; after -p, show threads.' }
            @{ Token = '-u'; Description = 'Suppress errors.' }
            @{ Token = '-v'; Description = 'Verbose output.' }
            @{ Token = '-w'; Description = 'Show only objects that have write access.' }
            @{ Token = '-?'; Description = 'Show AccessChk help.' }
            @{ Token = '/?'; Description = 'Show AccessChk help.' }
        )
        ModeTokens          = @('-a', '-c', '-h', '-k', '-m', '-o', '-p')
        ProcessCache        = @()
        ProcessCacheUpdated = $null
        ProcessCacheTtl     = 5
        ServiceCache        = @()
        ServiceCacheUpdated = $null
        ServiceCacheTtl     = 30
        RegistryRoots       = @(
            @{ Token = 'HKLM\'; ProviderPath = 'Registry::HKEY_LOCAL_MACHINE'; Description = 'HKEY_LOCAL_MACHINE' }
            @{ Token = 'HKCU\'; ProviderPath = 'Registry::HKEY_CURRENT_USER'; Description = 'HKEY_CURRENT_USER' }
            @{ Token = 'HKCR\'; ProviderPath = 'Registry::HKEY_CLASSES_ROOT'; Description = 'HKEY_CLASSES_ROOT' }
            @{ Token = 'HKU\'; ProviderPath = 'Registry::HKEY_USERS'; Description = 'HKEY_USERS' }
            @{ Token = 'HKCC\'; ProviderPath = 'Registry::HKEY_CURRENT_CONFIG'; Description = 'HKEY_CURRENT_CONFIG' }
        )
        EventLogNames       = @('Application', 'System', 'Security', 'Setup', 'ForwardedEvents', '*')
        ShareNames          = @('*', 'ADMIN$', 'C$', 'IPC$', '<share>')
        AccountRights       = @('*', 'SeBackupPrivilege', 'SeDebugPrivilege', 'SeServiceLogonRight', 'SeShutdownPrivilege', '<account-right>')
        ObjectTypes         = @('Section', 'Directory', 'Event', 'File', 'Key', 'Mutant', 'Semaphore', 'SymbolicLink', 'Type', 'WindowStation')
        ObjectRoots         = @('\', '\BaseNamedObjects', '\KnownDlls', '\RPC Control', '\Sessions\', '<object-path>')
        AccountSamples      = @('Everyone', 'Users', 'Administrators', 'SYSTEM', '<username>')
    }
}

function New-AccessChkCompletionResult {
    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 Remove-AccessChkOuterQuotes {
    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-AccessChkQuotedValue {
    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('"'))) {
        $escaped = $Value.Replace('`', '``').Replace('"', '`"')
        return '"' + $escaped + '"'
    }

    $Value
}

function Get-AccessChkTokenState {
    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 = $prefix -match '\s$'
    if ($builder.Length -gt 0) {
        $tokens.Add($builder.ToString())
    }

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

    if ($tokens.Count -gt 0) {
        return [pscustomobject]@{
            TokensBeforeCurrent = @($tokens | Select-Object -First ($tokens.Count - 1))
            CurrentToken        = $tokens[$tokens.Count - 1]
        }
    }

    [pscustomobject]@{
        TokensBeforeCurrent = @()
        CurrentToken        = ''
    }
}

function Get-AccessChkArgumentsFromTokenState {
    param([pscustomobject]$TokenState)

    if (-not $TokenState) {
        return [pscustomobject]@{
            ArgumentsBeforeCurrent = @()
            CurrentArgument        = ''
        }
    }

    [pscustomobject]@{
        ArgumentsBeforeCurrent = @($TokenState.TokensBeforeCurrent | Select-Object -Skip 1)
        CurrentArgument        = $TokenState.CurrentToken
    }
}

function Get-AccessChkUniqueCompletions {
    param([object[]]$Results)

    $seen = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal)
    $unique = New-Object System.Collections.Generic.List[object]

    foreach ($result in $Results) {
        if ($null -eq $result) {
            continue
        }

        if (-not $seen.Add($result.CompletionText)) {
            continue
        }

        [void]$unique.Add($result)
    }

    @($unique.ToArray())
}

function Get-AccessChkStaticValueResults {
    param(
        [string]$CurrentValue,
        [string[]]$Values,
        [string]$ToolTip
    )

    $typedValue = Remove-AccessChkOuterQuotes -Value $CurrentValue
    $results = New-Object System.Collections.Generic.List[object]

    foreach ($value in $Values) {
        if (-not [string]::IsNullOrWhiteSpace($typedValue) -and
            -not $value.StartsWith($typedValue, [System.StringComparison]::OrdinalIgnoreCase)) {
            continue
        }

        [void]$results.Add((New-AccessChkCompletionResult -CompletionText $value -ListItemText $value -ResultType 'ParameterValue' -ToolTip $ToolTip))
    }

    if ($results.Count -eq 0 -and -not [string]::IsNullOrWhiteSpace($CurrentValue)) {
        [void]$results.Add((New-AccessChkCompletionResult -CompletionText $CurrentValue -ListItemText $CurrentValue -ResultType 'ParameterValue' -ToolTip $ToolTip))
    }

    @($results.ToArray())
}

function Get-AccessChkPathCompletions {
    param(
        [string]$CurrentWord,
        [string]$ToolTip,
        [string]$Placeholder = '<path>'
    )

    $typedValue = Remove-AccessChkOuterQuotes -Value $CurrentWord
    $alwaysQuote = $CurrentWord.StartsWith('"')
    $results = New-Object System.Collections.Generic.List[object]

    $parentPath = '.'
    $leaf = ''
    if (-not [string]::IsNullOrWhiteSpace($typedValue)) {
        if ($typedValue.EndsWith('\') -or $typedValue.EndsWith('/')) {
            $parentPath = $typedValue
        } else {
            $candidateParent = Split-Path -Path $typedValue -Parent
            if ([string]::IsNullOrWhiteSpace($candidateParent)) {
                $parentPath = '.'
                $leaf = $typedValue
            } else {
                $parentPath = $candidateParent
                $leaf = Split-Path -Path $typedValue -Leaf
            }
        }
    }

    try {
        $items = @(Get-ChildItem -LiteralPath $parentPath -ErrorAction Stop)
    } catch {
        $items = @()
    }

    foreach ($item in $items) {
        if (-not [string]::IsNullOrWhiteSpace($leaf) -and
            -not $item.Name.StartsWith($leaf, [System.StringComparison]::OrdinalIgnoreCase)) {
            continue
        }

        $candidatePath = if ($parentPath -eq '.') {
            $item.Name
        } else {
            Join-Path -Path $parentPath -ChildPath $item.Name
        }

        if ($item.PSIsContainer) {
            $candidatePath += '\'
        }

        $completionText = ConvertTo-AccessChkQuotedValue -Value $candidatePath -AlwaysQuote $alwaysQuote
        $itemToolTip = if ($item.PSIsContainer) { 'Directory path.' } else { 'File path.' }
        [void]$results.Add((New-AccessChkCompletionResult -CompletionText $completionText -ListItemText $completionText -ResultType 'ParameterValue' -ToolTip $itemToolTip))
    }

    if ($results.Count -eq 0) {
        if ([string]::IsNullOrWhiteSpace($CurrentWord)) {
            [void]$results.Add((New-AccessChkCompletionResult -CompletionText $Placeholder -ListItemText $Placeholder -ResultType 'ParameterValue' -ToolTip $ToolTip))
        } else {
            [void]$results.Add((New-AccessChkCompletionResult -CompletionText $CurrentWord -ListItemText $CurrentWord -ResultType 'ParameterValue' -ToolTip $ToolTip))
        }
    }

    @($results.ToArray())
}

function Update-AccessChkProcessCache {
    $lastUpdated = $script:AccessChkCompletionCatalog.ProcessCacheUpdated
    if ($null -ne $lastUpdated -and $script:AccessChkCompletionCatalog.ProcessCache.Count -gt 0 -and ((Get-Date) - $lastUpdated).TotalSeconds -lt $script:AccessChkCompletionCatalog.ProcessCacheTtl) {
        return
    }

    try {
        $entries = New-Object System.Collections.Generic.List[string]
        foreach ($process in (Get-Process -ErrorAction Stop | Sort-Object -Property ProcessName)) {
            if (-not [string]::IsNullOrWhiteSpace($process.ProcessName)) {
                [void]$entries.Add($process.ProcessName)
            }
            [void]$entries.Add($process.Id.ToString())
        }

        $script:AccessChkCompletionCatalog.ProcessCache = @($entries | Sort-Object -Unique)
        $script:AccessChkCompletionCatalog.ProcessCacheUpdated = Get-Date
    } catch {
        $script:AccessChkCompletionCatalog.ProcessCache = @()
    }
}

function Update-AccessChkServiceCache {
    $lastUpdated = $script:AccessChkCompletionCatalog.ServiceCacheUpdated
    if ($null -ne $lastUpdated -and $script:AccessChkCompletionCatalog.ServiceCache.Count -gt 0 -and ((Get-Date) - $lastUpdated).TotalSeconds -lt $script:AccessChkCompletionCatalog.ServiceCacheTtl) {
        return
    }

    try {
        $script:AccessChkCompletionCatalog.ServiceCache = @(
            Get-Service -ErrorAction Stop |
                Select-Object -ExpandProperty Name |
                Where-Object { -not [string]::IsNullOrWhiteSpace($_) } |
                Sort-Object -Unique
        )
        $script:AccessChkCompletionCatalog.ServiceCacheUpdated = Get-Date
    } catch {
        $script:AccessChkCompletionCatalog.ServiceCache = @()
    }
}

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

    $typedValue = Remove-AccessChkOuterQuotes -Value $CurrentWord
    $results = New-Object System.Collections.Generic.List[object]

    foreach ($root in $script:AccessChkCompletionCatalog.RegistryRoots) {
        if ([string]::IsNullOrWhiteSpace($typedValue) -or
            $root.Token.StartsWith($typedValue, [System.StringComparison]::OrdinalIgnoreCase)) {
            [void]$results.Add((New-AccessChkCompletionResult -CompletionText $root.Token -ListItemText $root.Token -ResultType 'ParameterValue' -ToolTip $root.Description))
        }
    }

    $rootMatch = $null
    foreach ($root in $script:AccessChkCompletionCatalog.RegistryRoots) {
        if ($typedValue.StartsWith($root.Token, [System.StringComparison]::OrdinalIgnoreCase)) {
            $rootMatch = $root
            break
        }
    }

    if ($rootMatch) {
        $relativePath = $typedValue.Substring($rootMatch.Token.Length)
        $relativeParent = ''
        $leaf = ''

        if (-not [string]::IsNullOrWhiteSpace($relativePath)) {
            if ($relativePath.EndsWith('\')) {
                $relativeParent = $relativePath.TrimEnd('\')
            } else {
                $candidateParent = Split-Path -Path $relativePath -Parent
                if ([string]::IsNullOrWhiteSpace($candidateParent)) {
                    $leaf = $relativePath
                } else {
                    $relativeParent = $candidateParent
                    $leaf = Split-Path -Path $relativePath -Leaf
                }
            }
        }

        $providerParent = $rootMatch.ProviderPath
        if (-not [string]::IsNullOrWhiteSpace($relativeParent)) {
            $providerParent = Join-Path -Path $providerParent -ChildPath $relativeParent
        }

        try {
            $children = @(Get-ChildItem -LiteralPath $providerParent -ErrorAction Ignore)
        } catch {
            $children = @()
        }

        foreach ($child in $children) {
            if (-not $child.PSIsContainer) {
                continue
            }

            if (-not [string]::IsNullOrWhiteSpace($leaf) -and
                -not $child.PSChildName.StartsWith($leaf, [System.StringComparison]::OrdinalIgnoreCase)) {
                continue
            }

            $baseToken = $rootMatch.Token
            if (-not [string]::IsNullOrWhiteSpace($relativeParent)) {
                $baseToken += ($relativeParent.TrimEnd('\') + '\')
            }

            $candidate = $baseToken + $child.PSChildName
            [void]$results.Add((New-AccessChkCompletionResult -CompletionText $candidate -ListItemText $candidate -ResultType 'ParameterValue' -ToolTip 'Registry key path.'))
        }
    }

    if ($results.Count -eq 0) {
        if ([string]::IsNullOrWhiteSpace($CurrentWord)) {
            [void]$results.Add((New-AccessChkCompletionResult -CompletionText 'HKLM\' -ListItemText 'HKLM\' -ResultType 'ParameterValue' -ToolTip 'Registry key path.'))
        } else {
            [void]$results.Add((New-AccessChkCompletionResult -CompletionText $CurrentWord -ListItemText $CurrentWord -ResultType 'ParameterValue' -ToolTip 'Registry key path.'))
        }
    }

    @($results.ToArray())
}

function Get-AccessChkCommandState {
    param([string[]]$ArgumentsBeforeCurrent)

    $usedTokens = @{}
    $positionals = New-Object System.Collections.Generic.List[string]
    $mode = $null
    $valueContext = $null

    for ($index = 0; $index -lt $ArgumentsBeforeCurrent.Count; $index++) {
        $token = $ArgumentsBeforeCurrent[$index]
        if ([string]::IsNullOrWhiteSpace($token)) {
            continue
        }

        $lookup = $token.ToLowerInvariant()
        $usedTokens[$lookup] = $true

        if ($lookup -in @('-?', '/?')) {
            continue
        }

        if ($lookup -in $script:AccessChkCompletionCatalog.ModeTokens -and -not $mode) {
            $mode = $lookup
            continue
        }

        if ($lookup -eq '-f') {
            if ($mode -ne '-p') {
                if ($index -eq ($ArgumentsBeforeCurrent.Count - 1)) {
                    $valueContext = '-f'
                    break
                }

                $index++
            }

            continue
        }

        if ($lookup -eq '-t') {
            if ($mode -ne '-p') {
                if ($index -eq ($ArgumentsBeforeCurrent.Count - 1)) {
                    $valueContext = '-t'
                    break
                }

                $index++
            }

            continue
        }

        if ($lookup.StartsWith('-') -or $lookup.StartsWith('/')) {
            continue
        }

        $positionals.Add($token)
    }

    [pscustomobject]@{
        UsedTokens   = $usedTokens
        Mode         = $mode
        ValueContext = $valueContext
        Positionals  = @($positionals.ToArray())
    }
}

function Test-AccessChkPathLikeValue {
    param([string]$Value)

    if ([string]::IsNullOrWhiteSpace($Value)) {
        return $false
    }

    $trimmedValue = Remove-AccessChkOuterQuotes -Value $Value
    $trimmedValue -match '^[A-Za-z]:\\|^[.]{1,2}(\\|/)|^[\\/]|[*?]'
}

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

    $line = if ($CommandAst.Extent -and $null -ne $CommandAst.Extent.Text) { $CommandAst.Extent.Text } else { $CommandAst.ToString() }
    $relativeCursor = $CursorPosition - $CommandAst.Extent.StartOffset
    if ($relativeCursor -gt $line.Length) {
        $line = $line.PadRight($relativeCursor)
    }
    $tokenState = Get-AccessChkTokenState -Line $line -CursorPosition $relativeCursor
    $argumentsState = Get-AccessChkArgumentsFromTokenState -TokenState $tokenState
    $state = Get-AccessChkCommandState -ArgumentsBeforeCurrent $argumentsState.ArgumentsBeforeCurrent
    $currentWord = $argumentsState.CurrentArgument
    $currentValue = Remove-AccessChkOuterQuotes -Value $currentWord

    if ($state.ValueContext) {
        switch ($state.ValueContext) {
            '-f' { return Get-AccessChkStaticValueResults -CurrentValue $currentWord -Values @('Everyone', 'Users', 'Administrators', 'SYSTEM', '<account[,account...]>') -ToolTip 'Comma-separated account filter.' }
            '-t' { return Get-AccessChkStaticValueResults -CurrentValue $currentWord -Values $script:AccessChkCompletionCatalog.ObjectTypes -ToolTip 'Object Manager object type filter.' }
        }
    }

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

    if (-not $currentWord.StartsWith('-') -and -not $currentWord.StartsWith('/')) {
        switch ($state.Mode) {
            '-a' {
                $results.AddRange(@(Get-AccessChkStaticValueResults -CurrentValue $currentWord -Values $script:AccessChkCompletionCatalog.AccountRights -ToolTip 'Account right name or * for all rights.'))
            }
            '-c' {
                Update-AccessChkServiceCache
                $serviceValues = @('*', 'scmanager') + $script:AccessChkCompletionCatalog.ServiceCache
                $results.AddRange(@(Get-AccessChkStaticValueResults -CurrentValue $currentWord -Values $serviceValues -ToolTip 'Service name, scmanager, or * for all services.'))
            }
            '-h' {
                $results.AddRange(@(Get-AccessChkStaticValueResults -CurrentValue $currentWord -Values $script:AccessChkCompletionCatalog.ShareNames -ToolTip 'Share name or * for all shares.'))
            }
            '-k' {
                $results.AddRange(@(Get-AccessChkRegistryCompletions -CurrentWord $currentWord))
            }
            '-m' {
                $results.AddRange(@(Get-AccessChkStaticValueResults -CurrentValue $currentWord -Values $script:AccessChkCompletionCatalog.EventLogNames -ToolTip 'Event log name or * for all event logs.'))
            }
            '-o' {
                $results.AddRange(@(Get-AccessChkStaticValueResults -CurrentValue $currentWord -Values $script:AccessChkCompletionCatalog.ObjectRoots -ToolTip 'Object Manager namespace path.'))
            }
            '-p' {
                Update-AccessChkProcessCache
                $processValues = @('*') + $script:AccessChkCompletionCatalog.ProcessCache + @('<process-or-pid>')
                $results.AddRange(@(Get-AccessChkStaticValueResults -CurrentValue $currentWord -Values $processValues -ToolTip 'Process name, PID, or * for all processes.'))
            }
            default {
                if ($state.Positionals.Count -eq 0) {
                    $results.AddRange(@(Get-AccessChkStaticValueResults -CurrentValue $currentWord -Values $script:AccessChkCompletionCatalog.AccountSamples -ToolTip 'User or group name for effective-permission calculation.'))
                    $results.AddRange(@(Get-AccessChkPathCompletions -CurrentWord $currentWord -ToolTip 'File system path, named pipe path, or other securable object path.' -Placeholder '<path>'))
                } elseif (($state.Positionals.Count -eq 1) -and -not (Test-AccessChkPathLikeValue -Value $state.Positionals[0])) {
                    $results.AddRange(@(Get-AccessChkPathCompletions -CurrentWord $currentWord -ToolTip 'File system path, named pipe path, or other securable object path.' -Placeholder '<path>'))
                } else {
                    $results.AddRange(@(Get-AccessChkPathCompletions -CurrentWord $currentWord -ToolTip 'File system path, named pipe path, or other securable object path.' -Placeholder '<path>'))
                }
            }
        }
    }

    # An empty word directly inside a mode value slot (-c, -p, ...) is a value, not an option.
    $wantsSwitches = $currentWord.StartsWith('-') -or $currentWord.StartsWith('/') -or
        ([string]::IsNullOrEmpty($currentWord) -and -not $state.Mode)
    if ($wantsSwitches) {
        foreach ($switchSpec in $script:AccessChkCompletionCatalog.Switches) {
            if (-not $switchSpec.Token.StartsWith($currentWord, [System.StringComparison]::Ordinal)) {
                continue
            }

            $lowerToken = $switchSpec.Token.ToLowerInvariant()
            if (($lowerToken -eq '-t') -and -not ($state.Mode -in @('-o', '-p'))) {
                continue
            }

            if (($lowerToken -eq '-f') -and $state.UsedTokens.ContainsKey('-f')) {
                continue
            }

            if (($lowerToken -eq '-?' -or $lowerToken -eq '/?') -and $state.UsedTokens.Count -gt 0 -and -not [string]::IsNullOrEmpty($currentWord)) {
                continue
            }

            [void]$results.Add((New-AccessChkCompletionResult -CompletionText $switchSpec.Token -ListItemText $switchSpec.Token -ResultType 'ParameterName' -ToolTip $switchSpec.Description))
        }
    }

    Get-AccessChkUniqueCompletions -Results @($results.ToArray())
}

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

    Complete-AccessChk -WordToComplete $wordToComplete -CommandAst $commandAst -CursorPosition $cursorPosition
}