completers/findstr_completer/findstr_completer.ps1
|
<# .SYNOPSIS Registers a native PowerShell argument completer for findstr. .DESCRIPTION Provides a static-first native completer for `findstr` and `findstr.exe`. The completer covers: - slash-style switch completion from the built-in help surface - attached value switches such as `/A:`, `/C:`, `/D:`, `/F:`, `/G:`, and `/Q:` - placeholder-driven search-string completion to suppress unwanted filesystem fallback - local path completion for filename operands, file-list values, pattern-file values, and directory lists - conservative handling of the ambiguous bare `search-string` vs `filename` boundary The script keeps its top level compatible with `Import-CompleterScript`. #> Set-StrictMode -Version Latest function New-FindStrCompletionResult { 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 New-FindStrSwitchSpec { param( [string]$Token, [string]$Description, [string]$ValueKind ) [pscustomobject]@{ Token = $Token Key = $Token.ToLowerInvariant() Description = $Description ValueKind = $ValueKind TakesValue = -not [string]::IsNullOrWhiteSpace($ValueKind) } } function Get-FindStrCompletionCatalog { if (Get-Variable -Name FindStrCompletionCatalog -Scope Script -ErrorAction Ignore) { return $script:FindStrCompletionCatalog } $switches = @( New-FindStrSwitchSpec -Token '/B' -Description 'Matches pattern if at the beginning of a line.' New-FindStrSwitchSpec -Token '/E' -Description 'Matches pattern if at the end of a line.' New-FindStrSwitchSpec -Token '/L' -Description 'Uses search strings literally.' New-FindStrSwitchSpec -Token '/R' -Description 'Uses search strings as regular expressions.' New-FindStrSwitchSpec -Token '/S' -Description 'Searches for matching files in the current directory and all subdirectories.' New-FindStrSwitchSpec -Token '/I' -Description 'Specifies that the search is not case-sensitive.' New-FindStrSwitchSpec -Token '/X' -Description 'Prints lines that match exactly.' New-FindStrSwitchSpec -Token '/V' -Description 'Prints only lines that do not contain a match.' New-FindStrSwitchSpec -Token '/N' -Description 'Prints the line number before each line that matches.' New-FindStrSwitchSpec -Token '/M' -Description 'Prints only the filename if a file contains a match.' New-FindStrSwitchSpec -Token '/O' -Description 'Prints character offset before each matching line.' New-FindStrSwitchSpec -Token '/P' -Description 'Skips files with non-printable characters.' New-FindStrSwitchSpec -Token '/A:' -Description 'Specifies color attribute with two hex digits. See color /?.' -ValueKind 'ColorAttr' New-FindStrSwitchSpec -Token '/F:' -Description 'Reads the file list from the specified file, or / for console input.' -ValueKind 'FileListPathOrConsole' New-FindStrSwitchSpec -Token '/C:' -Description 'Uses the specified string as a literal search string.' -ValueKind 'SearchString' New-FindStrSwitchSpec -Token '/G:' -Description 'Gets search strings from the specified file, or / for console input.' -ValueKind 'PatternFilePathOrConsole' New-FindStrSwitchSpec -Token '/D:' -Description 'Searches a semicolon-delimited list of directories.' -ValueKind 'DirectoryList' New-FindStrSwitchSpec -Token '/Q:' -Description 'Quiet mode flags. Currently supports u to suppress unsupported-Unicode warnings.' -ValueKind 'QuietFlags' New-FindStrSwitchSpec -Token '/OFF' -Description 'Does not skip files with the offline attribute set.' New-FindStrSwitchSpec -Token '/OFFLINE' -Description 'Does not skip files with the offline attribute set.' New-FindStrSwitchSpec -Token '/?' -Description 'Displays help for findstr.' ) $switchLookup = @{} $attachedValueLookup = @{} foreach ($switch in $switches) { $switchLookup[$switch.Key] = $switch if ($switch.TakesValue) { $attachedValueLookup[$switch.Key.TrimEnd(':')] = $switch } } $script:FindStrCompletionCatalog = [pscustomobject]@{ Switches = $switches SwitchLookup = $switchLookup AttachedValueLookup = $attachedValueLookup ColorNames = [ordered]@{ '0' = 'Black'; '1' = 'Blue'; '2' = 'Green'; '3' = 'Aqua' '4' = 'Red'; '5' = 'Purple'; '6' = 'Yellow'; '7' = 'White' '8' = 'Gray'; '9' = 'Light Blue'; 'A' = 'Light Green'; 'B' = 'Light Aqua' 'C' = 'Light Red'; 'D' = 'Light Purple'; 'E' = 'Light Yellow'; 'F' = 'Bright White' } QuietFlags = @('u') } $script:FindStrCompletionCatalog } function Get-FindStrColorCompletion { param( [string]$Prefix, [string]$CurrentValue ) # /A: takes exactly two hex digits, background then foreground, using the # digit table from color /?; every one of the 256 pairs is valid. $names = (Get-FindStrCompletionCatalog).ColorNames $typedValue = (Remove-FindStrOuterQuotes -Value $CurrentValue).ToUpperInvariant() $results = New-Object System.Collections.Generic.List[object] foreach ($background in $names.Keys) { foreach ($foreground in $names.Keys) { $pair = $background + $foreground if (-not $pair.StartsWith($typedValue)) { continue } [void]$results.Add((New-FindStrCompletionResult -CompletionText ($Prefix + $pair) -ResultType 'ParameterValue' -ToolTip ($names[$foreground] + ' on ' + $names[$background] + '.'))) } } if ($results.Count -eq 0) { $fallback = if ([string]::IsNullOrWhiteSpace($CurrentValue)) { $Prefix + '<hh>' } else { $Prefix + $CurrentValue } [void]$results.Add((New-FindStrCompletionResult -CompletionText $fallback -ResultType 'ParameterValue' -ToolTip 'Color attribute: two hex digits, background then foreground.')) } @($results.ToArray()) } function Remove-FindStrOuterQuotes { 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 ConvertFrom-FindStrTypedWord { # The value of a typed word and the quote it opened with ('' when bare). A word opened # with a quote (ASCII or typographic) is read by the PowerShell tokenizer, which drops # the quotes and undoes that quote style's escapes. param([string]$Value) if ($Value -notmatch '^[''"\u2018-\u201E]') { return [pscustomobject]@{ Value = $Value; Quote = '' } } $tokens = $null $parseErrors = $null [void][System.Management.Automation.Language.Parser]::ParseInput($Value, [ref]$tokens, [ref]$parseErrors) [pscustomobject]@{ Value = [string]$tokens[0].Value; Quote = $Value.Substring(0, 1) } } function ConvertTo-FindStrQuotedValue { # Renders a value as one PowerShell argument: bare when safe and no quote was typed, # otherwise in the quote the user typed (single by default). PowerShell reads ' and # U+2018-U+201B as single quotes and " and U+201C-U+201E as double quotes. param( [string]$Value, [string]$QuoteChar = '' ) if ([string]::IsNullOrEmpty($Value)) { return $Value } if (-not $QuoteChar) { if ($Value -notmatch '[\s{}();,|&<>''"`$@#\u2018-\u201E]') { return $Value } $QuoteChar = "'" } if ($QuoteChar -match '^[''\u2018-\u201B]$') { return $QuoteChar + ($Value -replace '([''\u2018-\u201B])', '$1$1') + $QuoteChar } $QuoteChar + ($Value -replace '([`"$\u201C-\u201E])', '`$1') + $QuoteChar } function Get-FindStrTokenState { 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 # PowerShell reads ' and U+2018-U+201B as single quotes and " and # U+201C-U+201E as double quotes; a backtick outside single quotes escapes # the next character. $quoteClass = '' $escapeNext = $false foreach ($character in $prefix.ToCharArray()) { if ($escapeNext) { $escapeNext = $false [void]$builder.Append($character) continue } if ($character -eq '`' -and $quoteClass -ne 'single') { $escapeNext = $true [void]$builder.Append($character) continue } $characterClass = if ($character -match '[''\u2018-\u201B]') { 'single' } elseif ($character -match '["\u201C-\u201E]') { 'double' } else { '' } if ($characterClass) { if (-not $quoteClass) { $quoteClass = $characterClass } elseif ($quoteClass -eq $characterClass) { $quoteClass = '' } [void]$builder.Append($character) continue } if ([char]::IsWhiteSpace($character) -and -not $quoteClass) { 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-FindStrArgumentsFromTokenState { param([pscustomobject]$TokenState) $tokensBeforeCurrent = @($TokenState.TokensBeforeCurrent) $currentArgument = if ($null -eq $TokenState.CurrentToken) { '' } else { $TokenState.CurrentToken } if ($tokensBeforeCurrent.Count -gt 0) { $argumentsBeforeCurrent = @($tokensBeforeCurrent | Select-Object -Skip 1) } else { $argumentsBeforeCurrent = @() } if ($tokensBeforeCurrent.Count -eq 0 -and $currentArgument -match '^(?i)findstr(?:\.exe)?$') { $currentArgument = '' } [pscustomobject]@{ ArgumentsBeforeCurrent = $argumentsBeforeCurrent CurrentArgument = $currentArgument } } function Get-FindStrAttachedTokenInfo { param([string]$Token) if ([string]::IsNullOrWhiteSpace($Token)) { return $null } # findstr parses a switch word letter by letter, so plain flags may lead a # value switch in one cluster (/nic:text, /na:0C); the value switch must # come last, and the whole typed head stays the completion prefix. $match = [regex]::Match($Token, '^(?<head>/(?i:[belrsixvnmop]*)(?<root>[A-Za-z])):(?<value>.*)$') if (-not $match.Success) { return $null } $catalog = Get-FindStrCompletionCatalog $rootKey = '/' + $match.Groups['root'].Value.ToLowerInvariant() if (-not $catalog.AttachedValueLookup.ContainsKey($rootKey)) { return $null } [pscustomobject]@{ RootKey = $rootKey Prefix = $match.Groups['head'].Value + ':' Value = $match.Groups['value'].Value Switch = $catalog.AttachedValueLookup[$rootKey] } } function Get-FindStrCompletionContext { param([string[]]$ArgumentsBeforeCurrent) $catalog = Get-FindStrCompletionCatalog $helpRequested = $false $hasExplicitSearchSource = $false $bareSearchCount = 0 $inFilenameMode = $false foreach ($argument in @($ArgumentsBeforeCurrent)) { if ([string]::IsNullOrWhiteSpace($argument)) { continue } $attachedInfo = Get-FindStrAttachedTokenInfo -Token $argument if ($null -ne $attachedInfo) { switch ($attachedInfo.RootKey) { '/c' { $hasExplicitSearchSource = $true } '/g' { $hasExplicitSearchSource = $true } } continue } $argumentKey = $argument.ToLowerInvariant() if ($catalog.SwitchLookup.ContainsKey($argumentKey)) { if ($argumentKey -eq '/?') { $helpRequested = $true } continue } # A cluster of plain flags (/si, /spin, /i?) is still switches, not # the bare search string. if ([regex]::IsMatch($argument, '^/(?i:[belrsixvnmop?]{2,})$')) { if ($argument.Contains('?')) { $helpRequested = $true } continue } if ($hasExplicitSearchSource) { $inFilenameMode = $true continue } if ($bareSearchCount -eq 0) { $bareSearchCount = 1 continue } $inFilenameMode = $true } [pscustomobject]@{ HelpRequested = $helpRequested HasExplicitSearchSource = $hasExplicitSearchSource BareSearchCount = $bareSearchCount HasBareSearchStrings = $bareSearchCount -gt 0 InFilenameMode = $inFilenameMode } } function Get-FindStrUniqueCompletions { param([object[]]$Results) $seen = @{} $unique = New-Object System.Collections.Generic.List[object] foreach ($result in @($Results)) { if ($null -eq $result) { continue } if ($seen.ContainsKey($result.CompletionText)) { continue } $seen[$result.CompletionText] = $true [void]$unique.Add($result) } @($unique.ToArray()) } function Get-FindStrSwitchCompletions { param([string]$CurrentWord) $catalog = Get-FindStrCompletionCatalog $results = New-Object System.Collections.Generic.List[object] foreach ($switch in $catalog.Switches) { if (-not [string]::IsNullOrWhiteSpace($CurrentWord) -and -not $switch.Token.StartsWith($CurrentWord, [System.StringComparison]::OrdinalIgnoreCase)) { continue } [void]$results.Add((New-FindStrCompletionResult -CompletionText $switch.Token -ResultType 'ParameterName' -ToolTip $switch.Description)) } @($results.ToArray()) } function Get-FindStrPrefixedValueCompletions { param( [string]$Prefix, [string]$CurrentValue, [string[]]$Suggestions, [string]$ToolTip, [string]$Placeholder ) $typedValue = Remove-FindStrOuterQuotes -Value $CurrentValue $results = New-Object System.Collections.Generic.List[object] foreach ($suggestion in @($Suggestions)) { if (-not [string]::IsNullOrWhiteSpace($typedValue) -and -not $suggestion.StartsWith($typedValue, [System.StringComparison]::OrdinalIgnoreCase)) { continue } [void]$results.Add((New-FindStrCompletionResult -CompletionText ($Prefix + $suggestion) -ResultType 'ParameterValue' -ToolTip $ToolTip)) } if ($results.Count -eq 0) { if ([string]::IsNullOrWhiteSpace($CurrentValue)) { [void]$results.Add((New-FindStrCompletionResult -CompletionText ($Prefix + $Placeholder) -ResultType 'ParameterValue' -ToolTip $ToolTip)) } else { [void]$results.Add((New-FindStrCompletionResult -CompletionText ($Prefix + $CurrentValue) -ResultType 'ParameterValue' -ToolTip $ToolTip)) } } @($results.ToArray()) } function Get-FindStrSearchStringCompletions { param( [string]$CurrentValue, [string]$Prefix = '' ) $results = New-Object System.Collections.Generic.List[object] $toolTip = if ([string]::IsNullOrEmpty($Prefix)) { 'Search string.' } else { 'Literal search string.' } if ([string]::IsNullOrEmpty($CurrentValue)) { [void]$results.Add((New-FindStrCompletionResult -CompletionText ($Prefix + '<search-string>') -ResultType 'ParameterValue' -ToolTip $toolTip)) if (-not [string]::IsNullOrEmpty($Prefix)) { [void]$results.Add((New-FindStrCompletionResult -CompletionText ($Prefix + '"<search-string>"') -ResultType 'ParameterValue' -ToolTip $toolTip)) } return @($results.ToArray()) } if ($CurrentValue -eq '"') { [void]$results.Add((New-FindStrCompletionResult -CompletionText ($Prefix + '"<search-string>"') -ResultType 'ParameterValue' -ToolTip $toolTip)) return @($results.ToArray()) } @( New-FindStrCompletionResult -CompletionText ($Prefix + $CurrentValue) -ResultType 'ParameterValue' -ToolTip $toolTip ) } function Get-FindStrPathCompletions { param( [string]$CurrentValue, [string]$Prefix = '', [ValidateSet('File', 'Directory', 'Any')] [string]$Kind = 'Any', [string]$ToolTip = 'Path value.', [string]$Placeholder = '<path>', [bool]$AllowConsoleSentinel = $false, [string]$ListSeparator = '' ) $results = New-Object System.Collections.Generic.List[object] $typedValue = if ($null -eq $CurrentValue) { '' } else { $CurrentValue } $typedWord = ConvertFrom-FindStrTypedWord -Value $typedValue $cleanValue = $typedWord.Value # In a list value the whole list is re-quoted as one argument, so the # entries before the last separator ride along in front of each candidate. $valuePrefix = '' if ($ListSeparator -and $cleanValue.Contains($ListSeparator)) { $separatorIndex = $cleanValue.LastIndexOf($ListSeparator) $valuePrefix = $cleanValue.Substring(0, $separatorIndex + 1) $cleanValue = $cleanValue.Substring($separatorIndex + 1) } if ($AllowConsoleSentinel) { if ([string]::IsNullOrWhiteSpace($cleanValue) -or '/'.StartsWith($cleanValue, [System.StringComparison]::OrdinalIgnoreCase)) { [void]$results.Add((New-FindStrCompletionResult -CompletionText ($Prefix + '/') -ResultType 'ParameterValue' -ToolTip 'Read this value from console input.')) } } # Candidates are built on the directory text exactly as typed, so a typed # .\ or ./ prefix and the typed separator style survive. $directoryText = '' $separatorIndex = $cleanValue.LastIndexOfAny([char[]]@('\', '/')) if ($separatorIndex -ge 0) { $directoryText = $cleanValue.Substring(0, $separatorIndex + 1) } elseif ($cleanValue -match '^[A-Za-z]:') { $directoryText = $cleanValue.Substring(0, 2) } $parentPath = if ($directoryText) { $directoryText } else { '.' } $leaf = $cleanValue.Substring($directoryText.Length) $items = @() if (Test-Path -LiteralPath $parentPath -PathType Container -ErrorAction Ignore) { $items = @(Get-ChildItem -LiteralPath $parentPath -ErrorAction Ignore) } foreach ($item in $items) { if (-not [string]::IsNullOrWhiteSpace($leaf) -and -not $item.Name.StartsWith($leaf, [System.StringComparison]::OrdinalIgnoreCase)) { continue } if ($Kind -eq 'Directory' -and -not $item.PSIsContainer) { continue } $candidate = $directoryText + $item.Name if ($item.PSIsContainer) { $candidate += '\' } # A whole-word value starting with a dash would parse as a parameter, so # it gets the current-directory prefix, as PowerShell's own file completion does. if (-not ($Prefix -or $valuePrefix) -and $candidate -match '^[-\u2013-\u2015]') { $candidate = '.' + [System.IO.Path]::DirectorySeparatorChar + $candidate } $completionText = ConvertTo-FindStrQuotedValue -Value ($valuePrefix + $candidate) -QuoteChar $typedWord.Quote [void]$results.Add((New-FindStrCompletionResult -CompletionText ($Prefix + $completionText) -ResultType 'ParameterValue' -ToolTip $item.FullName -ListItemText $candidate)) } if ($results.Count -eq 0) { if ([string]::IsNullOrWhiteSpace($CurrentValue)) { [void]$results.Add((New-FindStrCompletionResult -CompletionText ($Prefix + $Placeholder) -ResultType 'ParameterValue' -ToolTip $ToolTip)) } else { [void]$results.Add((New-FindStrCompletionResult -CompletionText ($Prefix + $CurrentValue) -ResultType 'ParameterValue' -ToolTip $ToolTip)) } } @($results.ToArray()) } function Get-FindStrDirectoryListCompletions { param( [string]$Prefix, [string]$CurrentValue ) # A real multi-directory list must be quoted (an unquoted ';' ends the # PowerShell statement); each candidate re-quotes the whole list, so a # typed closing quote is neither swallowed nor left dangling. @(Get-FindStrPathCompletions -CurrentValue $CurrentValue -Prefix $Prefix -Kind 'Directory' -ToolTip 'Directory list entry.' -Placeholder '<dir[;dir...]>' -ListSeparator ';') } function Get-FindStrTerminalCompletions { param([string]$CurrentValue) $completionText = if ([string]::IsNullOrEmpty($CurrentValue)) { ' ' } else { $CurrentValue } @( New-FindStrCompletionResult -CompletionText $completionText -ResultType 'ParameterValue' -ToolTip 'No further arguments are valid after /?.' ) } function Complete-FindStr { param( [string]$wordToComplete, [System.Management.Automation.Language.CommandAst]$commandAst, [int]$cursorPosition ) $tokenState = Get-FindStrTokenState -Line $commandAst.ToString() -CursorPosition ($cursorPosition - $commandAst.Extent.StartOffset) $argumentState = Get-FindStrArgumentsFromTokenState -TokenState $tokenState $hasTrailingSpace = [string]::IsNullOrEmpty($wordToComplete) if ($hasTrailingSpace -and -not [string]::IsNullOrEmpty($argumentState.CurrentArgument)) { $currentWord = '' $argumentsBeforeCurrent = @($argumentState.ArgumentsBeforeCurrent + $argumentState.CurrentArgument) } else { $currentWord = if ($null -eq $argumentState.CurrentArgument) { '' } else { $argumentState.CurrentArgument } $argumentsBeforeCurrent = @($argumentState.ArgumentsBeforeCurrent) } $context = Get-FindStrCompletionContext -ArgumentsBeforeCurrent $argumentsBeforeCurrent $catalog = Get-FindStrCompletionCatalog $canCompleteSwitches = -not $context.InFilenameMode -and -not $context.HasBareSearchStrings $fileOperandMode = $context.HasBareSearchStrings -or $context.InFilenameMode if ($canCompleteSwitches -and -not [string]::IsNullOrEmpty($currentWord) -and $currentWord.StartsWith('/')) { $attachedInfo = Get-FindStrAttachedTokenInfo -Token $currentWord if ($null -ne $attachedInfo) { switch ($attachedInfo.RootKey) { '/a' { return @(Get-FindStrColorCompletion -Prefix $attachedInfo.Prefix -CurrentValue $attachedInfo.Value) } '/q' { return @(Get-FindStrPrefixedValueCompletions -Prefix $attachedInfo.Prefix -CurrentValue $attachedInfo.Value -Suggestions $catalog.QuietFlags -ToolTip $attachedInfo.Switch.Description -Placeholder '<qflags>') } '/f' { return @(Get-FindStrPathCompletions -CurrentValue $attachedInfo.Value -Prefix $attachedInfo.Prefix -Kind 'File' -ToolTip $attachedInfo.Switch.Description -Placeholder '<file-list>' -AllowConsoleSentinel $true) } '/g' { return @(Get-FindStrPathCompletions -CurrentValue $attachedInfo.Value -Prefix $attachedInfo.Prefix -Kind 'File' -ToolTip $attachedInfo.Switch.Description -Placeholder '<pattern-file>' -AllowConsoleSentinel $true) } '/d' { return @(Get-FindStrDirectoryListCompletions -Prefix $attachedInfo.Prefix -CurrentValue $attachedInfo.Value) } '/c' { return @(Get-FindStrSearchStringCompletions -CurrentValue $attachedInfo.Value -Prefix $attachedInfo.Prefix) } } } if ($context.HelpRequested) { return @(Get-FindStrTerminalCompletions -CurrentValue $currentWord) } return @(Get-FindStrSwitchCompletions -CurrentWord $currentWord) } if ($context.HelpRequested) { return @(Get-FindStrTerminalCompletions -CurrentValue $currentWord) } if ($fileOperandMode) { return @(Get-FindStrPathCompletions -CurrentValue $currentWord -Kind 'File' -ToolTip 'File to search.' -Placeholder '<file>') } if ($context.HasExplicitSearchSource) { if ([string]::IsNullOrWhiteSpace($currentWord)) { $results = New-Object System.Collections.Generic.List[object] foreach ($completion in @(Get-FindStrSwitchCompletions -CurrentWord $currentWord)) { [void]$results.Add($completion) } foreach ($completion in @(Get-FindStrPathCompletions -CurrentValue $currentWord -Kind 'File' -ToolTip 'File to search.' -Placeholder '<file>')) { [void]$results.Add($completion) } return @(Get-FindStrUniqueCompletions -Results $results.ToArray()) } return @(Get-FindStrPathCompletions -CurrentValue $currentWord -Kind 'File' -ToolTip 'File to search.' -Placeholder '<file>') } if ([string]::IsNullOrWhiteSpace($currentWord)) { $results = New-Object System.Collections.Generic.List[object] foreach ($completion in @(Get-FindStrSwitchCompletions -CurrentWord $currentWord)) { [void]$results.Add($completion) } foreach ($completion in @(Get-FindStrSearchStringCompletions -CurrentValue $currentWord)) { [void]$results.Add($completion) } return @(Get-FindStrUniqueCompletions -Results $results.ToArray()) } @(Get-FindStrSearchStringCompletions -CurrentValue $currentWord) } Register-ArgumentCompleter -Native -CommandName @('findstr', 'findstr.exe') -ScriptBlock { param($wordToComplete, $commandAst, $cursorPosition) Complete-FindStr -wordToComplete $wordToComplete -commandAst $commandAst -cursorPosition $cursorPosition } |