completers/tskill_completer/tskill_completer.ps1
|
# tskill.exe tab completion for PowerShell # Provides lightweight native argument completion for process targets and documented switches. # Usage: . .\tskill_completer.ps1 Set-StrictMode -Version 2.0 if (-not (Get-Variable -Name TskillCompletionCatalog -Scope Script -ErrorAction Ignore)) { $script:TskillCompletionCatalog = @{ SwitchTokens = @('/SERVER:', '/ID:', '/A', '/V', '/?') ProcessEntries = @() ProcessCacheUpdated = $null ProcessCacheTtlSeconds = 2 SessionIds = @() SessionCacheUpdated = $null SessionCacheTtlSeconds = 20 } } function Test-TskillCommandAvailable { if (Get-Command -Name tskill.exe -ErrorAction SilentlyContinue) { return $true } if (Get-Command -Name tskill -ErrorAction SilentlyContinue) { return $true } $false } function New-TskillCompletionResult { param( [string]$CompletionText, [string]$ResultType, [string]$ToolTip ) if ([string]::IsNullOrWhiteSpace($ToolTip)) { $ToolTip = $CompletionText } [System.Management.Automation.CompletionResult]::new( $CompletionText, $CompletionText, $ResultType, $ToolTip ) } function Get-TskillCurrentToken { param( [string]$Line, [int]$CursorPosition, [string]$Fallback ) if ([string]::IsNullOrEmpty($Line)) { return $Fallback } if ($CursorPosition -gt $Line.Length) { return '' } $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 Update-TskillProcessCache { $lastUpdated = $script:TskillCompletionCatalog.ProcessCacheUpdated if ($null -ne $lastUpdated) { $cacheAge = (Get-Date) - $lastUpdated if ($cacheAge.TotalSeconds -lt $script:TskillCompletionCatalog.ProcessCacheTtlSeconds) { return } } $nameSet = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) $idSet = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) $processNames = [System.Collections.Generic.List[string]]::new() $processIds = [System.Collections.Generic.List[int]]::new() foreach ($process in @(Get-Process -ErrorAction SilentlyContinue)) { if ($process.ProcessName -and $nameSet.Add($process.ProcessName)) { $processNames.Add($process.ProcessName) } $processIdText = [string]$process.Id if ($processIdText -and $idSet.Add($processIdText)) { $processIds.Add($process.Id) } } $entries = [System.Collections.Generic.List[object]]::new() foreach ($processName in @($processNames | Sort-Object)) { $entries.Add([pscustomobject]@{ CompletionText = $processName ResultType = 'ParameterValue' ToolTip = "Process name $processName" }) } foreach ($processId in @($processIds | Sort-Object)) { $processIdText = [string]$processId $entries.Add([pscustomobject]@{ CompletionText = $processIdText ResultType = 'ParameterValue' ToolTip = "Process ID $processIdText" }) } $script:TskillCompletionCatalog.ProcessEntries = @($entries) $script:TskillCompletionCatalog.ProcessCacheUpdated = Get-Date } function Update-TskillSessionIdCache { $lastUpdated = $script:TskillCompletionCatalog.SessionCacheUpdated if ($null -ne $lastUpdated) { $cacheAge = (Get-Date) - $lastUpdated if ($cacheAge.TotalSeconds -lt $script:TskillCompletionCatalog.SessionCacheTtlSeconds) { return } } $lines = @() if (Get-Command -Name qwinsta.exe -ErrorAction SilentlyContinue) { $lines = @(& qwinsta.exe 2>$null) } elseif (Get-Command -Name query.exe -ErrorAction SilentlyContinue) { $lines = @(& query.exe session 2>$null) } $sessionIds = foreach ($line in $lines) { foreach ($match in [regex]::Matches( $line, '(?<=\s)(\d+)(?=\s+(?:Active|Disc|Conn|Listen|Idle|Down|Reset|Init|Disconnected)\b)' )) { $match.Groups[1].Value } } $script:TskillCompletionCatalog.SessionIds = @($sessionIds | Sort-Object -Unique) $script:TskillCompletionCatalog.SessionCacheUpdated = Get-Date } function Get-TskillExpectedInlineValueOption { param([string]$CurrentWord) if ([string]::IsNullOrEmpty($CurrentWord)) { return $null } if ($CurrentWord.StartsWith('/ID:', [System.StringComparison]::OrdinalIgnoreCase)) { return '/ID:' } if ($CurrentWord.StartsWith('/SERVER:', [System.StringComparison]::OrdinalIgnoreCase)) { return '/SERVER:' } $null } function Get-TskillProcessCompletions { param([string]$CurrentWord) Update-TskillProcessCache $prefix = if ([string]::IsNullOrEmpty($CurrentWord)) { '' } else { $CurrentWord.Trim('"') } $script:TskillCompletionCatalog.ProcessEntries | Where-Object { $_.CompletionText -like ([System.Management.Automation.WildcardPattern]::Escape($prefix) + '*') } | ForEach-Object { New-TskillCompletionResult -CompletionText $_.CompletionText -ResultType $_.ResultType -ToolTip $_.ToolTip } } function Get-TskillSessionIdCompletions { param([string]$CurrentWord) Update-TskillSessionIdCache $typedValue = $CurrentWord.Substring(4) $script:TskillCompletionCatalog.SessionIds | Where-Object { $_ -like ([System.Management.Automation.WildcardPattern]::Escape($typedValue) + '*') } | ForEach-Object { $completionText = "/ID:$_" New-TskillCompletionResult -CompletionText $completionText -ResultType 'ParameterValue' -ToolTip "Session ID $_" } } function Get-TskillServerValueCompletions { param([string]$CurrentWord) $typedValue = $CurrentWord.Substring(8) if ([string]::IsNullOrEmpty($typedValue)) { return @(New-TskillCompletionResult -CompletionText '/SERVER:' -ResultType 'ParameterName' -ToolTip 'Remote server name') } @() } function Complete-Tskill { param( [string]$wordToComplete, [System.Management.Automation.Language.CommandAst]$commandAst, [int]$cursorPosition ) if (-not (Test-TskillCommandAvailable)) { return @() } $line = $commandAst.ToString() $safeCursor = [Math]::Min([Math]::Max($cursorPosition - $commandAst.Extent.StartOffset, 0), $line.Length) $linePrefix = $line.Substring(0, $safeCursor) $commandTokens = @([regex]::Matches($linePrefix, '"[^"]*"|\S+') | ForEach-Object { $_.Value }) [object[]]$argumentTokens = if ($commandTokens.Count -gt 1) { @($commandTokens | Select-Object -Skip 1) } else { @() } $currentWord = if ([string]::IsNullOrEmpty($wordToComplete)) { Get-TskillCurrentToken -Line $line -CursorPosition ($cursorPosition - $commandAst.Extent.StartOffset) -Fallback $wordToComplete } else { $wordToComplete } $hasTrailingSpace = [string]::IsNullOrEmpty($currentWord) -and (($linePrefix -match '\s$') -or (($cursorPosition - $commandAst.Extent.StartOffset) -gt $line.Length)) [object[]]$tokensBeforeCurrent = if ($hasTrailingSpace) { @($argumentTokens) } elseif ($argumentTokens.Count -gt 0) { @($argumentTokens | Select-Object -First ($argumentTokens.Count - 1)) } else { @() } $expectedInlineValueOption = Get-TskillExpectedInlineValueOption -CurrentWord $currentWord switch ($expectedInlineValueOption) { '/ID:' { return @(Get-TskillSessionIdCompletions -CurrentWord $currentWord) } '/SERVER:' { return @(Get-TskillServerValueCompletions -CurrentWord $currentWord) } } if (-not [string]::IsNullOrEmpty($currentWord) -and $currentWord.StartsWith('/')) { return @( $script:TskillCompletionCatalog.SwitchTokens | Where-Object { $_ -like ([System.Management.Automation.WildcardPattern]::Escape($currentWord) + '*') } | ForEach-Object { New-TskillCompletionResult -CompletionText $_ -ResultType 'ParameterName' -ToolTip $_ } ) } $hasCompletedPositional = $false foreach ($token in $tokensBeforeCurrent) { if (-not [string]::IsNullOrWhiteSpace($token) -and -not $token.StartsWith('/')) { $hasCompletedPositional = $true break } } if (-not $hasCompletedPositional) { return @(Get-TskillProcessCompletions -CurrentWord $currentWord) } @() } Register-ArgumentCompleter -Native -CommandName @('tskill', 'tskill.exe') -ScriptBlock { param($wordToComplete, $commandAst, $cursorPosition) Complete-Tskill -wordToComplete $wordToComplete -commandAst $commandAst -cursorPosition $cursorPosition } |