completers/testlimit_completer/testlimit_completer.ps1
|
# Testlimit.exe tab completion for PowerShell # Static native completer for Testlimit modelled on the v5.24 usage grammar: # testlimit [[-h [-u]] | [-p [-n]] | [-t [-n [KB]]] | [-u [-i]] | [-g [object size]] # | [-a|-d|-l|-m|-r|-s|-v [MB]] | [-w]] [-c [count]] [-e [seconds]] # Every numeric value is optional, the primary mode groups are mutually # exclusive, and -c must be the last option specified. Set-StrictMode -Version 2.0 if (-not (Get-Variable -Name TestlimitCompletionCatalog -Scope Script -ErrorAction Ignore)) { $script:TestlimitCompletionCatalog = @{ SwitchOrder = @('-a', '-c', '-d', '-e', '-g', '-h', '-i', '-l', '-m', '-n', '-p', '-r', '-s', '-t', '-u', '-v', '-w', '-accepteula', '-?', '/?', '/accepteula') SwitchInfo = @{ '-a' = 'Leak Address Windowing Extensions (AWE) memory in specified MBs (default is 1).' '-c' = 'Count of objects to allocate (default is as many as possible). This must be the last option specified.' '-d' = 'Leak and touch memory in specified MBs (default is 1).' '-e' = 'Seconds elapsed between allocations (default is 0).' '-g' = 'Create GDI handles of specified size (default 1 byte). A size of 0 causes GDI object exhaustion.' '-h' = 'Create handles. Specify -u to also allocate file objects.' '-i' = 'Exhaust USER desktop heap.' '-l' = 'Allocate the specified amount of large pages.' '-m' = 'Leak memory in specified MBs (default is 1).' '-n' = 'With -p set min working set; with -t specify minimum stack reserve in KB.' '-p' = 'Create processes. Add -n to set min working set.' '-r' = 'Reserve memory in specified MBs (default is 1).' '-s' = 'Leak shared memory in specified MBs (default is 1).' '-t' = 'Create threads. Add -n to specify minimum stack reserve in KB.' '-u' = 'Create USER handles to menus.' '-v' = 'VirtualLock memory in specified MBs (default is 1).' '-w' = 'Reset working set minimum to the highest possible value.' '-accepteula' = 'Silently accept the Sysinternals EULA (required for unattended use).' '-?' = 'Display Testlimit help.' '/?' = 'Display Testlimit help.' '/accepteula' = 'Slash form of -accepteula.' } # Exactly one primary mode may be chosen; the '|' groups of the usage line. ModeGroups = @{ handles = @('-h') processes = @('-p') threads = @('-t') user = @('-u') gdi = @('-g') memory = @('-a', '-d', '-l', '-m', '-r', '-s', '-v') workingset = @('-w') } # Sub-options each mode still accepts once it has been chosen. GroupModifiers = @{ handles = @('-u') processes = @('-n') threads = @('-n') user = @('-i') gdi = @() memory = @() workingset = @() } # Legal alongside any mode. GlobalSwitches = @('-c', '-e', '-accepteula', '/accepteula') SlashSwitches = @('/?', '/accepteula') HelpSwitches = @('-?', '/?') ValueSpec = @{ '-a' = @{ Samples = @('1', '16', '64', '256'); Placeholder = '<mb>'; ToolTip = 'AWE memory MB (optional, default 1).' } '-d' = @{ Samples = @('1', '16', '64', '256'); Placeholder = '<mb>'; ToolTip = 'Memory MB (optional, default 1).' } '-l' = @{ Samples = @('1', '16', '64', '256'); Placeholder = '<mb>'; ToolTip = 'Large pages MB (optional, default 1).' } '-m' = @{ Samples = @('1', '16', '64', '256'); Placeholder = '<mb>'; ToolTip = 'Memory MB (optional, default 1).' } '-r' = @{ Samples = @('1', '16', '64', '256'); Placeholder = '<mb>'; ToolTip = 'Reserved memory MB (optional, default 1).' } '-s' = @{ Samples = @('1', '16', '64', '256'); Placeholder = '<mb>'; ToolTip = 'Shared memory MB (optional, default 1).' } '-v' = @{ Samples = @('1', '16', '64', '256'); Placeholder = '<mb>'; ToolTip = 'VirtualLock memory MB (optional, default 1).' } '-g' = @{ Samples = @('0', '1', '256', '4096'); Placeholder = '<object-size-bytes>'; ToolTip = 'GDI object size in bytes (optional, default 1).' } '-c' = @{ Samples = @('1', '10', '100', '1000'); Placeholder = '<count>'; ToolTip = 'Object allocation count (optional, default as many as possible).' } '-e' = @{ Samples = @('0', '1', '5', '10'); Placeholder = '<seconds>'; ToolTip = 'Seconds between allocations (optional, default 0).' } '-n' = @{ Samples = @('64', '128', '256', '1024'); Placeholder = '<stack-kb>'; ToolTip = 'Minimum stack reserve in KB for -t (optional).' } } } } function New-TestlimitCompletionResult { param( [string]$CompletionText, [string]$ResultType, [string]$ToolTip ) if ([string]::IsNullOrWhiteSpace($ToolTip)) { $ToolTip = $CompletionText } [System.Management.Automation.CompletionResult]::new( $CompletionText, $CompletionText, $ResultType, $ToolTip ) } function Get-TestlimitCurrentToken { param( [string]$Line, [int]$CursorPosition, [string]$Fallback ) if ([string]::IsNullOrWhiteSpace($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 Get-TestlimitSwitchGroup { param([string]$Token) $groups = $script:TestlimitCompletionCatalog.ModeGroups foreach ($name in @($groups.Keys)) { if (@($groups[$name]) -contains $Token) { return $name } } $null } function Get-TestlimitCommandState { param([object[]]$TokensBeforeCurrent) $catalog = $script:TestlimitCompletionCatalog $tokens = @(@($TokensBeforeCurrent) | Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_) }) $usedSwitchLookup = @{} $valueContext = $null $helpRequested = $false $activeGroup = $null for ($index = 0; $index -lt $tokens.Count; $index++) { $lookup = ([string]$tokens[$index]).ToLowerInvariant() if (-not ($lookup.StartsWith('-') -or $lookup.StartsWith('/'))) { continue } $usedSwitchLookup[$lookup] = $true if ($catalog.HelpSwitches -contains $lookup) { $helpRequested = $true continue } if ($null -eq $activeGroup) { $group = Get-TestlimitSwitchGroup -Token $lookup if ($group) { $activeGroup = $group } } # -n only carries a stack-reserve value after -t. $takesValue = $catalog.ValueSpec.ContainsKey($lookup) if ($lookup -eq '-n' -and -not $usedSwitchLookup.ContainsKey('-t')) { $takesValue = $false } if (-not $takesValue) { continue } if ($index -eq ($tokens.Count - 1)) { $valueContext = $lookup break } # The value is optional, so only an actual number consumes the slot. if (([string]$tokens[$index + 1]) -match '^\d+$') { $index++ } } [pscustomobject]@{ UsedSwitchLookup = $usedSwitchLookup ValueContext = $valueContext HelpRequested = $helpRequested ActiveGroup = $activeGroup } } function Test-TestlimitSwitchAllowed { param( [string]$Token, [pscustomobject]$State, [bool]$NoArgumentsYet ) $catalog = $script:TestlimitCompletionCatalog if ($State.UsedSwitchLookup.ContainsKey($Token)) { return $false } if ($catalog.HelpSwitches -contains $Token) { return $NoArgumentsYet } # '-c ... must be the last option specified.' if ($State.UsedSwitchLookup.ContainsKey('-c')) { return $false } if ($Token -in @('-accepteula', '/accepteula')) { return -not ($State.UsedSwitchLookup.ContainsKey('-accepteula') -or $State.UsedSwitchLookup.ContainsKey('/accepteula')) } if ($catalog.GlobalSwitches -contains $Token) { return $true } if ($null -eq $State.ActiveGroup) { # No primary mode chosen yet, so only a mode switch is legal. return ($null -ne (Get-TestlimitSwitchGroup -Token $Token)) } @($catalog.GroupModifiers[$State.ActiveGroup]) -contains $Token } function Get-TestlimitSwitchCompletions { param( [string]$CurrentWord, [pscustomobject]$State, [bool]$NoArgumentsYet ) $catalog = $script:TestlimitCompletionCatalog $prefix = if ([string]::IsNullOrWhiteSpace($CurrentWord)) { '' } else { $CurrentWord } $results = [System.Collections.Generic.List[object]]::new() foreach ($token in $catalog.SwitchOrder) { if ($catalog.SlashSwitches -contains $token) { if (-not $prefix.StartsWith('/')) { continue } } if ($prefix -and -not $token.StartsWith($prefix, [System.StringComparison]::OrdinalIgnoreCase)) { continue } if (-not (Test-TestlimitSwitchAllowed -Token $token -State $State -NoArgumentsYet $NoArgumentsYet)) { continue } $results.Add((New-TestlimitCompletionResult -CompletionText $token -ResultType 'ParameterName' -ToolTip $catalog.SwitchInfo[$token])) } @($results.ToArray()) } function Get-TestlimitValueCompletions { param( [string]$ValueContext, [string]$CurrentValue, [pscustomobject]$State, [bool]$NoArgumentsYet ) $spec = $script:TestlimitCompletionCatalog.ValueSpec[$ValueContext] $typedValue = if ([string]::IsNullOrWhiteSpace($CurrentValue)) { '' } else { $CurrentValue.Trim('"') } $results = [System.Collections.Generic.List[object]]::new() foreach ($sample in $spec.Samples) { if ($typedValue -and -not $sample.StartsWith($typedValue, [System.StringComparison]::OrdinalIgnoreCase)) { continue } $results.Add((New-TestlimitCompletionResult -CompletionText $sample -ResultType 'ParameterValue' -ToolTip $spec.ToolTip)) } if (-not $typedValue -or $spec.Placeholder.StartsWith($typedValue, [System.StringComparison]::OrdinalIgnoreCase)) { $results.Add((New-TestlimitCompletionResult -CompletionText $spec.Placeholder -ResultType 'ParameterValue' -ToolTip $spec.ToolTip)) } elseif ($typedValue -match '^\d+$' -and @($spec.Samples) -notcontains $typedValue) { # A number the sample ladder does not list is still a valid value. $results.Add((New-TestlimitCompletionResult -CompletionText $typedValue -ResultType 'ParameterValue' -ToolTip $spec.ToolTip)) } if ($results.Count -eq 0) { $results.Add((New-TestlimitCompletionResult -CompletionText $spec.Placeholder -ResultType 'ParameterValue' -ToolTip $spec.ToolTip)) } # Every value is optional, so the still-legal switches remain reachable - # except after -c, which the tool requires to be the last option. if ($ValueContext -ne '-c') { foreach ($result in Get-TestlimitSwitchCompletions -CurrentWord $CurrentValue -State $State -NoArgumentsYet $NoArgumentsYet) { $results.Add($result) } } @($results.ToArray()) } function Complete-Testlimit { param( [string]$wordToComplete, [System.Management.Automation.Language.CommandAst]$commandAst, [int]$cursorPosition ) $line = $commandAst.ToString() $relativeCursor = $cursorPosition - $commandAst.Extent.StartOffset $safeCursor = [Math]::Min([Math]::Max($relativeCursor, 0), $line.Length) $linePrefix = $line.Substring(0, $safeCursor) $commandTokens = @([regex]::Matches($linePrefix, '"[^"]*"|\S+') | ForEach-Object { $_.Value }) $argumentTokens = @() if ($commandTokens.Count -gt 1) { $argumentTokens = @($commandTokens | Select-Object -Skip 1) } $currentWord = if ([string]::IsNullOrEmpty($wordToComplete)) { Get-TestlimitCurrentToken -Line $line -CursorPosition $relativeCursor -Fallback $wordToComplete } else { $wordToComplete } $hasTrailingSpace = [string]::IsNullOrEmpty($currentWord) -and (($linePrefix -match '\s$') -or ($relativeCursor -gt $line.Length)) $tokensBeforeCurrent = @() if ($hasTrailingSpace) { $tokensBeforeCurrent = @($argumentTokens) } elseif ($argumentTokens.Count -gt 1) { $tokensBeforeCurrent = @($argumentTokens | Select-Object -First ($argumentTokens.Count - 1)) } $state = Get-TestlimitCommandState -TokensBeforeCurrent $tokensBeforeCurrent $noArgumentsYet = ($tokensBeforeCurrent.Count -eq 0) if ($state.HelpRequested) { return @( New-TestlimitCompletionResult -CompletionText '-?' -ResultType 'ParameterName' -ToolTip 'Display Testlimit help.' ) } # A dash or slash always means a switch, even inside an optional value slot. if ($currentWord -and ($currentWord.StartsWith('-') -or $currentWord.StartsWith('/'))) { return @(Get-TestlimitSwitchCompletions -CurrentWord $currentWord -State $state -NoArgumentsYet $noArgumentsYet) } if ($state.ValueContext) { return @(Get-TestlimitValueCompletions -ValueContext $state.ValueContext -CurrentValue $currentWord -State $state -NoArgumentsYet $noArgumentsYet) } if ([string]::IsNullOrWhiteSpace($currentWord)) { return @(Get-TestlimitSwitchCompletions -CurrentWord $currentWord -State $state -NoArgumentsYet $noArgumentsYet) } @() } Register-ArgumentCompleter -Native -CommandName @('testlimit', 'testlimit.exe', 'Testlimit', 'Testlimit.exe') -ScriptBlock { param($wordToComplete, $commandAst, $cursorPosition) Complete-Testlimit -wordToComplete $wordToComplete -commandAst $commandAst -cursorPosition $cursorPosition } |