completers/psping_completer/psping_completer.ps1
|
# psping tab completion for PowerShell # Hybrid completer for PsPing modes, switches, and value-aware placeholders. Set-StrictMode -Version 2.0 if (-not (Get-Variable -Name PsPingCompletionCatalog -Scope Script -ErrorAction Ignore)) { $script:PsPingCompletionCatalog = @{ Initialized = $false CommandName = $null RootHelpText = $null RootHelpValidated = $false HelpModes = @() HelpModeLookup = @{} SwitchOrder = @() SwitchInfo = @{} } } function New-PsPingCompletionResult { 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-PsPingOuterQuotes { 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 Get-PsPingCurrentToken { 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 Resolve-PsPingCommandName { if ($script:PsPingCompletionCatalog.CommandName) { return $script:PsPingCompletionCatalog.CommandName } foreach ($candidate in @('psping.exe', 'psping')) { $command = Get-Command $candidate -ErrorAction SilentlyContinue if ($null -eq $command) { continue } $resolvedName = if ($command.Path) { $command.Path } elseif ($command.Source) { $command.Source } else { $command.Name } if (-not [string]::IsNullOrWhiteSpace($resolvedName)) { $script:PsPingCompletionCatalog.CommandName = $resolvedName return $script:PsPingCompletionCatalog.CommandName } } $null } function Get-PsPingRootHelpText { if ($script:PsPingCompletionCatalog.RootHelpText) { return $script:PsPingCompletionCatalog.RootHelpText } $commandName = Resolve-PsPingCommandName if ([string]::IsNullOrWhiteSpace($commandName)) { return $null } # psping writes its help as UTF-16LE when redirected, so the capture decodes # stdout/stderr as Unicode; stdin is closed and the call is bounded so a # prompt (EULA) or a hang can never stall the completion thread. $text = '' try { $startInfo = [System.Diagnostics.ProcessStartInfo]::new() $startInfo.FileName = $commandName $startInfo.ArgumentList.Add('-nobanner') $startInfo.ArgumentList.Add('-?') $startInfo.UseShellExecute = $false $startInfo.CreateNoWindow = $true $startInfo.RedirectStandardInput = $true $startInfo.RedirectStandardOutput = $true $startInfo.RedirectStandardError = $true $startInfo.StandardOutputEncoding = [System.Text.Encoding]::Unicode $startInfo.StandardErrorEncoding = [System.Text.Encoding]::Unicode $process = [System.Diagnostics.Process]::Start($startInfo) try { $process.StandardInput.Close() $outputTask = $process.StandardOutput.ReadToEndAsync() $errorTask = $process.StandardError.ReadToEndAsync() if ($process.WaitForExit(5000)) { $text = ($outputTask.Result + $errorTask.Result) -replace '\e\[[0-9;?]*[ -/]*[@-~]', '' } else { $process.Kill() } } finally { $process.Dispose() } } catch { Write-Debug ('psping completer: help capture failed: ' + $_.Exception.Message) $text = '' } $script:PsPingCompletionCatalog.RootHelpText = $text $script:PsPingCompletionCatalog.RootHelpText } function Initialize-PsPingCompletionCatalog { if ($script:PsPingCompletionCatalog.Initialized) { return } $script:PsPingCompletionCatalog.HelpModes = @( [pscustomobject]@{ Token = 'i'; Description = 'Usage for ICMP ping.' } [pscustomobject]@{ Token = 't'; Description = 'Usage for TCP ping.' } [pscustomobject]@{ Token = 'l'; Description = 'Usage for latency test.' } [pscustomobject]@{ Token = 'b'; Description = 'Usage for bandwidth test.' } ) $script:PsPingCompletionCatalog.HelpModeLookup = @{} foreach ($helpMode in $script:PsPingCompletionCatalog.HelpModes) { $script:PsPingCompletionCatalog.HelpModeLookup[$helpMode.Token] = $helpMode } $script:PsPingCompletionCatalog.SwitchOrder = @( '-?', '-b', '-s', '-u', '-r', '-f', '-h', '-i', '-l', '-q', '-t', '-n', '-w', '-4', '-6', '-nobanner', '-accepteula' ) $script:PsPingCompletionCatalog.SwitchInfo = @{ '-?' = @{ Description = 'Display usage. Accepts i, t, l, or b.' } '-b' = @{ Description = 'Bandwidth test mode.' } '-s' = @{ Description = 'Server listening address and port.' } '-u' = @{ Description = 'UDP mode. In bandwidth mode it can take an optional target MB/s value.' } '-r' = @{ Description = 'Receive from the server instead of sending.' } '-f' = @{ Description = 'Open the source firewall port during the run.' } '-h' = @{ Description = 'Print a histogram. Accepts a bucket count or comma-separated thresholds.' } '-i' = @{ Description = 'Interval seconds for ping modes, or outstanding I/Os for bandwidth mode.' } '-l' = @{ Description = 'Request size. Append k for kilobytes or m for megabytes.' } '-q' = @{ Description = 'Do not output during ping tests.' } '-t' = @{ Description = 'Run until stopped with Ctrl+C.' } '-n' = @{ Description = 'Count of pings/sends/receives, or append s for seconds.' } '-w' = @{ Description = 'Warmup iteration count.' } '-4' = @{ Description = 'Force using IPv4.' } '-6' = @{ Description = 'Force using IPv6.' } '-nobanner' = @{ Description = 'Do not display the startup banner and copyright message.' } '-accepteula' = @{ Description = 'Accept the Sysinternals license agreement silently (undocumented in psping -?, accepted by every Sysinternals tool).' } } $rootHelpText = Get-PsPingRootHelpText if (-not [string]::IsNullOrWhiteSpace($rootHelpText) -and $rootHelpText -match 'psping\s+-\?\s+\[i\|t\|l\|b\]') { $script:PsPingCompletionCatalog.RootHelpValidated = $true } $script:PsPingCompletionCatalog.Initialized = $true } function Test-PsPingEndpointToken { param([string]$Value) $trimmedValue = Remove-PsPingOuterQuotes -Value $Value if ([string]::IsNullOrWhiteSpace($trimmedValue)) { return $false } if ($trimmedValue -match '^\[[^\]]+\]:\d+$') { return $true } $trimmedValue -match '^[^:\s]+:\d+$' } function Get-PsPingResolvedMode { param( [hashtable]$UsedSwitchLookup, [string[]]$Positionals ) if ($UsedSwitchLookup.ContainsKey('-?')) { return 'help' } if ($UsedSwitchLookup.ContainsKey('-s')) { return 'server' } if ($UsedSwitchLookup.ContainsKey('-b')) { return 'bandwidth-client' } if ($UsedSwitchLookup.ContainsKey('-r')) { return 'latency-client' } if ($UsedSwitchLookup.ContainsKey('-u')) { return 'latency-client' } if ($Positionals.Count -gt 0) { $lastPositional = $Positionals[-1] if (Test-PsPingEndpointToken -Value $lastPositional) { return 'tcp-ping' } return 'icmp-ping' } 'generic' } function Get-PsPingCommandState { param( [string[]]$TokensBeforeCurrent, [string]$CurrentWord ) $usedSwitchLookup = @{} $valuesBySwitch = @{} $positionals = New-Object System.Collections.Generic.List[string] $valueContext = $null $mandatoryValueSwitches = @{ '-i' = $true '-l' = $true '-n' = $true '-w' = $true '-s' = $true } for ($i = 0; $i -lt $TokensBeforeCurrent.Count; $i++) { $token = $TokensBeforeCurrent[$i] $lookup = $token.ToLowerInvariant() $nextToken = if ($i + 1 -lt $TokensBeforeCurrent.Count) { $TokensBeforeCurrent[$i + 1] } else { $null } if (-not $script:PsPingCompletionCatalog.SwitchInfo.ContainsKey($lookup)) { [void]$positionals.Add($token) continue } $usedSwitchLookup[$lookup] = $true switch ($lookup) { '-?' { if ($null -eq $nextToken) { $valueContext = '-?' break } if (-not $nextToken.StartsWith('-')) { $valuesBySwitch[$lookup] = $nextToken $i++ continue } $valueContext = '-?' break } '-h' { # '-h [buckets|val1,val2,...]': the argument is optional, so only # a bucket count or threshold list is consumed; 'psping -h host' # keeps host as the destination. if ($null -ne $nextToken -and $nextToken -match '^[\d.,]+$') { $valuesBySwitch[$lookup] = $nextToken $i++ } continue } '-u' { if ($usedSwitchLookup.ContainsKey('-b') -and $null -ne $nextToken -and -not $nextToken.StartsWith('-')) { $valuesBySwitch[$lookup] = $nextToken $i++ } continue } } if ($mandatoryValueSwitches.ContainsKey($lookup)) { if ($null -eq $nextToken) { $valueContext = $lookup break } if (-not $nextToken.StartsWith('-')) { $valuesBySwitch[$lookup] = $nextToken $i++ continue } $valueContext = $lookup break } } $mode = Get-PsPingResolvedMode -UsedSwitchLookup $usedSwitchLookup -Positionals @($positionals.ToArray()) $currentStartsWithDash = -not [string]::IsNullOrEmpty($CurrentWord) -and $CurrentWord.StartsWith('-') if (-not $valueContext -and $TokensBeforeCurrent.Count -gt 0 -and -not $currentStartsWithDash) { $lastToken = $TokensBeforeCurrent[-1].ToLowerInvariant() switch ($lastToken) { '-?' { $valueContext = '-?' } '-h' { if ($CurrentWord -match '^[\d.,]*$') { $valueContext = '-h' } } '-u' { if ($mode -eq 'bandwidth-client') { $valueContext = '-u' } } } } [pscustomobject]@{ UsedSwitchLookup = $usedSwitchLookup ValuesBySwitch = $valuesBySwitch Positionals = @($positionals.ToArray()) ValueContext = $valueContext Mode = $mode } } function Get-PsPingSwitchDescription { param( [string]$Token, [string]$Mode ) switch ($Token.ToLowerInvariant()) { '-i' { if ($Mode -eq 'bandwidth-client') { return 'Number of outstanding I/Os (default is min of 16 and 2x CPU cores).' } return 'Interval in seconds. Specify 0 for fast ping.' } '-u' { if ($Mode -eq 'bandwidth-client') { return 'UDP (default is TCP). Optionally specify target bandwidth in MB/s.' } return 'UDP (default is TCP).' } '-w' { switch ($Mode) { 'bandwidth-client' { return 'Warmup for the specified iterations (default is 2x CPU cores).' } 'latency-client' { return 'Warmup with the specified number of iterations (default is 5).' } default { return 'Warmup with the specified number of iterations (default is 1).' } } } '-n' { if ($Mode -eq 'icmp-ping' -or $Mode -eq 'tcp-ping') { return 'Number of pings, or append s to specify seconds.' } return 'Number of sends/receives, or append s to specify seconds.' } default { return $script:PsPingCompletionCatalog.SwitchInfo[$Token.ToLowerInvariant()].Description } } } function Get-PsPingModeSwitchTokens { param([string]$Mode) switch ($Mode) { 'server' { @('-?', '-s', '-f', '-4', '-6', '-nobanner', '-accepteula') } 'bandwidth-client' { @('-?', '-b', '-u', '-i', '-l', '-n', '-w', '-h', '-r', '-f', '-4', '-6', '-nobanner', '-accepteula') } 'latency-client' { @('-?', '-u', '-l', '-n', '-w', '-h', '-r', '-f', '-4', '-6', '-nobanner', '-accepteula') } 'tcp-ping' { @('-?', '-h', '-i', '-l', '-q', '-t', '-n', '-w', '-4', '-6', '-nobanner', '-accepteula') } 'icmp-ping' { @('-?', '-h', '-i', '-l', '-q', '-t', '-n', '-w', '-4', '-6', '-nobanner', '-accepteula') } default { @($script:PsPingCompletionCatalog.SwitchOrder) } } } function Get-PsPingStaticValueResults { param( [string]$CurrentWord, [object[]]$Candidates, [string]$FallbackToolTip ) $typedValue = Remove-PsPingOuterQuotes -Value $CurrentWord $results = New-Object System.Collections.Generic.List[object] foreach ($candidate in $Candidates) { $completionText = $candidate.CompletionText if (-not [string]::IsNullOrWhiteSpace($typedValue) -and -not $completionText.StartsWith($typedValue, [System.StringComparison]::OrdinalIgnoreCase)) { continue } [void]$results.Add(( New-PsPingCompletionResult ` -CompletionText $completionText ` -ListItemText $candidate.ListItemText ` -ResultType 'ParameterValue' ` -ToolTip $candidate.ToolTip )) } if ($results.Count -eq 0 -and -not [string]::IsNullOrWhiteSpace($CurrentWord)) { [void]$results.Add(( New-PsPingCompletionResult ` -CompletionText $CurrentWord ` -ListItemText $CurrentWord ` -ResultType 'ParameterValue' ` -ToolTip $FallbackToolTip )) } @($results.ToArray()) } function Get-PsPingHelpModeCompletions { param([string]$CurrentWord) $typedValue = Remove-PsPingOuterQuotes -Value $CurrentWord $results = New-Object System.Collections.Generic.List[object] foreach ($helpMode in $script:PsPingCompletionCatalog.HelpModes) { if (-not [string]::IsNullOrWhiteSpace($typedValue) -and -not $helpMode.Token.StartsWith($typedValue, [System.StringComparison]::OrdinalIgnoreCase)) { continue } [void]$results.Add(( New-PsPingCompletionResult ` -CompletionText $helpMode.Token ` -ListItemText $helpMode.Token ` -ResultType 'ParameterValue' ` -ToolTip $helpMode.Description )) } if ($results.Count -eq 0 -and -not [string]::IsNullOrWhiteSpace($CurrentWord)) { [void]$results.Add(( New-PsPingCompletionResult ` -CompletionText $CurrentWord ` -ListItemText $CurrentWord ` -ResultType 'ParameterValue' ` -ToolTip 'Usage topic for PsPing help.' )) } @($results.ToArray()) } function Get-PsPingValueCompletions { param( [string]$SwitchToken, [string]$CurrentWord, [string]$Mode ) switch ($SwitchToken.ToLowerInvariant()) { '-?' { return @(Get-PsPingHelpModeCompletions -CurrentWord $CurrentWord) } '-h' { return @(Get-PsPingStaticValueResults -CurrentWord $CurrentWord -FallbackToolTip 'Histogram bucket count or comma-separated thresholds.' -Candidates @( [pscustomobject]@{ CompletionText = '20'; ListItemText = '20'; ToolTip = 'Histogram with 20 buckets.' } [pscustomobject]@{ CompletionText = '100'; ListItemText = '100'; ToolTip = 'Histogram with 100 buckets.' } [pscustomobject]@{ CompletionText = '0.01,0.05,1,5,10'; ListItemText = '0.01,0.05,1,5,10'; ToolTip = 'Custom histogram thresholds.' } [pscustomobject]@{ CompletionText = '<buckets|comma-separated thresholds>'; ListItemText = '<buckets|thresholds>'; ToolTip = 'Histogram bucket count or comma-separated thresholds.' } )) } '-i' { if ($Mode -eq 'bandwidth-client') { return @(Get-PsPingStaticValueResults -CurrentWord $CurrentWord -FallbackToolTip 'Outstanding I/O count.' -Candidates @( [pscustomobject]@{ CompletionText = '1'; ListItemText = '1'; ToolTip = 'Single outstanding I/O.' } [pscustomobject]@{ CompletionText = '4'; ListItemText = '4'; ToolTip = 'Four outstanding I/Os.' } [pscustomobject]@{ CompletionText = '8'; ListItemText = '8'; ToolTip = 'Eight outstanding I/Os.' } [pscustomobject]@{ CompletionText = '16'; ListItemText = '16'; ToolTip = 'Sixteen outstanding I/Os.' } [pscustomobject]@{ CompletionText = '<outstanding I/Os>'; ListItemText = '<outstanding I/Os>'; ToolTip = 'Outstanding I/O count.' } )) } return @(Get-PsPingStaticValueResults -CurrentWord $CurrentWord -FallbackToolTip 'Interval in seconds.' -Candidates @( [pscustomobject]@{ CompletionText = '0'; ListItemText = '0'; ToolTip = 'Fast ping with no delay.' } [pscustomobject]@{ CompletionText = '0.1'; ListItemText = '0.1'; ToolTip = 'Interval of one tenth of a second.' } [pscustomobject]@{ CompletionText = '1'; ListItemText = '1'; ToolTip = 'Interval of one second.' } [pscustomobject]@{ CompletionText = '<seconds>'; ListItemText = '<seconds>'; ToolTip = 'Interval in seconds.' } )) } '-l' { return @(Get-PsPingStaticValueResults -CurrentWord $CurrentWord -FallbackToolTip 'Request size with optional k or m suffix.' -Candidates @( [pscustomobject]@{ CompletionText = '64'; ListItemText = '64'; ToolTip = '64-byte request size.' } [pscustomobject]@{ CompletionText = '1k'; ListItemText = '1k'; ToolTip = '1 KB request size.' } [pscustomobject]@{ CompletionText = '8k'; ListItemText = '8k'; ToolTip = '8 KB request size.' } [pscustomobject]@{ CompletionText = '64k'; ListItemText = '64k'; ToolTip = '64 KB request size.' } [pscustomobject]@{ CompletionText = '1m'; ListItemText = '1m'; ToolTip = '1 MB request size.' } [pscustomobject]@{ CompletionText = '<requestsize[k|m]>'; ListItemText = '<requestsize[k|m]>'; ToolTip = 'Request size with optional k or m suffix.' } )) } '-n' { return @(Get-PsPingStaticValueResults -CurrentWord $CurrentWord -FallbackToolTip 'Iteration count, or append s for seconds.' -Candidates @( [pscustomobject]@{ CompletionText = '10'; ListItemText = '10'; ToolTip = 'Run 10 iterations.' } [pscustomobject]@{ CompletionText = '100'; ListItemText = '100'; ToolTip = 'Run 100 iterations.' } [pscustomobject]@{ CompletionText = '1000'; ListItemText = '1000'; ToolTip = 'Run 1000 iterations.' } [pscustomobject]@{ CompletionText = '10s'; ListItemText = '10s'; ToolTip = 'Run for 10 seconds.' } [pscustomobject]@{ CompletionText = '<count[s]>'; ListItemText = '<count[s]>'; ToolTip = 'Iteration count, or append s for seconds.' } )) } '-w' { return @(Get-PsPingStaticValueResults -CurrentWord $CurrentWord -FallbackToolTip 'Warmup iteration count.' -Candidates @( [pscustomobject]@{ CompletionText = '1'; ListItemText = '1'; ToolTip = 'Warm up once.' } [pscustomobject]@{ CompletionText = '3'; ListItemText = '3'; ToolTip = 'Warm up three times.' } [pscustomobject]@{ CompletionText = '5'; ListItemText = '5'; ToolTip = 'Warm up five times.' } [pscustomobject]@{ CompletionText = '10'; ListItemText = '10'; ToolTip = 'Warm up ten times.' } [pscustomobject]@{ CompletionText = '<count>'; ListItemText = '<count>'; ToolTip = 'Warmup iteration count.' } )) } '-s' { return @(Get-PsPingStaticValueResults -CurrentWord $CurrentWord -FallbackToolTip 'Server bind address and port.' -Candidates @( [pscustomobject]@{ CompletionText = '<address:port>'; ListItemText = '<address:port>'; ToolTip = 'Server listening address and port.' } [pscustomobject]@{ CompletionText = '0.0.0.0:5000'; ListItemText = '0.0.0.0:5000'; ToolTip = 'Listen on all IPv4 interfaces on port 5000.' } [pscustomobject]@{ CompletionText = '127.0.0.1:5000'; ListItemText = '127.0.0.1:5000'; ToolTip = 'Listen on loopback port 5000.' } )) } '-u' { return @(Get-PsPingStaticValueResults -CurrentWord $CurrentWord -FallbackToolTip 'Target UDP bandwidth in MB/s.' -Candidates @( [pscustomobject]@{ CompletionText = '10'; ListItemText = '10'; ToolTip = 'Target 10 MB/s UDP bandwidth.' } [pscustomobject]@{ CompletionText = '100'; ListItemText = '100'; ToolTip = 'Target 100 MB/s UDP bandwidth.' } [pscustomobject]@{ CompletionText = '1000'; ListItemText = '1000'; ToolTip = 'Target 1000 MB/s UDP bandwidth.' } [pscustomobject]@{ CompletionText = '<target MB/s>'; ListItemText = '<target MB/s>'; ToolTip = 'Target UDP bandwidth in MB/s.' } )) } } @() } function Get-PsPingPositionalCompletions { param( [string]$CurrentWord, [string]$Mode ) $candidates = switch ($Mode) { 'server' { @( [pscustomobject]@{ CompletionText = '<address:port>'; ListItemText = '<address:port>'; ToolTip = 'Server listening address and port.' } [pscustomobject]@{ CompletionText = '0.0.0.0:5000'; ListItemText = '0.0.0.0:5000'; ToolTip = 'Listen on all IPv4 interfaces on port 5000.' } [pscustomobject]@{ CompletionText = '127.0.0.1:5000'; ListItemText = '127.0.0.1:5000'; ToolTip = 'Listen on loopback port 5000.' } ) } 'bandwidth-client' { @( [pscustomobject]@{ CompletionText = '<destination:port>'; ListItemText = '<destination:port>'; ToolTip = 'Destination endpoint for a bandwidth test.' } [pscustomobject]@{ CompletionText = 'localhost:80'; ListItemText = 'localhost:80'; ToolTip = 'Loopback endpoint example.' } [pscustomobject]@{ CompletionText = '127.0.0.1:443'; ListItemText = '127.0.0.1:443'; ToolTip = 'IPv4 loopback endpoint example.' } ) } 'latency-client' { @( [pscustomobject]@{ CompletionText = '<destination:port>'; ListItemText = '<destination:port>'; ToolTip = 'Destination endpoint for a latency test.' } [pscustomobject]@{ CompletionText = 'localhost:80'; ListItemText = 'localhost:80'; ToolTip = 'Loopback endpoint example.' } [pscustomobject]@{ CompletionText = '127.0.0.1:443'; ListItemText = '127.0.0.1:443'; ToolTip = 'IPv4 loopback endpoint example.' } ) } 'tcp-ping' { @( [pscustomobject]@{ CompletionText = '<destination:port>'; ListItemText = '<destination:port>'; ToolTip = 'Destination endpoint for TCP ping.' } [pscustomobject]@{ CompletionText = 'localhost:80'; ListItemText = 'localhost:80'; ToolTip = 'Loopback endpoint example.' } [pscustomobject]@{ CompletionText = '127.0.0.1:443'; ListItemText = '127.0.0.1:443'; ToolTip = 'IPv4 loopback endpoint example.' } ) } 'icmp-ping' { @( [pscustomobject]@{ CompletionText = '<destination>'; ListItemText = '<destination>'; ToolTip = 'Destination host name or address.' } [pscustomobject]@{ CompletionText = 'localhost'; ListItemText = 'localhost'; ToolTip = 'Loopback host name.' } [pscustomobject]@{ CompletionText = '127.0.0.1'; ListItemText = '127.0.0.1'; ToolTip = 'IPv4 loopback address.' } [pscustomobject]@{ CompletionText = '::1'; ListItemText = '::1'; ToolTip = 'IPv6 loopback address.' } ) } default { if ((Remove-PsPingOuterQuotes -Value $CurrentWord).Contains(':')) { @( [pscustomobject]@{ CompletionText = '<destination:port>'; ListItemText = '<destination:port>'; ToolTip = 'Destination endpoint.' } [pscustomobject]@{ CompletionText = 'localhost:80'; ListItemText = 'localhost:80'; ToolTip = 'Loopback endpoint example.' } [pscustomobject]@{ CompletionText = '127.0.0.1:443'; ListItemText = '127.0.0.1:443'; ToolTip = 'IPv4 loopback endpoint example.' } ) } else { @( [pscustomobject]@{ CompletionText = '<destination>'; ListItemText = '<destination>'; ToolTip = 'Destination host name or address.' } [pscustomobject]@{ CompletionText = 'localhost'; ListItemText = 'localhost'; ToolTip = 'Loopback host name.' } [pscustomobject]@{ CompletionText = '127.0.0.1'; ListItemText = '127.0.0.1'; ToolTip = 'IPv4 loopback address.' } [pscustomobject]@{ CompletionText = '::1'; ListItemText = '::1'; ToolTip = 'IPv6 loopback address.' } [pscustomobject]@{ CompletionText = '<destination:port>'; ListItemText = '<destination:port>'; ToolTip = 'Destination endpoint for TCP, latency, or bandwidth tests.' } [pscustomobject]@{ CompletionText = 'localhost:80'; ListItemText = 'localhost:80'; ToolTip = 'Loopback endpoint example.' } [pscustomobject]@{ CompletionText = '127.0.0.1:443'; ListItemText = '127.0.0.1:443'; ToolTip = 'IPv4 loopback endpoint example.' } ) } } } @(Get-PsPingStaticValueResults -CurrentWord $CurrentWord -Candidates $candidates -FallbackToolTip 'Destination or endpoint value.') } function Get-PsPingSwitchCompletions { param( [string]$CurrentWord, [string]$Mode, [hashtable]$UsedSwitchLookup ) $results = New-Object System.Collections.Generic.List[object] foreach ($switchToken in (Get-PsPingModeSwitchTokens -Mode $Mode)) { $lookup = $switchToken.ToLowerInvariant() if ($UsedSwitchLookup.ContainsKey($lookup)) { continue } if (($lookup -eq '-t' -and $UsedSwitchLookup.ContainsKey('-n')) -or ($lookup -eq '-n' -and $UsedSwitchLookup.ContainsKey('-t')) -or ($lookup -eq '-4' -and $UsedSwitchLookup.ContainsKey('-6')) -or ($lookup -eq '-6' -and $UsedSwitchLookup.ContainsKey('-4'))) { continue } if (-not [string]::IsNullOrWhiteSpace($CurrentWord) -and -not $switchToken.StartsWith($CurrentWord, [System.StringComparison]::OrdinalIgnoreCase)) { continue } [void]$results.Add(( New-PsPingCompletionResult ` -CompletionText $switchToken ` -ListItemText $switchToken ` -ResultType 'ParameterName' ` -ToolTip (Get-PsPingSwitchDescription -Token $switchToken -Mode $Mode) )) } @($results.ToArray()) } function Complete-PsPing { param( [string]$wordToComplete, [System.Management.Automation.Language.CommandAst]$commandAst, [int]$cursorPosition ) Initialize-PsPingCompletionCatalog $line = $commandAst.Extent.Text $currentWord = if ($null -ne $wordToComplete) { $wordToComplete } else { Get-PsPingCurrentToken -Line $line -CursorPosition ($cursorPosition - $commandAst.Extent.StartOffset) -Fallback '' } $commandTokens = @($commandAst.CommandElements | ForEach-Object { $_.Extent.Text }) $tokensBeforeCurrent = @( if ($commandTokens.Count -gt 0) { $commandTokens | Select-Object -Skip 1 } else { @() } ) if (-not [string]::IsNullOrEmpty($currentWord) -and $tokensBeforeCurrent.Count -gt 0) { $lastToken = $tokensBeforeCurrent[-1] if ($lastToken.Equals($currentWord, [System.StringComparison]::OrdinalIgnoreCase)) { if ($tokensBeforeCurrent.Count -eq 1) { $tokensBeforeCurrent = @() } else { $tokensBeforeCurrent = $tokensBeforeCurrent[0..($tokensBeforeCurrent.Count - 2)] } } } $state = Get-PsPingCommandState -TokensBeforeCurrent $tokensBeforeCurrent -CurrentWord $currentWord $results = New-Object System.Collections.Generic.List[object] $seenCompletions = @{} if ($state.ValueContext) { foreach ($result in (Get-PsPingValueCompletions -SwitchToken $state.ValueContext -CurrentWord $currentWord -Mode $state.Mode)) { if (-not $seenCompletions.ContainsKey($result.CompletionText)) { $seenCompletions[$result.CompletionText] = $true [void]$results.Add($result) } } if ($state.ValueContext -eq '-h' -and [string]::IsNullOrWhiteSpace($currentWord)) { # The histogram argument is optional, so the destination is just as # valid in this slot. foreach ($result in (Get-PsPingPositionalCompletions -CurrentWord $currentWord -Mode $state.Mode)) { if (-not $seenCompletions.ContainsKey($result.CompletionText)) { $seenCompletions[$result.CompletionText] = $true [void]$results.Add($result) } } } return @($results.ToArray()) } if ($state.UsedSwitchLookup.ContainsKey('-?')) { foreach ($result in (Get-PsPingHelpModeCompletions -CurrentWord $currentWord)) { if (-not $seenCompletions.ContainsKey($result.CompletionText)) { $seenCompletions[$result.CompletionText] = $true [void]$results.Add($result) } } return @($results.ToArray()) } if ([string]::IsNullOrWhiteSpace($currentWord) -or $currentWord.StartsWith('-')) { foreach ($result in (Get-PsPingSwitchCompletions -CurrentWord $currentWord -Mode $state.Mode -UsedSwitchLookup $state.UsedSwitchLookup)) { if (-not $seenCompletions.ContainsKey($result.CompletionText)) { $seenCompletions[$result.CompletionText] = $true [void]$results.Add($result) } } } if ([string]::IsNullOrWhiteSpace($currentWord) -or -not $currentWord.StartsWith('-')) { foreach ($result in (Get-PsPingPositionalCompletions -CurrentWord $currentWord -Mode $state.Mode)) { if (-not $seenCompletions.ContainsKey($result.CompletionText)) { $seenCompletions[$result.CompletionText] = $true [void]$results.Add($result) } } } @($results.ToArray()) } Register-ArgumentCompleter -Native -CommandName 'psping', 'psping.exe' -ScriptBlock { param($wordToComplete, $commandAst, $cursorPosition) Complete-PsPing -wordToComplete $wordToComplete -commandAst $commandAst -cursorPosition $cursorPosition } |