completers/tasklist_completer/tasklist_completer.ps1
|
# tasklist.exe tab completion for PowerShell # Builds completion data from tasklist built-in help. # Usage: . .\tasklist_completer.ps1 Set-StrictMode -Version 2.0 if (-not (Get-Variable -Name TasklistCompletionCatalog -Scope Script -ErrorAction Ignore)) { $script:TasklistCompletionCatalog = @{ Initialized = $false SwitchTokens = @() OptionToolTipsByToken = @{} ValueHintsByOption = @{} FilterNames = @() FilterOperatorsByName = @{} FilterValueHintsByName = @{} RuntimeSnapshot = @() RuntimeSnapshotUpdated = $null RuntimeSnapshotTtlSeconds = 15 UserNames = @() UserNamesUpdated = $null UserNamesTtlSeconds = 30 ServiceNames = @() ServiceNamesUpdated = $null ServiceNamesTtlSeconds = 60 WindowTitles = @() WindowTitlesUpdated = $null WindowTitlesTtlSeconds = 10 ModuleNames = @() ModuleNamesUpdated = $null ModuleNamesTtlSeconds = 60 MaxFilterValues = 500 } } function Test-TasklistCommandAvailable { if (Get-Command -Name tasklist.exe -ErrorAction SilentlyContinue) { return $true } if (Get-Command -Name tasklist -ErrorAction SilentlyContinue) { return $true } $false } function Invoke-TasklistCommandText { # Read-only tasklist.exe invocations only (help and listings): stdin closed, output read # asynchronously, bounded by a timeout, and never recorded in $Error. param([string[]]$Arguments) $command = Get-Command -Name tasklist.exe -ErrorAction Ignore if (-not $command) { return @() } try { $startInfo = [System.Diagnostics.ProcessStartInfo]::new() $startInfo.FileName = $command.Source foreach ($argument in $Arguments) { $startInfo.ArgumentList.Add($argument) } $startInfo.UseShellExecute = $false $startInfo.CreateNoWindow = $true $startInfo.RedirectStandardInput = $true $startInfo.RedirectStandardOutput = $true $startInfo.RedirectStandardError = $true $startInfo.StandardOutputEncoding = [Console]::OutputEncoding $process = [System.Diagnostics.Process]::Start($startInfo) try { $process.StandardInput.Close() $outputTask = $process.StandardOutput.ReadToEndAsync() $errorTask = $process.StandardError.ReadToEndAsync() if (-not $process.WaitForExit(5000)) { $process.Kill() return @() } $null = $errorTask.Result return @(($outputTask.Result -replace '\e\[[0-9;?]*[ -/]*[@-~]', '') -split '\r?\n' | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }) } finally { $process.Dispose() } } catch { Write-Debug "tasklist.exe $($Arguments -join ' ') failed: $($_.Exception.Message)" @() } } function Invoke-TasklistHelpText { Invoke-TasklistCommandText -Arguments @('/?') } function New-TasklistCompletionResult { param( [string]$CompletionText, [string]$ResultType, [string]$ToolTip ) if ([string]::IsNullOrWhiteSpace($ToolTip)) { $ToolTip = $CompletionText } [System.Management.Automation.CompletionResult]::new( $CompletionText, $CompletionText, $ResultType, $ToolTip ) } function New-TasklistValueItem { param( [string]$CompletionText, [string]$ToolTip ) if ([string]::IsNullOrWhiteSpace($ToolTip)) { $ToolTip = $CompletionText } [pscustomobject]@{ CompletionText = $CompletionText ToolTip = $ToolTip } } function Get-TasklistDefaultSwitchTokens { @('/S', '/U', '/P', '/M', '/SVC', '/APPS', '/V', '/FI', '/FO', '/NH', '/?') } function Get-TasklistDefaultOptionToolTips { @{ '/S' = 'Specifies the remote system to connect to.' '/U' = 'Specifies the user context under which the command should execute.' '/P' = 'Specifies the password for the given user context.' '/M' = 'Lists tasks using the given exe or dll name.' '/SVC' = 'Displays services hosted in each process.' '/APPS' = 'Displays Store Apps and their associated processes.' '/V' = 'Displays verbose task information.' '/FI' = 'Displays tasks that match the specified filter expression.' '/FO' = 'Specifies the output format: TABLE, LIST, or CSV.' '/NH' = 'Suppresses the header row for TABLE and CSV output.' '/?' = 'Displays tasklist help.' } } function Get-TasklistDefaultFilterOperators { @{ 'STATUS' = @('eq', 'ne') 'IMAGENAME' = @('eq', 'ne') 'PID' = @('eq', 'ne', 'gt', 'lt', 'ge', 'le') 'SESSION' = @('eq', 'ne', 'gt', 'lt', 'ge', 'le') 'SESSIONNAME' = @('eq', 'ne') 'CPUTIME' = @('eq', 'ne', 'gt', 'lt', 'ge', 'le') 'MEMUSAGE' = @('eq', 'ne', 'gt', 'lt', 'ge', 'le') 'USERNAME' = @('eq', 'ne') 'SERVICES' = @('eq', 'ne') 'WINDOWTITLE' = @('eq', 'ne') 'MODULES' = @('eq', 'ne') } } function Get-TasklistDefaultFilterValueHints { @{ 'STATUS' = @('RUNNING', 'SUSPENDED', 'NOT RESPONDING', 'UNKNOWN') } } function Get-TasklistParameterMap { param([string[]]$Lines) $result = @{} $inParameterList = $false $currentToken = $null foreach ($line in $Lines) { if ($line -match '^\s*Parameter List:\s*$') { $inParameterList = $true continue } if (-not $inParameterList) { continue } if ($line -match '^\s*Filters:\s*$' -or $line -match '^\s*Examples?:\s*$') { break } if ($line -match '^\s*(/[A-Za-z?][A-Za-z0-9?]*)\b') { $currentToken = $matches[1].ToUpperInvariant() if (-not $result.ContainsKey($currentToken)) { $result[$currentToken] = New-Object System.Collections.Generic.List[string] } $result[$currentToken].Add($line.Trim()) continue } if ($currentToken -and -not [string]::IsNullOrWhiteSpace($line)) { $result[$currentToken].Add($line.Trim()) } } $parameterMap = @{} foreach ($token in $result.Keys) { $parameterMap[$token] = @($result[$token]) } $parameterMap } function Get-TasklistFilterDefinitions { param([string[]]$Lines) $definitions = @{} $inFilters = $false $currentFilterName = $null foreach ($line in $Lines) { if ($line -match '^\s*Filters:\s*$') { $inFilters = $true continue } if (-not $inFilters) { continue } if ($line -match '^\s*(NOTE:|Examples?:)\b') { break } if ($line -match '^\s*Filter Name\s+Valid Operators' -or $line -match '^\s*-{3,}') { continue } if ($line -match '^\s*([A-Z][A-Z0-9]+)\s+([a-z,\s]+?)\s{2,}(.+?)\s*$') { $currentFilterName = $matches[1].ToUpperInvariant() $definitions[$currentFilterName] = [pscustomobject]@{ FilterName = $currentFilterName Operators = @($matches[2] -split '\s*,\s*' | Where-Object { $_ }) ValueLines = [System.Collections.Generic.List[string]]::new() } if (-not [string]::IsNullOrWhiteSpace($matches[3])) { $definitions[$currentFilterName].ValueLines.Add($matches[3].Trim()) } continue } if ($currentFilterName -and -not [string]::IsNullOrWhiteSpace($line)) { $definitions[$currentFilterName].ValueLines.Add($line.Trim()) } } @($definitions.Values) } function Get-TasklistDocumentedFilterValueHints { param( [string]$FilterName, [string[]]$ValueLines ) switch ($FilterName.ToUpperInvariant()) { 'STATUS' { $joined = ($ValueLines -join ' | ') @( $joined -split '\|' | ForEach-Object { $_.Trim() } | Where-Object { $_ } ) } default { @() } } } function Initialize-TasklistCompletionCatalog { if ($script:TasklistCompletionCatalog.Initialized) { return } $defaultSwitchTokens = Get-TasklistDefaultSwitchTokens $script:TasklistCompletionCatalog.SwitchTokens = @($defaultSwitchTokens) $script:TasklistCompletionCatalog.OptionToolTipsByToken = Get-TasklistDefaultOptionToolTips $script:TasklistCompletionCatalog.ValueHintsByOption = @{ '/FO' = @('TABLE', 'LIST', 'CSV') } $defaultFilterOperators = Get-TasklistDefaultFilterOperators $defaultFilterValueHints = Get-TasklistDefaultFilterValueHints $script:TasklistCompletionCatalog.FilterNames = @($defaultFilterOperators.Keys | Sort-Object) $script:TasklistCompletionCatalog.FilterOperatorsByName = @{} $script:TasklistCompletionCatalog.FilterValueHintsByName = @{} foreach ($filterName in $defaultFilterOperators.Keys) { $script:TasklistCompletionCatalog.FilterOperatorsByName[$filterName] = @($defaultFilterOperators[$filterName]) } foreach ($filterName in $defaultFilterValueHints.Keys) { $script:TasklistCompletionCatalog.FilterValueHintsByName[$filterName] = @($defaultFilterValueHints[$filterName]) } $helpLines = Invoke-TasklistHelpText if ($helpLines -and $helpLines.Count -gt 0) { $parameterMap = Get-TasklistParameterMap -Lines $helpLines if ($parameterMap.Count -gt 0) { $orderedTokens = [System.Collections.Generic.List[string]]::new() $seenTokens = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) foreach ($token in $defaultSwitchTokens) { if ($seenTokens.Add($token)) { $orderedTokens.Add($token) } } foreach ($token in @($parameterMap.Keys | Sort-Object)) { if ($seenTokens.Add($token)) { $orderedTokens.Add($token) } $script:TasklistCompletionCatalog.OptionToolTipsByToken[$token] = ($parameterMap[$token] -join ' ') } $script:TasklistCompletionCatalog.SwitchTokens = @($orderedTokens) } $filterDefinitions = Get-TasklistFilterDefinitions -Lines $helpLines if ($filterDefinitions.Count -gt 0) { $filterNames = [System.Collections.Generic.List[string]]::new() foreach ($definition in $filterDefinitions | Sort-Object FilterName) { $filterNames.Add($definition.FilterName) $script:TasklistCompletionCatalog.FilterOperatorsByName[$definition.FilterName] = @($definition.Operators) $documentedHints = Get-TasklistDocumentedFilterValueHints -FilterName $definition.FilterName -ValueLines @($definition.ValueLines) if (@($documentedHints).Count -gt 0) { $script:TasklistCompletionCatalog.FilterValueHintsByName[$definition.FilterName] = @($documentedHints) } } $script:TasklistCompletionCatalog.FilterNames = @($filterNames) } } $script:TasklistCompletionCatalog.Initialized = $true } function Update-TasklistRuntimeSnapshotCache { $lastUpdated = $script:TasklistCompletionCatalog.RuntimeSnapshotUpdated if ($null -ne $lastUpdated) { $cacheAge = (Get-Date) - $lastUpdated if ($cacheAge.TotalSeconds -lt $script:TasklistCompletionCatalog.RuntimeSnapshotTtlSeconds) { return } } $csvLines = @(Invoke-TasklistCommandText -Arguments @('/FO', 'CSV', '/NH')) if ($csvLines.Count -eq 0) { $script:TasklistCompletionCatalog.RuntimeSnapshot = @() $script:TasklistCompletionCatalog.RuntimeSnapshotUpdated = Get-Date return } $rows = @( $csvLines | ConvertFrom-Csv -Header @('ImageName', 'PID', 'SessionName', 'SessionId', 'MemUsage') ) $script:TasklistCompletionCatalog.RuntimeSnapshot = @($rows) $script:TasklistCompletionCatalog.RuntimeSnapshotUpdated = Get-Date } function Update-TasklistUserNameCache { $lastUpdated = $script:TasklistCompletionCatalog.UserNamesUpdated if ($null -ne $lastUpdated) { $cacheAge = (Get-Date) - $lastUpdated if ($cacheAge.TotalSeconds -lt $script:TasklistCompletionCatalog.UserNamesTtlSeconds) { return } } $nameSet = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) $userNames = [System.Collections.Generic.List[string]]::new() # Get-Process -IncludeUserName only sees other accounts when elevated, so always seed # the current user and the well-known service principals that own most of the table. if ($env:USERDOMAIN -and $env:USERNAME) { $currentUserName = "$($env:USERDOMAIN)\$($env:USERNAME)" if ($nameSet.Add($currentUserName)) { $userNames.Add($currentUserName) } } foreach ($wellKnown in @('NT AUTHORITY\SYSTEM', 'NT AUTHORITY\LOCAL SERVICE', 'NT AUTHORITY\NETWORK SERVICE')) { if ($nameSet.Add($wellKnown)) { $userNames.Add($wellKnown) } } foreach ($process in @(Get-Process -IncludeUserName -ErrorAction Ignore)) { if ($process.UserName -and $process.UserName -ne 'N/A' -and $nameSet.Add($process.UserName)) { $userNames.Add($process.UserName) } } $script:TasklistCompletionCatalog.UserNames = @($userNames | Sort-Object) $script:TasklistCompletionCatalog.UserNamesUpdated = Get-Date } function Update-TasklistServiceNameCache { $lastUpdated = $script:TasklistCompletionCatalog.ServiceNamesUpdated if ($null -ne $lastUpdated) { $cacheAge = (Get-Date) - $lastUpdated if ($cacheAge.TotalSeconds -lt $script:TasklistCompletionCatalog.ServiceNamesTtlSeconds) { return } } $serviceNames = @( Get-Service -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Name | Sort-Object -Unique ) $script:TasklistCompletionCatalog.ServiceNames = @($serviceNames) $script:TasklistCompletionCatalog.ServiceNamesUpdated = Get-Date } function Update-TasklistWindowTitleCache { $lastUpdated = $script:TasklistCompletionCatalog.WindowTitlesUpdated if ($null -ne $lastUpdated) { $cacheAge = (Get-Date) - $lastUpdated if ($cacheAge.TotalSeconds -lt $script:TasklistCompletionCatalog.WindowTitlesTtlSeconds) { return } } $windowTitles = @( Get-Process -ErrorAction SilentlyContinue | Where-Object { -not [string]::IsNullOrWhiteSpace($_.MainWindowTitle) } | Select-Object -ExpandProperty MainWindowTitle | Sort-Object -Unique ) $script:TasklistCompletionCatalog.WindowTitles = @($windowTitles) $script:TasklistCompletionCatalog.WindowTitlesUpdated = Get-Date } function Update-TasklistModuleNameCache { $lastUpdated = $script:TasklistCompletionCatalog.ModuleNamesUpdated if ($null -ne $lastUpdated) { $cacheAge = (Get-Date) - $lastUpdated if ($cacheAge.TotalSeconds -lt $script:TasklistCompletionCatalog.ModuleNamesTtlSeconds) { return } } # 'tasklist /M /FO CSV /NH' lists every process with its modules in one read-only call # (~0.7 s here versus ~3.5 s and hundreds of access-denied records for Get-Process -Module). $moduleSet = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) $rows = @( Invoke-TasklistCommandText -Arguments @('/M', '/FO', 'CSV', '/NH') | ConvertFrom-Csv -Header @('ImageName', 'PID', 'Modules') ) foreach ($row in $rows) { if ([string]::IsNullOrWhiteSpace($row.Modules) -or $row.Modules -eq 'N/A') { continue } foreach ($moduleName in ($row.Modules -split ',')) { $trimmed = $moduleName.Trim() if ($trimmed) { $null = $moduleSet.Add($trimmed) } } } $script:TasklistCompletionCatalog.ModuleNames = @($moduleSet | Sort-Object) $script:TasklistCompletionCatalog.ModuleNamesUpdated = Get-Date } function Get-TasklistTokenState { param( [string]$Line, [int]$CursorPosition ) if ([string]::IsNullOrEmpty($Line)) { return [pscustomobject]@{ ArgumentTokens = @() CurrentToken = '' TokensBeforeCurrent = @() HasTrailingDelimiter = $false } } $safeCursor = [Math]::Min([Math]::Max($CursorPosition, 0), $Line.Length) $linePrefix = $Line.Substring(0, $safeCursor) $tokenBuilder = [System.Text.StringBuilder]::new() $commandTokens = [System.Collections.Generic.List[string]]::new() $inQuotes = $false foreach ($character in $linePrefix.ToCharArray()) { if ($character -eq '"') { $null = $tokenBuilder.Append($character) $inQuotes = -not $inQuotes continue } if ([char]::IsWhiteSpace($character) -and -not $inQuotes) { if ($tokenBuilder.Length -gt 0) { $commandTokens.Add($tokenBuilder.ToString()) $null = $tokenBuilder.Clear() } continue } $null = $tokenBuilder.Append($character) } $hasTrailingDelimiter = $false if (-not $inQuotes -and $linePrefix.Length -gt 0) { $hasTrailingDelimiter = [char]::IsWhiteSpace($linePrefix[$linePrefix.Length - 1]) } $currentToken = if ($hasTrailingDelimiter) { '' } else { $tokenBuilder.ToString() } if (-not $hasTrailingDelimiter -and $tokenBuilder.Length -gt 0) { $commandTokens.Add($currentToken) } [object[]]$argumentTokens = if ($commandTokens.Count -gt 1) { @($commandTokens | Select-Object -Skip 1) } else { @() } [object[]]$tokensBeforeCurrent = if ($hasTrailingDelimiter) { @($argumentTokens) } elseif ($argumentTokens.Count -gt 0) { @($argumentTokens | Select-Object -First ($argumentTokens.Count - 1)) } else { @() } [pscustomobject]@{ ArgumentTokens = $argumentTokens CurrentToken = $currentToken TokensBeforeCurrent = $tokensBeforeCurrent HasTrailingDelimiter = $hasTrailingDelimiter } } function Get-TasklistExpectedValueOption { param([string[]]$TokensBeforeCurrent) if (-not $TokensBeforeCurrent -or $TokensBeforeCurrent.Count -eq 0) { return $null } $lastToken = $TokensBeforeCurrent[-1].ToUpperInvariant() switch ($lastToken) { '/S' { '/S' } '/U' { '/U' } '/P' { '/P' } '/M' { '/M' } '/FO' { '/FO' } default { $null } } } function Resolve-TasklistFilterName { param([string]$FilterName) foreach ($knownFilterName in $script:TasklistCompletionCatalog.FilterNames) { if ($knownFilterName.Equals($FilterName, [System.StringComparison]::OrdinalIgnoreCase)) { return $knownFilterName } } $null } function Format-TasklistFilterExpressionCompletion { param( [string]$Expression, [bool]$CloseQuote = $false ) if ($CloseQuote) { return '"' + $Expression + '"' } '"' + $Expression } function Test-TasklistClosedQuotedExpression { param([string]$Expression) if ([string]::IsNullOrEmpty($Expression)) { return $false } $trimmedExpression = $Expression.Trim() if (-not $trimmedExpression.StartsWith('"')) { return $false } $quoteCount = 0 foreach ($character in $trimmedExpression.ToCharArray()) { if ($character -eq '"') { $quoteCount++ } } ($quoteCount -ge 2) -and (($quoteCount % 2) -eq 0) -and $trimmedExpression.EndsWith('"') } function Get-TasklistFilterValueItems { param( [string]$FilterName, [string]$TypedValue ) switch ($FilterName.ToUpperInvariant()) { 'STATUS' { @( $script:TasklistCompletionCatalog.FilterValueHintsByName['STATUS'] | ForEach-Object { New-TasklistValueItem -CompletionText $_ -ToolTip "Status value $_" } ) } 'CPUTIME' { @( New-TasklistValueItem -CompletionText '00:00:00' -ToolTip 'CPU time in hh:mm:ss format' New-TasklistValueItem -CompletionText '00:01:00' -ToolTip 'CPU time in hh:mm:ss format' New-TasklistValueItem -CompletionText '01:00:00' -ToolTip 'CPU time in hh:mm:ss format' ) } 'MEMUSAGE' { @( New-TasklistValueItem -CompletionText '1024' -ToolTip 'Memory usage in KB' New-TasklistValueItem -CompletionText '4096' -ToolTip 'Memory usage in KB' New-TasklistValueItem -CompletionText '65536' -ToolTip 'Memory usage in KB' ) } 'IMAGENAME' { Update-TasklistRuntimeSnapshotCache @( $script:TasklistCompletionCatalog.RuntimeSnapshot | ForEach-Object { $_.ImageName } | Where-Object { $_ } | Sort-Object -Unique | ForEach-Object { New-TasklistValueItem -CompletionText $_ -ToolTip "Image name $_" } ) } 'PID' { Update-TasklistRuntimeSnapshotCache @( $script:TasklistCompletionCatalog.RuntimeSnapshot | ForEach-Object { [string]$_.PID } | Where-Object { $_ -match '^\d+$' } | Sort-Object -Property { [int]$_ } -Unique | ForEach-Object { New-TasklistValueItem -CompletionText $_ -ToolTip "Process ID $_" } ) } 'SESSION' { Update-TasklistRuntimeSnapshotCache @( $script:TasklistCompletionCatalog.RuntimeSnapshot | ForEach-Object { [string]$_.SessionId } | Where-Object { $_ -match '^\d+$' } | Sort-Object -Property { [int]$_ } -Unique | ForEach-Object { New-TasklistValueItem -CompletionText $_ -ToolTip "Session ID $_" } ) } 'SESSIONNAME' { Update-TasklistRuntimeSnapshotCache @( $script:TasklistCompletionCatalog.RuntimeSnapshot | ForEach-Object { $_.SessionName } | Where-Object { $_ } | Sort-Object -Unique | ForEach-Object { New-TasklistValueItem -CompletionText $_ -ToolTip "Session name $_" } ) } 'USERNAME' { Update-TasklistUserNameCache @( $script:TasklistCompletionCatalog.UserNames | ForEach-Object { New-TasklistValueItem -CompletionText $_ -ToolTip "User name $_" } ) } 'SERVICES' { Update-TasklistServiceNameCache @( $script:TasklistCompletionCatalog.ServiceNames | ForEach-Object { New-TasklistValueItem -CompletionText $_ -ToolTip "Service name $_" } ) } 'WINDOWTITLE' { Update-TasklistWindowTitleCache @( $script:TasklistCompletionCatalog.WindowTitles | ForEach-Object { New-TasklistValueItem -CompletionText $_ -ToolTip "Window title $_" } ) } 'MODULES' { Update-TasklistModuleNameCache # Thousands of DLL names are loaded on a typical box; narrow on the typed prefix # and cap the list so the menu stays usable. $modulePrefix = if ($null -eq $TypedValue) { '' } else { $TypedValue } @( $script:TasklistCompletionCatalog.ModuleNames | Where-Object { $_.StartsWith($modulePrefix, [System.StringComparison]::OrdinalIgnoreCase) } | Select-Object -First $script:TasklistCompletionCatalog.MaxFilterValues | ForEach-Object { New-TasklistValueItem -CompletionText $_ -ToolTip "Module name $_" } ) } default { @() } } } function Get-TasklistSwitchCompletions { param([string]$CurrentWord) $prefix = if ([string]::IsNullOrEmpty($CurrentWord)) { '' } else { $CurrentWord } $script:TasklistCompletionCatalog.SwitchTokens | Where-Object { $_ -like ([System.Management.Automation.WildcardPattern]::Escape($prefix) + '*') } | ForEach-Object { $toolTip = $script:TasklistCompletionCatalog.OptionToolTipsByToken[$_] New-TasklistCompletionResult -CompletionText $_ -ResultType 'ParameterName' -ToolTip $toolTip } } function Get-TasklistFormatCompletions { param([string]$CurrentWord) $prefix = if ([string]::IsNullOrEmpty($CurrentWord)) { '' } else { $CurrentWord.Trim('"') } $script:TasklistCompletionCatalog.ValueHintsByOption['/FO'] | Where-Object { $_ -like ([System.Management.Automation.WildcardPattern]::Escape($prefix) + '*') } | ForEach-Object { New-TasklistCompletionResult -CompletionText $_ -ResultType 'ParameterValue' -ToolTip "Output format $_" } } function Get-TasklistFilterExpressionCompletions { param([string]$CurrentWord) $filterWord = if ([string]::IsNullOrEmpty($CurrentWord)) { '"' } else { $CurrentWord } if (-not $filterWord.StartsWith('"')) { $filterWord = '"' + $filterWord } $expression = $filterWord.Substring(1) $expressionTrimmed = $expression.TrimStart() $endsWithWhitespace = $expression.Length -gt 0 -and [char]::IsWhiteSpace($expression[$expression.Length - 1]) $parts = @($expressionTrimmed -split '\s+' | Where-Object { $_ }) if ($parts.Count -eq 0) { return @( $script:TasklistCompletionCatalog.FilterNames | ForEach-Object { $completionText = Format-TasklistFilterExpressionCompletion -Expression "$_ " -CloseQuote $false New-TasklistCompletionResult -CompletionText $completionText -ResultType 'ParameterValue' -ToolTip "Filter $_" } ) } $partialFilterName = $parts[0] if ($parts.Count -eq 1 -and -not $endsWithWhitespace) { return @( $script:TasklistCompletionCatalog.FilterNames | Where-Object { $_ -like ([System.Management.Automation.WildcardPattern]::Escape($partialFilterName) + '*') } | ForEach-Object { $completionText = Format-TasklistFilterExpressionCompletion -Expression "$_ " -CloseQuote $false New-TasklistCompletionResult -CompletionText $completionText -ResultType 'ParameterValue' -ToolTip "Filter $_" } ) } $filterName = Resolve-TasklistFilterName -FilterName $partialFilterName if (-not $filterName) { return @() } $operators = @($script:TasklistCompletionCatalog.FilterOperatorsByName[$filterName]) if ($parts.Count -eq 1) { return @( $operators | ForEach-Object { $completionText = Format-TasklistFilterExpressionCompletion -Expression "$filterName $_ " -CloseQuote $false New-TasklistCompletionResult -CompletionText $completionText -ResultType 'ParameterValue' -ToolTip "$filterName $_" } ) } $partialOperator = $parts[1] if ($parts.Count -eq 2 -and -not $endsWithWhitespace) { return @( $operators | Where-Object { $_ -like ([System.Management.Automation.WildcardPattern]::Escape($partialOperator) + '*') } | ForEach-Object { $completionText = Format-TasklistFilterExpressionCompletion -Expression "$filterName $_ " -CloseQuote $false New-TasklistCompletionResult -CompletionText $completionText -ResultType 'ParameterValue' -ToolTip "$filterName $_" } ) } $resolvedOperator = $null foreach ($operator in $operators) { if ($operator.Equals($partialOperator, [System.StringComparison]::OrdinalIgnoreCase)) { $resolvedOperator = $operator break } } if (-not $resolvedOperator) { return @() } $typedValue = if ($parts.Count -gt 2) { ($parts[2..($parts.Count - 1)] -join ' ') } else { '' } $valueItems = @(Get-TasklistFilterValueItems -FilterName $filterName -TypedValue $typedValue) if ($valueItems.Count -eq 0) { return @() } @( $valueItems | Where-Object { $_.CompletionText -like ([System.Management.Automation.WildcardPattern]::Escape($typedValue) + '*') } | ForEach-Object { $completionText = Format-TasklistFilterExpressionCompletion -Expression "$filterName $resolvedOperator $($_.CompletionText)" -CloseQuote $true New-TasklistCompletionResult -CompletionText $completionText -ResultType 'ParameterValue' -ToolTip $_.ToolTip } ) } function Get-TasklistFilterExpressionWord { param( [string[]]$TokensBeforeCurrent, [string]$CurrentToken, [bool]$HasTrailingDelimiter ) if (-not $TokensBeforeCurrent -or $TokensBeforeCurrent.Count -eq 0) { return $null } $fiIndex = -1 for ($index = $TokensBeforeCurrent.Count - 1; $index -ge 0; $index--) { if ($TokensBeforeCurrent[$index].Equals('/FI', [System.StringComparison]::OrdinalIgnoreCase)) { $fiIndex = $index break } } if ($fiIndex -lt 0) { return $null } $expressionTokensBeforeCurrent = @() if ($fiIndex + 1 -lt $TokensBeforeCurrent.Count) { $expressionTokensBeforeCurrent = @($TokensBeforeCurrent[($fiIndex + 1)..($TokensBeforeCurrent.Count - 1)]) } $hasCompletedQuotedExpression = $false $containsFollowingSwitch = $false if ($expressionTokensBeforeCurrent.Count -gt 0) { $hasCompletedQuotedExpression = (Test-TasklistClosedQuotedExpression -Expression ($expressionTokensBeforeCurrent -join ' ')) -and ($HasTrailingDelimiter -or -not [string]::IsNullOrEmpty($CurrentToken)) $containsFollowingSwitch = [bool]@( $expressionTokensBeforeCurrent | Where-Object { -not [string]::IsNullOrEmpty($_) -and $_.StartsWith('/') } | Select-Object -First 1 ) } if ($hasCompletedQuotedExpression -or $containsFollowingSwitch) { return $null } $expressionTokens = @($expressionTokensBeforeCurrent) if (-not [string]::IsNullOrEmpty($CurrentToken)) { $expressionTokens += $CurrentToken } $expression = $expressionTokens -join ' ' if ($HasTrailingDelimiter) { $expression += ' ' } $expression } function Complete-Tasklist { param( [string]$wordToComplete, [System.Management.Automation.Language.CommandAst]$commandAst, [int]$cursorPosition ) if (-not (Test-TasklistCommandAvailable)) { return @() } Initialize-TasklistCompletionCatalog # $cursorPosition is line-absolute; the extent text is command-relative. $relativeCursor = [Math]::Max(0, $cursorPosition - $commandAst.Extent.StartOffset) $completionLine = $commandAst.Extent.Text + (' ' * [Math]::Max(0, $relativeCursor - $commandAst.Extent.Text.Length)) $tokenState = Get-TasklistTokenState -Line $completionLine -CursorPosition $relativeCursor $currentWord = $tokenState.CurrentToken $tokensBeforeCurrent = @($tokenState.TokensBeforeCurrent) $hasTrailingDelimiter = [bool]$tokenState.HasTrailingDelimiter if ([string]::IsNullOrEmpty($wordToComplete)) { $currentWord = '' $tokensBeforeCurrent = @($tokenState.ArgumentTokens) } if (-not [string]::IsNullOrEmpty($currentWord) -and $currentWord.StartsWith('/')) { return @(Get-TasklistSwitchCompletions -CurrentWord $currentWord) } $filterExpressionWord = Get-TasklistFilterExpressionWord -TokensBeforeCurrent $tokensBeforeCurrent -CurrentToken $currentWord -HasTrailingDelimiter $hasTrailingDelimiter if ($null -ne $filterExpressionWord) { return @(Get-TasklistFilterExpressionCompletions -CurrentWord $filterExpressionWord) } $expectedValueOption = Get-TasklistExpectedValueOption -TokensBeforeCurrent $tokensBeforeCurrent switch ($expectedValueOption) { '/FO' { return @(Get-TasklistFormatCompletions -CurrentWord $currentWord) } '/S' { return @() } '/U' { return @() } '/P' { return @() } '/M' { return @() } } if ([string]::IsNullOrEmpty($currentWord)) { return @(Get-TasklistSwitchCompletions -CurrentWord '') } @() } Register-ArgumentCompleter -Native -CommandName @('tasklist', 'tasklist.exe') -ScriptBlock { param($wordToComplete, $commandAst, $cursorPosition) Complete-Tasklist -wordToComplete $wordToComplete -commandAst $commandAst -cursorPosition $cursorPosition } |