completers/grep_completer/grep_completer.ps1
|
# grep tab completion for PowerShell # Builds a help-driven option catalog for grep.exe and adds value-aware completion. # The catalog parser is format-agnostic: it understands both the clap-based coreutils # synopsis style (`-x, --long <PLACEHOLDER>` with inline [possible values: ...]) and # GNU grep 3.x style (`--long=PLACEHOLDER` with inline descriptions, no --group-separator), # so the same script adapts to whichever build resolves first on PATH. Set-StrictMode -Version 2.0 function New-GrepCompletionResult { 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-GrepBinaryFilesValues { @('binary', 'text', 'without-match') } function Get-GrepDirectoriesValues { @('read', 'skip', 'recurse') } function Get-GrepDevicesValues { @('read', 'skip') } function Get-GrepColorWhenValues { @('always', 'never', 'auto') } function Get-GrepCompletionCatalog { if (Get-Variable -Name GrepCompletionCatalog -Scope Script -ErrorAction Ignore) { return $script:GrepCompletionCatalog } $script:GrepCompletionCatalog = @{ Initialized = $false CommandName = $null Options = @() OptionByToken = [System.Collections.Generic.Dictionary[string, object]]::new([System.StringComparer]::Ordinal) BinaryFilesValues = Get-GrepBinaryFilesValues DirectoriesValues = Get-GrepDirectoriesValues DevicesValues = Get-GrepDevicesValues ColorWhenValues = Get-GrepColorWhenValues } $script:GrepCompletionCatalog } function Resolve-GrepCommandName { $catalog = Get-GrepCompletionCatalog if ($catalog.CommandName) { return $catalog.CommandName } $command = Get-Command -Name grep.exe, grep -ErrorAction SilentlyContinue | Select-Object -First 1 if ($command) { $catalog.CommandName = if ($command.Source) { $command.Source } else { $command.Name } } $catalog.CommandName } function Invoke-GrepCapture { param([string[]]$Arguments) $commandName = Resolve-GrepCommandName if (-not $commandName) { return @() } try { @(& $commandName @Arguments 2>$null | ForEach-Object { $_ -replace '\e\[[0-9;?]*[ -/]*[@-~]', '' }) } catch { @() } } function Remove-GrepOuterQuotes { param([string]$Value) if ($null -eq $Value) { return '' } $Value.Trim([char[]]@([char]34, [char]39)) } function ConvertTo-GrepQuotedValue { 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-GrepTokenText { param([System.Management.Automation.Language.Ast]$Element) if ($Element -is [System.Management.Automation.Language.StringConstantExpressionAst]) { return $Element.Value } if ($Element -is [System.Management.Automation.Language.CommandParameterAst]) { return $Element.Extent.Text } $Element.Extent.Text } function Get-GrepCurrentToken { 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 Get-GrepArgumentTokens { param( [System.Management.Automation.Language.CommandAst]$CommandAst, [int]$CursorPosition ) $tokens = @() foreach ($element in $CommandAst.CommandElements | Select-Object -Skip 1) { if ($element.Extent.EndOffset -lt $CursorPosition) { $tokens += Get-GrepTokenText -Element $element } } $tokens } function Get-GrepValueKind { param( [string]$Token, [string]$Placeholder ) $tokenKey = if ($Token.StartsWith('--')) { $Token.ToLowerInvariant() } else { $Token } $placeholderKey = if ([string]::IsNullOrWhiteSpace($Placeholder)) { '' } else { $Placeholder.ToUpperInvariant() } switch -CaseSensitive ($tokenKey) { '-m' { return 'Number' } '--max-count' { return 'Number' } '-A' { return 'Number' } '--after-context' { return 'Number' } '-B' { return 'Number' } '--before-context' { return 'Number' } '-C' { return 'Number' } '--context' { return 'Number' } '--binary-files' { return 'BinaryFiles' } '-d' { return 'DirectoriesAction' } '--directories' { return 'DirectoriesAction' } '-D' { return 'DevicesAction' } '--devices' { return 'DevicesAction' } '--color' { return 'ColorWhen' } '--include' { return 'Glob' } '--exclude' { return 'Glob' } '--exclude-dir' { return 'Glob' } '-e' { return 'Pattern' } '--regexp' { return 'Pattern' } '-f' { return 'PatternFilePathOrStdin' } '--file' { return 'PatternFilePathOrStdin' } '--exclude-from' { return 'FilePath' } '--label' { return 'Label' } '--group-separator' { return 'Separator' } } switch ($placeholderKey) { 'NUM' { return 'Number' } 'GLOB' { return 'Glob' } 'FILE' { return 'FilePath' } 'PATTERNS' { return 'Pattern' } 'LABEL' { return 'Label' } 'SEP' { return 'Separator' } 'WHEN' { return 'ColorWhen' } 'TYPE' { return 'BinaryFiles' } } $null } function Get-GrepCanonicalOptionKey { param([string]$Token) if ([string]::IsNullOrWhiteSpace($Token)) { return $null } if ($Token.StartsWith('--')) { return 'long:' + $Token.ToLowerInvariant() } if ($Token.StartsWith('-')) { return 'short:' + $Token } $null } function ConvertFrom-GrepOptionSpecLine { param([string]$Line) $results = New-Object System.Collections.Generic.List[object] # Separate the option synopsis from any inline description. Both GNU and clap help # put the description after a run of two or more spaces, so cut at the first such gap. $specPart = ($Line -replace '^\s+', '') $description = '' if ($specPart -match '^(?<spec>.*?\S)\s{2,}(?<desc>\S.*)$') { $specPart = $matches['spec'] $description = $matches['desc'].Trim() } $specPart = $specPart.TrimEnd() # Trim a trailing comma left by split alias lines such as "--color[=WHEN],". $specLine = $specPart.TrimEnd(',').Trim() if ([string]::IsNullOrWhiteSpace($specLine)) { return @($results.ToArray()) } # Every comma-separated alias on the line is a spec of its own ("-q, --quiet, --silent"); the # placeholder found on any alias applies to all of them, and a bracketed value marks it optional. $parts = @($specLine -split ',\s*' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) $placeholder = $null $optionalValue = $false $parsedParts = New-Object System.Collections.Generic.List[object] foreach ($part in $parts) { $token = $null if ($part -match '^(?<token>--[A-Za-z0-9][A-Za-z0-9\-]*)(?:(?<bracket>\[?)[= ]\<?(?<value>[^\]\s>]+)\>?\]?)?$') { $token = $matches['token'] } elseif ($part -match '^(?<token>-[A-Za-z0-9])(?:(?<bracket>\[?)[= ]?\<?(?<value>[^\]\s>]+)\>?\]?)?$') { $token = $matches['token'] } else { continue } if ($matches['value'] -and -not $placeholder) { $placeholder = $matches['value'] } if ($matches['bracket']) { $optionalValue = $true } [void]$parsedParts.Add([pscustomobject]@{ Token = $token; DisplayText = $part }) } foreach ($parsed in $parsedParts) { [void]$results.Add([pscustomobject]@{ Token = $parsed.Token DisplayText = $parsed.DisplayText Placeholder = $placeholder OptionalValue = $optionalValue Description = $description }) } @($results.ToArray()) } function Add-GrepOptionSpec { param( [string]$Token, [string]$DisplayText, [string]$Description, [string]$Placeholder, [bool]$OptionalValue = $false ) if ([string]::IsNullOrWhiteSpace($Token)) { return } $catalog = Get-GrepCompletionCatalog $key = Get-GrepCanonicalOptionKey -Token $Token if ($catalog.OptionByToken.ContainsKey($key)) { if (-not [string]::IsNullOrWhiteSpace($Description)) { $catalog.OptionByToken[$key].Description = $Description } return } $catalog.OptionByToken[$key] = [pscustomobject]@{ Token = $Token DisplayText = if ([string]::IsNullOrWhiteSpace($DisplayText)) { $Token } else { $DisplayText } Description = $Description Placeholder = $Placeholder OptionalValue = $OptionalValue ValueKind = Get-GrepValueKind -Token $Token -Placeholder $Placeholder } $catalog.Options += $catalog.OptionByToken[$key] } function Add-GrepGnuHiddenOptionSpec { # GNU grep accepts these without printing them in --help (verified against GNU grep 3.0): # the --group-separator pair, the --fixed-regexp alias of -F and the obsolete -y synonym of -i. $isGnu = $false foreach ($line in @(Invoke-GrepCapture -Arguments @('--version'))) { if ($line -match 'GNU grep') { $isGnu = $true break } } if (-not $isGnu) { return } Add-GrepOptionSpec -Token '--group-separator' -DisplayText '--group-separator=SEP' -Description 'print SEP on line between matches with context' -Placeholder 'SEP' Add-GrepOptionSpec -Token '--no-group-separator' -DisplayText '--no-group-separator' -Description 'do not print separator for matches with context' Add-GrepOptionSpec -Token '--fixed-regexp' -DisplayText '--fixed-regexp' -Description 'obsolete synonym for -F, --fixed-strings' Add-GrepOptionSpec -Token '-y' -DisplayText '-y' -Description 'obsolete synonym for -i, --ignore-case' } function Initialize-GrepCompletionCatalog { $catalog = Get-GrepCompletionCatalog if ($catalog.Initialized) { return } $helpLines = Invoke-GrepCapture -Arguments @('--help') $currentKeys = @() $carriedKeys = @() foreach ($line in @($helpLines)) { # A synopsis line is indented and begins (after indent) with an option token: # short pair "-x, --long", long-only " --long", or short-only " -I". # The "-NUM" pseudo option (grep context shortcut) is intentionally skipped. $isSynopsis = ($line -match '^\s{2,}-[A-Za-z](?:[, =\[]|$)' -or $line -match '^\s{2,}--[A-Za-z0-9]') -and ($line -notmatch '^\s{2,}-NUM\b') if ($isSynopsis) { $parsed = @(ConvertFrom-GrepOptionSpecLine -Line $line) if ($parsed.Count -gt 0) { $currentKeys = @() foreach ($spec in $parsed) { $description = if ([string]::IsNullOrWhiteSpace($spec.Description)) { $spec.DisplayText } else { $spec.Description } Add-GrepOptionSpec -Token $spec.Token -DisplayText $spec.DisplayText -Description $description -Placeholder $spec.Placeholder -OptionalValue $spec.OptionalValue $currentKeys += Get-GrepCanonicalOptionKey -Token $spec.Token } # "--color[=WHEN]," continues onto "--colour[=WHEN] use markers ...": the aliases # on the earlier line share the description that the later line carries. foreach ($carriedKey in $carriedKeys) { $carriedOption = $catalog.OptionByToken[$carriedKey] if ($carriedOption.Description -eq $carriedOption.DisplayText -and $parsed[0].Description) { $carriedOption.Description = $parsed[0].Description } } $currentKeys = @($carriedKeys) + @($currentKeys) $carriedKeys = if ($line.TrimEnd().EndsWith(',')) { @($currentKeys) } else { @() } continue } } if ($currentKeys.Count -gt 0 -and $line -match '^\s{8,}(?<text>\S.*)$') { $continuation = $matches['text'].Trim() foreach ($key in $currentKeys) { $option = $catalog.OptionByToken[$key] if ([string]::IsNullOrWhiteSpace($option.Description) -or $option.Description -eq $option.DisplayText) { $option.Description = $continuation } elseif (-not $option.Description.EndsWith($continuation, [System.StringComparison]::OrdinalIgnoreCase)) { $option.Description += ' ' + $continuation } } continue } if ([string]::IsNullOrWhiteSpace($line) -or $line -match '^\S') { $currentKeys = @() } } Add-GrepGnuHiddenOptionSpec $catalog.Initialized = $true } function Get-GrepPathCompletions { param( [string]$InputPath, [string]$Prefix = '', [switch]$DirectoriesOnly ) $cleanInput = if ([string]::IsNullOrWhiteSpace($InputPath)) { '' } else { $InputPath.Trim('"') } $alwaysQuote = -not [string]::IsNullOrEmpty($InputPath) -and $InputPath.StartsWith('"') if ([string]::IsNullOrWhiteSpace($cleanInput)) { $parent = '.' $leaf = '' } elseif ($cleanInput.EndsWith('\') -or $cleanInput.EndsWith('/')) { $parent = $cleanInput $leaf = '' } else { $parent = Split-Path -Path $cleanInput -Parent if ([string]::IsNullOrWhiteSpace($parent)) { $parent = '.' } $leaf = Split-Path -Path $cleanInput -Leaf } $filter = if ([string]::IsNullOrWhiteSpace($leaf)) { '*' } else { "$leaf*" } $items = @(Get-ChildItem -Path $parent -Filter $filter -ErrorAction SilentlyContinue) if ($DirectoriesOnly) { $items = @($items | Where-Object { $_.PSIsContainer }) } foreach ($item in $items) { $completionText = if ($cleanInput -and -not [System.IO.Path]::IsPathRooted($cleanInput)) { if ($parent -eq '.') { $item.Name } else { Join-Path -Path $parent -ChildPath $item.Name } } else { $item.FullName } if ($item.PSIsContainer -and -not $completionText.EndsWith([System.IO.Path]::DirectorySeparatorChar)) { $completionText += [System.IO.Path]::DirectorySeparatorChar } $completionText = ConvertTo-GrepQuotedValue -Value $completionText -AlwaysQuote $alwaysQuote $completionText = $Prefix + $completionText New-GrepCompletionResult -CompletionText $completionText -ListItemText $item.Name -ResultType 'ParameterValue' -ToolTip $item.FullName } } function New-GrepLiteralValueResults { param( [string]$CurrentValue, [string]$Placeholder, [string]$ToolTip, [string]$Prefix = '' ) if ([string]::IsNullOrWhiteSpace($CurrentValue)) { return @( New-GrepCompletionResult -CompletionText ($Prefix + $Placeholder) -ListItemText $Placeholder -ResultType 'ParameterValue' -ToolTip $ToolTip ) } @( New-GrepCompletionResult -CompletionText ($Prefix + $CurrentValue) -ListItemText $CurrentValue -ResultType 'ParameterValue' -ToolTip $ToolTip ) } function Get-GrepEnumValueResults { param( [string[]]$Values, [string]$CurrentValue, [string]$ToolTip, [string]$Prefix = '' ) $typedValue = if ($null -eq $CurrentValue) { '' } else { $CurrentValue } foreach ($value in @($Values)) { if ($value -like ([System.Management.Automation.WildcardPattern]::Escape($typedValue) + '*')) { New-GrepCompletionResult -CompletionText ($Prefix + $value) -ResultType 'ParameterValue' -ToolTip $ToolTip } } } function Get-GrepEnumOrLiteralValueResults { param( [string[]]$Values, [string]$CurrentValue, [string]$Placeholder, [string]$ToolTip, [string]$Prefix = '' ) $results = @(Get-GrepEnumValueResults -Values $Values -CurrentValue $CurrentValue -ToolTip $ToolTip -Prefix $Prefix) if ($results.Count -gt 0) { return $results } New-GrepLiteralValueResults -CurrentValue $CurrentValue -Placeholder $Placeholder -ToolTip $ToolTip -Prefix $Prefix } function Get-GrepPathValueResults { param( [string]$CurrentValue, [string]$Placeholder, [string]$ToolTip, [string]$Prefix = '', [switch]$DirectoriesOnly, [switch]$AllowStdinSentinel ) $results = New-Object System.Collections.Generic.List[object] $typedValue = if ($null -eq $CurrentValue) { '' } else { $CurrentValue } if ($AllowStdinSentinel -and ('-' -like ([System.Management.Automation.WildcardPattern]::Escape($typedValue) + '*'))) { [void]$results.Add((New-GrepCompletionResult -CompletionText ($Prefix + '-') -ResultType 'ParameterValue' -ToolTip 'Read this value from standard input.')) } foreach ($item in @(Get-GrepPathCompletions -InputPath $typedValue -Prefix $Prefix -DirectoriesOnly:$DirectoriesOnly)) { [void]$results.Add($item) } if ($results.Count -gt 0) { return @($results.ToArray()) } New-GrepLiteralValueResults -CurrentValue $typedValue -Placeholder $Placeholder -ToolTip $ToolTip -Prefix $Prefix } function Get-GrepValueCompletions { param( [pscustomobject]$OptionSpec, [string]$CurrentValue, [string]$Prefix = '' ) $catalog = Get-GrepCompletionCatalog $typedValue = if ($null -eq $CurrentValue) { '' } else { $CurrentValue } $toolTip = if ([string]::IsNullOrWhiteSpace($OptionSpec.Description)) { $OptionSpec.DisplayText } else { $OptionSpec.Description } switch ($OptionSpec.ValueKind) { 'Pattern' { return New-GrepLiteralValueResults -CurrentValue $typedValue -Placeholder '<pattern>' -ToolTip $toolTip -Prefix $Prefix } 'Glob' { return New-GrepLiteralValueResults -CurrentValue $typedValue -Placeholder '<glob>' -ToolTip $toolTip -Prefix $Prefix } 'Label' { return New-GrepLiteralValueResults -CurrentValue $typedValue -Placeholder '<label>' -ToolTip $toolTip -Prefix $Prefix } 'Separator' { return New-GrepLiteralValueResults -CurrentValue $typedValue -Placeholder '<sep>' -ToolTip $toolTip -Prefix $Prefix } 'PatternFilePathOrStdin' { return @(Get-GrepPathValueResults -CurrentValue $typedValue -Placeholder '<pattern-file>' -ToolTip $toolTip -Prefix $Prefix -AllowStdinSentinel) } 'FilePath' { return @(Get-GrepPathValueResults -CurrentValue $typedValue -Placeholder '<file>' -ToolTip $toolTip -Prefix $Prefix) } 'BinaryFiles' { return @(Get-GrepEnumOrLiteralValueResults -Values $catalog.BinaryFilesValues -CurrentValue $typedValue -Placeholder '<type>' -ToolTip $toolTip -Prefix $Prefix) } 'DirectoriesAction' { return @(Get-GrepEnumOrLiteralValueResults -Values $catalog.DirectoriesValues -CurrentValue $typedValue -Placeholder '<action>' -ToolTip $toolTip -Prefix $Prefix) } 'DevicesAction' { return @(Get-GrepEnumOrLiteralValueResults -Values $catalog.DevicesValues -CurrentValue $typedValue -Placeholder '<action>' -ToolTip $toolTip -Prefix $Prefix) } 'ColorWhen' { return @(Get-GrepEnumOrLiteralValueResults -Values $catalog.ColorWhenValues -CurrentValue $typedValue -Placeholder '<when>' -ToolTip $toolTip -Prefix $Prefix) } 'Number' { return @(Get-GrepEnumOrLiteralValueResults -Values @('0', '1', '2', '5', '10', '100') -CurrentValue $typedValue -Placeholder '<num>' -ToolTip $toolTip -Prefix $Prefix) } default { return New-GrepLiteralValueResults -CurrentValue $typedValue -Placeholder '<value>' -ToolTip $toolTip -Prefix $Prefix } } } function Get-GrepOptionKey { param([string]$Token) $cleanToken = Remove-GrepOuterQuotes -Value $Token if ($cleanToken -match '^(--[A-Za-z0-9][A-Za-z0-9\-]*)') { return Get-GrepCanonicalOptionKey -Token $matches[1] } if ($cleanToken -match '^(-[^=\s]+)$') { return Get-GrepCanonicalOptionKey -Token $matches[1] } $null } function Get-GrepAttachedShortOption { param([string]$Token) $cleanToken = Remove-GrepOuterQuotes -Value $Token if ($cleanToken -match '^(?<flag>-(?:e|f|m|A|B|C|D|d))(?<value>.+)$') { [pscustomobject]@{ Flag = $matches['flag'] Value = $matches['value'] } return } $null } function Get-GrepCompletionContext { param([string[]]$TokensBeforeCurrent) Initialize-GrepCompletionCatalog $catalog = Get-GrepCompletionCatalog $positionals = New-Object System.Collections.Generic.List[string] $pendingOption = $null $endOfOptions = $false $hasPatternSource = $false foreach ($token in @($TokensBeforeCurrent)) { $cleanToken = Remove-GrepOuterQuotes -Value $token if ([string]::IsNullOrWhiteSpace($cleanToken)) { continue } if ($pendingOption) { if ($pendingOption.ValueKind -in @('Pattern', 'PatternFilePathOrStdin')) { $hasPatternSource = $true } $pendingOption = $null continue } if ($endOfOptions) { $positionals.Add($cleanToken) continue } if ($cleanToken -eq '--') { $endOfOptions = $true continue } $attached = Get-GrepAttachedShortOption -Token $cleanToken $attachedKey = if ($attached) { Get-GrepCanonicalOptionKey -Token $attached.Flag } else { $null } if ($attachedKey -and $catalog.OptionByToken.ContainsKey($attachedKey)) { $option = $catalog.OptionByToken[$attachedKey] if ($option.ValueKind -in @('Pattern', 'PatternFilePathOrStdin')) { $hasPatternSource = $true } continue } if ($cleanToken -match '^(--[^=]+)=(.*)$') { $lookup = Get-GrepCanonicalOptionKey -Token $matches[1] if ($catalog.OptionByToken.ContainsKey($lookup)) { $option = $catalog.OptionByToken[$lookup] if ($option.ValueKind -in @('Pattern', 'PatternFilePathOrStdin')) { $hasPatternSource = $true } continue } } $lookup = Get-GrepOptionKey -Token $cleanToken if ($lookup -and $catalog.OptionByToken.ContainsKey($lookup)) { $option = $catalog.OptionByToken[$lookup] # An optional-value flag (--color[=<WHEN>], --colour[=WHEN]) never consumes the next token # as its value, so it must not become a pending value option. if ($option.ValueKind -and -not $option.OptionalValue) { $pendingOption = $option } continue } # Unknown leading-`-` tokens (short clusters such as -rni, unknown flags) are treated as # consumed boolean switches: they neither become positionals nor open a pending value slot. if ($cleanToken.StartsWith('-')) { continue } $positionals.Add($cleanToken) } [pscustomobject]@{ PendingOption = $pendingOption EndOfOptions = $endOfOptions HasPatternSource = $hasPatternSource Positionals = @($positionals) } } function Get-GrepOptionCompletions { param([string]$CurrentWord) Initialize-GrepCompletionCatalog $catalog = Get-GrepCompletionCatalog $cleanCurrent = Remove-GrepOuterQuotes -Value $CurrentWord foreach ($option in $catalog.Options) { $matchesPrefix = $false if ([string]::IsNullOrWhiteSpace($cleanCurrent)) { $matchesPrefix = $true } elseif ($cleanCurrent.StartsWith('--')) { $matchesPrefix = $option.Token.StartsWith($cleanCurrent, [System.StringComparison]::OrdinalIgnoreCase) } else { $matchesPrefix = $option.Token.StartsWith($cleanCurrent, [System.StringComparison]::Ordinal) } if (-not $matchesPrefix) { continue } $toolTip = if ([string]::IsNullOrWhiteSpace($option.Description)) { $option.DisplayText } else { $option.Description } New-GrepCompletionResult -CompletionText $option.Token -ListItemText $option.DisplayText -ResultType 'ParameterName' -ToolTip $toolTip } } function Get-GrepPositionalCompletions { param( [string]$CurrentWord, [pscustomobject]$Context ) if ($Context.HasPatternSource -or $Context.Positionals.Count -gt 0) { return @(Get-GrepPathValueResults -CurrentValue $CurrentWord -Placeholder '<file>' -ToolTip 'File or directory to search.') } New-GrepLiteralValueResults -CurrentValue $CurrentWord -Placeholder '<pattern>' -ToolTip 'Pattern to search for.' } Register-ArgumentCompleter -Native -CommandName 'grep', 'grep.exe' -ScriptBlock { param($wordToComplete, $commandAst, $cursorPosition) if ($wordToComplete -isnot [string]) { $wordToComplete = [string]$wordToComplete } Initialize-GrepCompletionCatalog $catalog = Get-GrepCompletionCatalog # When the cursor sits past the parsed command extent the user is on a fresh # token after trailing whitespace, which $commandAst.Extent.Text has trimmed # away. Treat that as an empty current token so terminal/positional routing # runs instead of falling through to the option-name branch. $currentToken = if ($cursorPosition -gt $commandAst.Extent.EndOffset) { '' } else { Get-GrepCurrentToken -Line $commandAst.Extent.Text -CursorPosition ($cursorPosition - $commandAst.Extent.StartOffset) -Fallback $wordToComplete } $tokensBeforeCurrent = Get-GrepArgumentTokens -CommandAst $commandAst -CursorPosition $cursorPosition $context = Get-GrepCompletionContext -TokensBeforeCurrent $tokensBeforeCurrent if ($currentToken -match '^(--[^=]+)=(.*)$') { $optionKey = Get-GrepCanonicalOptionKey -Token $matches[1] $valuePrefix = $matches[2] if ($catalog.OptionByToken.ContainsKey($optionKey)) { $optionSpec = $catalog.OptionByToken[$optionKey] if ($optionSpec.ValueKind) { return @(Get-GrepValueCompletions -OptionSpec $optionSpec -CurrentValue $valuePrefix -Prefix ($matches[1] + '=')) } } } if ($currentToken -match '^(?<flag>-(?:e|f|m|A|B|C|D|d))(?<value>.+)$') { $optionKey = Get-GrepCanonicalOptionKey -Token $matches['flag'] if ($catalog.OptionByToken.ContainsKey($optionKey)) { return @(Get-GrepValueCompletions -OptionSpec $catalog.OptionByToken[$optionKey] -CurrentValue $matches['value'] -Prefix $matches['flag']) } } if ($context.PendingOption) { return @(Get-GrepValueCompletions -OptionSpec $context.PendingOption -CurrentValue $currentToken) } if ($currentToken -match '^-\d+$' -and -not $context.EndOfOptions) { # GNU's documented -NUM shortcut: keep the typed number, it is the same as --context=NUM. return @(New-GrepCompletionResult -CompletionText $currentToken -ResultType 'ParameterName' -ToolTip ('same as --context=' + $currentToken.Substring(1))) } if ($currentToken.StartsWith('-')) { return @(Get-GrepOptionCompletions -CurrentWord $currentToken) } @(Get-GrepPositionalCompletions -CurrentWord $currentToken -Context $context) } |