completers/sc_completer/sc_completer.ps1
|
# sc.exe tab completion for PowerShell # Builds a help-seeded command catalog and value-aware completion for sc.exe. Set-StrictMode -Version 2.0 if (-not (Get-Variable -Name ScCompletionCatalog -Scope Script -ErrorAction Ignore)) { $script:ScCompletionCatalog = @{ Initialized = $false CommandName = $null TopLevelCommands = @() CommandInfoByKey = @{} BufferSizeHints = @('256', '1024', '4096', '8192', '16384') NodeNumberHints = @('0', '1', '2', '3', '4', '5', '6', '7') FailureFlagHints = @('0', '1') ManagedBoolHints = @('true', 'false') SidTypeHints = @('none', 'unrestricted', 'restricted') BootHints = @('bad', 'ok') ControlHints = @('paramchange', 'netbindadd', 'netbindremove', 'netbindenable', 'netbinddisable') StopReasonHints = @('0:0:0', '1:0:0') FailureActionHints = @('run/5000', 'restart/5000', 'reboot/60000') FailureResetHints = @('0', '60', '300', '3600', 'INFINITE') TriggerTemplates = @( 'start/device/UUID/HwId1/...' 'start/custom/UUID/data0/...' 'stop/custom/UUID/data0/...' 'start/strcustom/UUID/data0/...' 'stop/strcustom/UUID/data0/...' 'start/lvlcustom/UUID/data0/...' 'stop/lvlcustom/UUID/data0/...' 'start/kwanycustom/UUID/data0/...' 'stop/kwanycustom/UUID/data0/...' 'start/kwallcustom/UUID/data0/...' 'stop/kwallcustom/UUID/data0/...' 'start/networkon' 'stop/networkoff' 'start/domainjoin' 'stop/domainleave' 'start/portopen/parameter' 'stop/portclose/parameter' 'start/machinepolicy' 'start/userpolicy' 'start/rpcinterface/UUID' 'start/namedpipe/pipename' 'delete' ) QueryOptions = @() CreateOptions = @() ConfigOptions = @() FailureOptions = @() } } if (-not (Get-Variable -Name ScServiceCache -Scope Script -ErrorAction Ignore)) { $script:ScServiceCache = @{ LastUpdated = $null TtlSeconds = 30 ServiceNames = @() DisplayNames = @() } } function Resolve-ScCommandName { if ($script:ScCompletionCatalog.CommandName) { return $script:ScCompletionCatalog.CommandName } $command = Get-Command -Name 'sc.exe', 'sc' -ErrorAction Ignore | Select-Object -First 1 if ($command) { $script:ScCompletionCatalog.CommandName = if ($command.Source) { $command.Source } else { $command.Name } } $script:ScCompletionCatalog.CommandName } function Test-ScCommandAvailable { [bool](Resolve-ScCommandName) } function Invoke-ScHelpText { param([string[]]$Arguments) $commandName = Resolve-ScCommandName if (-not $commandName) { return @() } try { @(& $commandName @Arguments 2>$null | ForEach-Object { $_ -replace '\e\[[0-9;?]*[ -/]*[@-~]', '' }) } catch { @() } } function New-ScCompletionResult { 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 New-ScLiteralValueResults { param( [string]$CurrentValue, [string]$ToolTip, [string]$Placeholder = '<value>' ) if ([string]::IsNullOrEmpty($CurrentValue)) { return @( New-ScCompletionResult -CompletionText ' ' -ListItemText $Placeholder -ResultType 'ParameterValue' -ToolTip $ToolTip ) } @( New-ScCompletionResult -CompletionText $CurrentValue -ListItemText $CurrentValue -ResultType 'ParameterValue' -ToolTip $ToolTip ) } function Get-ScStaticCommandSpecs { $specs = @( @{ Name = 'query'; Description = 'Queries the status for a service, or enumerates the status for types of services.' } @{ Name = 'queryex'; Description = 'Queries the extended status for a service, or enumerates the status for types of services.' } @{ Name = 'start'; Description = 'Starts a service.' } @{ Name = 'pause'; Description = 'Sends a PAUSE control request to a service.' } @{ Name = 'interrogate'; Description = 'Sends an INTERROGATE control request to a service.' } @{ Name = 'continue'; Description = 'Sends a CONTINUE control request to a service.' } @{ Name = 'stop'; Description = 'Sends a STOP request to a service.' } @{ Name = 'config'; Description = 'Changes the configuration of a service (persistent).' } @{ Name = 'description'; Description = 'Changes the description of a service.' } @{ Name = 'failure'; Description = 'Changes the actions taken by a service upon failure.' } @{ Name = 'failureflag'; Description = 'Changes the failure actions flag of a service.' } @{ Name = 'sidtype'; Description = 'Changes the service SID type of a service.' } @{ Name = 'privs'; Description = 'Changes the required privileges of a service.' } @{ Name = 'managedaccount'; Description = 'Changes whether the service account password is managed by LSA.' } @{ Name = 'qc'; Description = 'Queries the configuration information for a service.' } @{ Name = 'qdescription'; Description = 'Queries the description for a service.' } @{ Name = 'qfailure'; Description = 'Queries the actions taken by a service upon failure.' } @{ Name = 'qfailureflag'; Description = 'Queries the failure actions flag of a service.' } @{ Name = 'qsidtype'; Description = 'Queries the service SID type of a service.' } @{ Name = 'qprivs'; Description = 'Queries the required privileges of a service.' } @{ Name = 'qtriggerinfo'; Description = 'Queries the trigger parameters of a service.' } @{ Name = 'qpreferrednode'; Description = 'Queries the preferred NUMA node of a service.' } @{ Name = 'qmanagedaccount'; Description = 'Queries whether a service uses an account with a password managed by LSA.' } @{ Name = 'qprotection'; Description = 'Queries the process protection level of a service.' } @{ Name = 'quserservice'; Description = 'Queries for a local instance of a user service template.' } @{ Name = 'delete'; Description = 'Deletes a service.' } @{ Name = 'create'; Description = 'Creates a service.' } @{ Name = 'control'; Description = 'Sends a control to a service.' } @{ Name = 'sdshow'; Description = 'Displays a service security descriptor.' } @{ Name = 'sdset'; Description = 'Sets a service security descriptor.' } @{ Name = 'showsid'; Description = 'Displays the service SID string corresponding to an arbitrary name.' } @{ Name = 'triggerinfo'; Description = 'Configures the trigger parameters of a service.' } @{ Name = 'preferrednode'; Description = 'Sets the preferred NUMA node of a service.' } @{ Name = 'GetDisplayName'; Description = 'Gets the DisplayName for a service.' } @{ Name = 'GetKeyName'; Description = 'Gets the ServiceKeyName for a service.' } @{ Name = 'EnumDepend'; Description = 'Enumerates service dependencies.' } @{ Name = 'boot'; Description = 'Sets whether the last boot is treated as bad or ok.' } @{ Name = 'Lock'; Description = 'Locks the Service Database.' } @{ Name = 'QueryLock'; Description = 'Queries the lock status for the Service Control Manager database.' } ) @( foreach ($spec in $specs) { [pscustomobject]$spec } ) } function Get-ScStaticQueryOptions { @( [pscustomobject]@{ Key = 'type='; CanonicalToken = 'type= '; Description = 'Type of services to enumerate.'; ValueKind = 'QueryType' } [pscustomobject]@{ Key = 'state='; CanonicalToken = 'state= '; Description = 'State of services to enumerate.'; ValueKind = 'QueryState' } [pscustomobject]@{ Key = 'bufsize='; CanonicalToken = 'bufsize= '; Description = 'Enumeration buffer size in bytes.'; ValueKind = 'BufferSize' } [pscustomobject]@{ Key = 'ri='; CanonicalToken = 'ri= '; Description = 'Resume index number.'; ValueKind = 'ResumeIndex' } [pscustomobject]@{ Key = 'group='; CanonicalToken = 'group= '; Description = 'Service group to enumerate.'; ValueKind = 'FreeText' } ) } function Get-ScStaticConfigOptions { param([bool]$IncludeAdapt) $typeValues = @('own', 'share', 'interact', 'kernel', 'filesys', 'rec', 'userown', 'usershare') if ($IncludeAdapt) { $typeValues = @('own', 'share', 'interact', 'kernel', 'filesys', 'rec', 'adapt', 'userown', 'usershare') } @( [pscustomobject]@{ Key = 'type='; CanonicalToken = 'type= '; Description = 'Service type.'; ValueKind = 'StaticList'; Values = $typeValues } [pscustomobject]@{ Key = 'start='; CanonicalToken = 'start= '; Description = 'Service start mode.'; ValueKind = 'StaticList'; Values = @('boot', 'system', 'auto', 'demand', 'disabled', 'delayed-auto') } [pscustomobject]@{ Key = 'error='; CanonicalToken = 'error= '; Description = 'Error severity.'; ValueKind = 'StaticList'; Values = @('normal', 'severe', 'critical', 'ignore') } [pscustomobject]@{ Key = 'binpath='; CanonicalToken = 'binPath= '; Description = 'Binary path for the service executable.'; ValueKind = 'Path'; Placeholder = '<binary path>' } [pscustomobject]@{ Key = 'group='; CanonicalToken = 'group= '; Description = 'Load order group.'; ValueKind = 'FreeText'; Placeholder = '<load order group>' } [pscustomobject]@{ Key = 'tag='; CanonicalToken = 'tag= '; Description = 'Whether to obtain a TagID.'; ValueKind = 'StaticList'; Values = @('yes', 'no') } [pscustomobject]@{ Key = 'depend='; CanonicalToken = 'depend= '; Description = 'Dependencies separated by forward slashes.'; ValueKind = 'FreeText'; Placeholder = '<dependency1/dependency2>' } [pscustomobject]@{ Key = 'obj='; CanonicalToken = 'obj= '; Description = 'Account name or object name.'; ValueKind = 'FreeText'; Placeholder = '<account name>' } [pscustomobject]@{ Key = 'displayname='; CanonicalToken = 'DisplayName= '; Description = 'Display name.'; ValueKind = 'FreeText'; Placeholder = '<display name>' } [pscustomobject]@{ Key = 'password='; CanonicalToken = 'password= '; Description = 'Account password.'; ValueKind = 'FreeText'; Placeholder = '<password>' } ) } function Get-ScStaticFailureOptions { @( [pscustomobject]@{ Key = 'reset='; CanonicalToken = 'reset= '; Description = 'Length of period after which to reset the failure count.'; ValueKind = 'FailureReset' } [pscustomobject]@{ Key = 'reboot='; CanonicalToken = 'reboot= '; Description = 'Message broadcast before rebooting on failure.'; ValueKind = 'FreeText'; Placeholder = '<reboot message>' } [pscustomobject]@{ Key = 'command='; CanonicalToken = 'command= '; Description = 'Command line to run on failure.'; ValueKind = 'Path'; Placeholder = '<command line>' } [pscustomobject]@{ Key = 'actions='; CanonicalToken = 'actions= '; Description = 'Failure actions with delay times separated by forward slashes.'; ValueKind = 'FailureActions' } ) } function Initialize-ScCompletionCatalog { if ($script:ScCompletionCatalog.Initialized) { return } $commandInfoByKey = @{} foreach ($commandSpec in (Get-ScStaticCommandSpecs)) { $commandInfoByKey[$commandSpec.Name.ToLowerInvariant()] = $commandSpec } $helpLines = Invoke-ScHelpText $orderedKeys = New-Object System.Collections.Generic.List[string] $currentKey = $null foreach ($line in $helpLines) { $match = [regex]::Match($line, '^\s+(?<name>[A-Za-z][A-Za-z0-9]*?)-+(?<description>\S.*)$') if ($match.Success) { $commandName = $match.Groups['name'].Value $currentKey = $commandName.ToLowerInvariant() if ($commandInfoByKey.ContainsKey($currentKey)) { $commandInfoByKey[$currentKey] = [pscustomobject]@{ Name = $commandInfoByKey[$currentKey].Name Description = $match.Groups['description'].Value.Trim() } } else { $commandInfoByKey[$currentKey] = [pscustomobject]@{ Name = $commandName Description = $match.Groups['description'].Value.Trim() } } if (-not $orderedKeys.Contains($currentKey)) { $orderedKeys.Add($currentKey) } continue } if ($currentKey -and $line -match '^\s{20,}(?<continuation>\S.*)$') { $currentInfo = $commandInfoByKey[$currentKey] $commandInfoByKey[$currentKey] = [pscustomobject]@{ Name = $currentInfo.Name Description = ($currentInfo.Description + ' ' + $matches['continuation'].Trim()) } continue } $currentKey = $null } if ($orderedKeys.Count -eq 0) { foreach ($entry in $commandInfoByKey.GetEnumerator() | Sort-Object { $_.Value.Name }) { $orderedKeys.Add($entry.Key) } } $script:ScCompletionCatalog.TopLevelCommands = @( foreach ($key in $orderedKeys) { $commandInfoByKey[$key] } ) $script:ScCompletionCatalog.CommandInfoByKey = $commandInfoByKey $script:ScCompletionCatalog.QueryOptions = @(Get-ScStaticQueryOptions) $script:ScCompletionCatalog.ConfigOptions = @(Get-ScStaticConfigOptions -IncludeAdapt $true) $script:ScCompletionCatalog.CreateOptions = @(Get-ScStaticConfigOptions -IncludeAdapt $false) $script:ScCompletionCatalog.FailureOptions = @(Get-ScStaticFailureOptions) $script:ScCompletionCatalog.Initialized = $true } function Update-ScServiceCache { if ($script:ScServiceCache.ServiceNames.Count -gt 0 -and $null -ne $script:ScServiceCache.LastUpdated) { $age = (Get-Date) - $script:ScServiceCache.LastUpdated if ($age.TotalSeconds -lt $script:ScServiceCache.TtlSeconds) { return } } try { $services = @(Get-Service -ErrorAction Stop | Sort-Object -Property Name) $script:ScServiceCache.ServiceNames = @( $services | Select-Object -ExpandProperty Name | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Sort-Object -Unique ) $script:ScServiceCache.DisplayNames = @( $services | Select-Object -ExpandProperty DisplayName | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Sort-Object -Unique ) $script:ScServiceCache.LastUpdated = Get-Date } catch { if (-not $script:ScServiceCache.ServiceNames) { $script:ScServiceCache.ServiceNames = @() } if (-not $script:ScServiceCache.DisplayNames) { $script:ScServiceCache.DisplayNames = @() } } } function Get-ScCurrentTokenState { 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 $quoteChar = [char]0 foreach ($character in $prefix.ToCharArray()) { if (($character -eq [char]34 -or $character -eq [char]39)) { if ($quoteChar -eq [char]0) { $quoteChar = $character } elseif ($quoteChar -eq $character) { $quoteChar = [char]0 } [void]$builder.Append($character) continue } if ([char]::IsWhiteSpace($character) -and $quoteChar -eq [char]0) { if ($builder.Length -gt 0) { $tokens.Add($builder.ToString()) [void]$builder.Clear() } continue } [void]$builder.Append($character) } # Whitespace inside a still-open quote belongs to the current word. $hasTrailingSpace = ($prefix -match '\s$') -and ($quoteChar -eq [char]0) if ($builder.Length -gt 0) { $tokens.Add($builder.ToString()) } if ($hasTrailingSpace) { return [pscustomobject]@{ TokensBeforeCurrent = @($tokens) CurrentToken = '' HasTrailingSpace = $true } } if ($tokens.Count -gt 0) { return [pscustomobject]@{ TokensBeforeCurrent = @($tokens | Select-Object -First ($tokens.Count - 1)) CurrentToken = $tokens[$tokens.Count - 1] HasTrailingSpace = $false } } [pscustomobject]@{ TokensBeforeCurrent = @() CurrentToken = '' HasTrailingSpace = $false } } function Get-ScArgumentsFromTokenState { param([pscustomobject]$TokenState) if (-not $TokenState) { return [pscustomobject]@{ ArgumentsBeforeCurrent = @() CurrentWord = '' } } $tokensBeforeCurrent = @($TokenState.TokensBeforeCurrent) if ($tokensBeforeCurrent.Count -eq 0) { return [pscustomobject]@{ ArgumentsBeforeCurrent = @() CurrentWord = $TokenState.CurrentToken } } $arguments = @() if ($tokensBeforeCurrent.Count -gt 1) { $arguments = @($tokensBeforeCurrent | Select-Object -Skip 1) } [pscustomobject]@{ ArgumentsBeforeCurrent = $arguments CurrentWord = $TokenState.CurrentToken } } function Get-ScCommandContext { param([string[]]$ArgumentsBeforeCurrent) $args = @($ArgumentsBeforeCurrent) $server = $null if ($args.Count -gt 0 -and $args[0] -like '\\*') { $server = $args[0] if ($args.Count -gt 1) { $args = @($args | Select-Object -Skip 1) } else { $args = @() } } $command = $null $commandKey = $null $commandArguments = @() if ($args.Count -gt 0) { $command = $args[0] $commandKey = $command.ToLowerInvariant() if ($args.Count -gt 1) { $commandArguments = @($args | Select-Object -Skip 1) } } [pscustomobject]@{ Server = $server Command = $command CommandKey = $commandKey CommandArguments = $commandArguments } } function Get-ScUniqueCompletions { param([System.Management.Automation.CompletionResult[]]$Results) $seen = @{} $unique = @() foreach ($result in $Results) { if ($null -eq $result) { continue } if ($seen.ContainsKey($result.CompletionText)) { continue } $seen[$result.CompletionText] = $true $unique += $result } $unique } function Test-ScStartsWith { param( [string]$Value, [string]$Prefix ) if ([string]::IsNullOrEmpty($Prefix)) { return $true } if ([string]::IsNullOrEmpty($Value)) { return $false } $Value.StartsWith($Prefix, [System.StringComparison]::OrdinalIgnoreCase) } function Get-ScCommandCompletions { param([string]$CurrentWord) $results = @() foreach ($command in $script:ScCompletionCatalog.TopLevelCommands) { if (Test-ScStartsWith -Value $command.Name -Prefix $CurrentWord) { $results += New-ScCompletionResult -CompletionText $command.Name -ListItemText $command.Name -ResultType 'ParameterValue' -ToolTip $command.Description } } $results } function Get-ScServerPlaceholderCompletions { param([string]$CurrentWord) if ([string]::IsNullOrWhiteSpace($CurrentWord)) { return @( New-ScCompletionResult -CompletionText '\\ServerName' -ListItemText '\\ServerName' -ResultType 'ParameterValue' -ToolTip 'Remote server name in \\ServerName form.' ) } if ('\\ServerName'.StartsWith($CurrentWord, [System.StringComparison]::OrdinalIgnoreCase)) { return @( New-ScCompletionResult -CompletionText '\\ServerName' -ListItemText '\\ServerName' -ResultType 'ParameterValue' -ToolTip 'Remote server name in \\ServerName form.' ) } @( New-ScCompletionResult -CompletionText $CurrentWord -ListItemText $CurrentWord -ResultType 'ParameterValue' -ToolTip 'Remote server name in \\ServerName form.' ) } function Get-ScStringValueCompletions { param( [string[]]$Values, [string]$CurrentWord, [string]$ToolTip, [string]$Prefix = '', [bool]$QuoteWhitespace = $false ) # The parser closes an open string, so 'sc start "Win' arrives as "Win": strip both # quotes and undo the escaping so the bare prefix is what gets matched. $matchPrefix = $CurrentWord if ($matchPrefix.Length -gt 0 -and ($matchPrefix[0] -eq [char]34 -or $matchPrefix[0] -eq [char]39)) { $quoteChar = $matchPrefix[0] $matchPrefix = $matchPrefix.Substring(1) if ($matchPrefix.Length -gt 0 -and $matchPrefix[$matchPrefix.Length - 1] -eq $quoteChar) { $matchPrefix = $matchPrefix.Substring(0, $matchPrefix.Length - 1) } $matchPrefix = if ($quoteChar -eq [char]39) { $matchPrefix.Replace("''", "'") } else { $matchPrefix.Replace('`"', '"') } } $results = @() foreach ($value in ($Values | Sort-Object -Unique)) { if (Test-ScStartsWith -Value $value -Prefix $matchPrefix) { $completionText = $value if ($QuoteWhitespace -and ($value -match '\s' -or ($CurrentWord.Length -gt 0 -and ($CurrentWord[0] -eq [char]34 -or $CurrentWord[0] -eq [char]39)))) { $quoteCharacter = if ($CurrentWord.Length -gt 0 -and $CurrentWord[0] -eq [char]34) { '"' } else { "'" } $escapedValue = if ($quoteCharacter -eq "'") { $value -replace "'", "''" } else { $value -replace '"', '`"' } $completionText = '{0}{1}{0}' -f $quoteCharacter, $escapedValue } $results += New-ScCompletionResult -CompletionText ($Prefix + $completionText) -ListItemText $value -ResultType 'ParameterValue' -ToolTip $ToolTip } } $results } function Get-ScServiceNameCompletions { param([string]$CurrentWord) Update-ScServiceCache Get-ScStringValueCompletions -Values $script:ScServiceCache.ServiceNames -CurrentWord $CurrentWord -ToolTip 'Local service name.' -QuoteWhitespace $true } function Get-ScDisplayNameCompletions { param([string]$CurrentWord) Update-ScServiceCache Get-ScStringValueCompletions -Values $script:ScServiceCache.DisplayNames -CurrentWord $CurrentWord -ToolTip 'Local service display name.' -QuoteWhitespace $true } function Get-ScCombinedNameCompletions { param([string]$CurrentWord) $results = @() $results += @(Get-ScServiceNameCompletions -CurrentWord $CurrentWord) $results += @(Get-ScDisplayNameCompletions -CurrentWord $CurrentWord) Get-ScUniqueCompletions -Results $results } function Get-ScNumericCompletions { param( [string[]]$Hints, [string]$CurrentWord, [string]$ToolTip, [string]$Prefix = '' ) Get-ScStringValueCompletions -Values $Hints -CurrentWord $CurrentWord -ToolTip $ToolTip -Prefix $Prefix } function Get-ScOptionKeyFromToken { param( [string]$Token, [bool]$AllowInlineValue = $true ) if ([string]::IsNullOrWhiteSpace($Token)) { return $null } $match = [regex]::Match($Token, '^(?<name>[A-Za-z][A-Za-z0-9]*=)(?<value>.*)$') if (-not $match.Success) { return $null } $valuePortion = $match.Groups['value'].Value if (-not $AllowInlineValue -and -not [string]::IsNullOrEmpty($valuePortion)) { return $null } $match.Groups['name'].Value.ToLowerInvariant() } function Get-ScTagArgumentState { param( [string[]]$Arguments, [object[]]$OptionMetadata ) $optionMap = @{} foreach ($option in $OptionMetadata) { $optionMap[$option.Key.ToLowerInvariant()] = $option } $pendingKey = $null $positionals = New-Object System.Collections.Generic.List[string] $optionCounts = @{} foreach ($argument in $Arguments) { if ($pendingKey) { $pendingKey = $null continue } $inlineKey = Get-ScOptionKeyFromToken -Token $argument -AllowInlineValue $true if ($inlineKey -and $optionMap.ContainsKey($inlineKey)) { if (-not $optionCounts.ContainsKey($inlineKey)) { $optionCounts[$inlineKey] = 0 } $optionCounts[$inlineKey]++ if ($argument.TrimEnd().Equals($inlineKey, [System.StringComparison]::OrdinalIgnoreCase)) { $pendingKey = $inlineKey } continue } $positionals.Add($argument) } [pscustomobject]@{ PendingKey = $pendingKey Positionals = @($positionals) OptionCounts = $optionCounts OptionMap = $optionMap } } function Get-ScOptionMetadataByInlineToken { param( [string]$CurrentWord, [hashtable]$OptionMap ) $key = Get-ScOptionKeyFromToken -Token $CurrentWord -AllowInlineValue $true if ($key -and $OptionMap.ContainsKey($key)) { return $OptionMap[$key] } $null } function Get-ScOptionTokenCompletions { param( [object[]]$OptionMetadata, [string]$CurrentWord ) $results = @() foreach ($option in $OptionMetadata) { if (Test-ScStartsWith -Value $option.CanonicalToken -Prefix $CurrentWord) { $results += New-ScCompletionResult -CompletionText $option.CanonicalToken -ListItemText $option.CanonicalToken.TrimEnd() -ResultType 'ParameterName' -ToolTip $option.Description } } $results } function Get-ScQueryTypeValues { param([hashtable]$OptionCounts) $typeCount = 0 if ($OptionCounts -and $OptionCounts.ContainsKey('type=')) { $typeCount = [int]$OptionCounts['type='] } if ($typeCount -ge 1) { return @('own', 'share', 'interact', 'kernel', 'filesys', 'rec', 'adapt') } @('driver', 'service', 'userservice', 'all') } function Get-ScOptionValueCompletions { param( [pscustomobject]$Option, [string]$CurrentWord, [hashtable]$OptionCounts, [string]$InlinePrefix = '' ) $prefixWord = $CurrentWord if (-not [string]::IsNullOrEmpty($InlinePrefix)) { $prefixWord = $CurrentWord } switch ($Option.ValueKind) { 'StaticList' { return @(Get-ScStringValueCompletions -Values $Option.Values -CurrentWord $prefixWord -ToolTip $Option.Description -Prefix $InlinePrefix) } 'QueryType' { return @(Get-ScStringValueCompletions -Values (Get-ScQueryTypeValues -OptionCounts $OptionCounts) -CurrentWord $prefixWord -ToolTip $Option.Description -Prefix $InlinePrefix) } 'QueryState' { return @(Get-ScStringValueCompletions -Values @('inactive', 'all') -CurrentWord $prefixWord -ToolTip ($Option.Description + ' (default = active; omit state= for active services)') -Prefix $InlinePrefix) } 'Path' { # binPath= and command= are filesystem paths: hand them to the engine's own # filename completer (in-process) and keep its ProviderItem/ProviderContainer types. $pathResults = @( foreach ($pathResult in @([System.Management.Automation.CompletionCompleters]::CompleteFilename($CurrentWord))) { New-ScCompletionResult -CompletionText ($InlinePrefix + $pathResult.CompletionText) -ListItemText $pathResult.ListItemText -ResultType $pathResult.ResultType -ToolTip $pathResult.ToolTip } ) if ($pathResults.Count -gt 0) { return $pathResults } $placeholder = if ($Option.PSObject.Properties.Name -contains 'Placeholder') { $Option.Placeholder } else { '<path>' } if ([string]::IsNullOrEmpty($InlinePrefix)) { return @(New-ScLiteralValueResults -CurrentValue $CurrentWord -Placeholder $placeholder -ToolTip $Option.Description) } return @( New-ScCompletionResult -CompletionText ($InlinePrefix + $CurrentWord) -ListItemText $(if ([string]::IsNullOrEmpty($CurrentWord)) { $placeholder } else { $CurrentWord }) -ResultType 'ParameterValue' -ToolTip $Option.Description ) } 'BufferSize' { return @(Get-ScNumericCompletions -Hints $script:ScCompletionCatalog.BufferSizeHints -CurrentWord $prefixWord -ToolTip $Option.Description -Prefix $InlinePrefix) } 'ResumeIndex' { return @(Get-ScNumericCompletions -Hints @('0', '10', '100', '1000') -CurrentWord $prefixWord -ToolTip $Option.Description -Prefix $InlinePrefix) } 'FailureReset' { return @(Get-ScStringValueCompletions -Values $script:ScCompletionCatalog.FailureResetHints -CurrentWord $prefixWord -ToolTip $Option.Description -Prefix $InlinePrefix) } 'FailureActions' { return @(Get-ScStringValueCompletions -Values $script:ScCompletionCatalog.FailureActionHints -CurrentWord $prefixWord -ToolTip $Option.Description -Prefix $InlinePrefix) } 'FreeText' { $placeholder = if ($Option.PSObject.Properties.Name -contains 'Placeholder') { $Option.Placeholder } else { '<value>' } if ([string]::IsNullOrEmpty($InlinePrefix)) { return @(New-ScLiteralValueResults -CurrentValue $CurrentWord -Placeholder $placeholder -ToolTip $Option.Description) } if ([string]::IsNullOrEmpty($CurrentWord)) { return @( New-ScCompletionResult -CompletionText $InlinePrefix -ListItemText $placeholder -ResultType 'ParameterValue' -ToolTip $Option.Description ) } return @( New-ScCompletionResult -CompletionText ($InlinePrefix + $CurrentWord) -ListItemText $CurrentWord -ResultType 'ParameterValue' -ToolTip $Option.Description ) } } @() } function Get-ScAdjustedOptionCounts { param( [hashtable]$OptionCounts, [string]$CurrentOptionKey ) $adjusted = @{} if ($OptionCounts) { foreach ($entry in $OptionCounts.GetEnumerator()) { $adjusted[$entry.Key] = $entry.Value } } if ($CurrentOptionKey -and $adjusted.ContainsKey($CurrentOptionKey) -and [int]$adjusted[$CurrentOptionKey] -gt 0) { $adjusted[$CurrentOptionKey] = [int]$adjusted[$CurrentOptionKey] - 1 } $adjusted } function Complete-ScTaggedCommand { param( [string]$CurrentWord, [string[]]$ArgumentsBeforeCurrent, [object[]]$OptionMetadata, [string]$FirstPositionalKind, [string]$FirstPositionalToolTip, [string]$FirstPositionalPlaceholder ) $state = Get-ScTagArgumentState -Arguments $ArgumentsBeforeCurrent -OptionMetadata $OptionMetadata if ($state.Positionals.Count -eq 0) { switch ($FirstPositionalKind) { 'ServiceName' { return @(Get-ScServiceNameCompletions -CurrentWord $CurrentWord) } 'Literal' { return @(New-ScLiteralValueResults -CurrentValue $CurrentWord -Placeholder $FirstPositionalPlaceholder -ToolTip $FirstPositionalToolTip) } } } if ($state.PendingKey) { return @(Get-ScOptionValueCompletions -Option $state.OptionMap[$state.PendingKey] -CurrentWord $CurrentWord -OptionCounts (Get-ScAdjustedOptionCounts -OptionCounts $state.OptionCounts -CurrentOptionKey $state.PendingKey)) } $inlineOption = Get-ScOptionMetadataByInlineToken -CurrentWord $CurrentWord -OptionMap $state.OptionMap if ($inlineOption) { $inlineMatch = [regex]::Match($CurrentWord, '^(?<name>[A-Za-z][A-Za-z0-9]*=\s?)(?<value>.*)$') if ($inlineMatch.Success) { return @(Get-ScOptionValueCompletions -Option $inlineOption -CurrentWord $inlineMatch.Groups['value'].Value.TrimStart() -OptionCounts $state.OptionCounts -InlinePrefix $inlineOption.CanonicalToken) } } if ([string]::IsNullOrWhiteSpace($CurrentWord) -or $CurrentWord -match '^[A-Za-z]') { return @(Get-ScOptionTokenCompletions -OptionMetadata $OptionMetadata -CurrentWord $CurrentWord) } @() } function Complete-ScQueryLikeCommand { param( [string]$CurrentWord, [string[]]$ArgumentsBeforeCurrent ) $state = Get-ScTagArgumentState -Arguments $ArgumentsBeforeCurrent -OptionMetadata $script:ScCompletionCatalog.QueryOptions if ($state.PendingKey) { return @(Get-ScOptionValueCompletions -Option $state.OptionMap[$state.PendingKey] -CurrentWord $CurrentWord -OptionCounts (Get-ScAdjustedOptionCounts -OptionCounts $state.OptionCounts -CurrentOptionKey $state.PendingKey)) } $inlineOption = Get-ScOptionMetadataByInlineToken -CurrentWord $CurrentWord -OptionMap $state.OptionMap if ($inlineOption) { $inlineMatch = [regex]::Match($CurrentWord, '^(?<name>[A-Za-z][A-Za-z0-9]*=\s?)(?<value>.*)$') if ($inlineMatch.Success) { return @(Get-ScOptionValueCompletions -Option $inlineOption -CurrentWord $inlineMatch.Groups['value'].Value.TrimStart() -OptionCounts $state.OptionCounts -InlinePrefix $inlineOption.CanonicalToken) } } if ($state.Positionals.Count -gt 0) { return @() } $results = @() $results += @(Get-ScOptionTokenCompletions -OptionMetadata $script:ScCompletionCatalog.QueryOptions -CurrentWord $CurrentWord) $results += @(Get-ScServiceNameCompletions -CurrentWord $CurrentWord) Get-ScUniqueCompletions -Results $results } function Complete-ScServiceThenLiteral { param( [string]$CurrentWord, [string[]]$ArgumentsBeforeCurrent, [string]$Placeholder, [string]$ToolTip ) if ($ArgumentsBeforeCurrent.Count -eq 0) { return @(Get-ScServiceNameCompletions -CurrentWord $CurrentWord) } @(New-ScLiteralValueResults -CurrentValue $CurrentWord -Placeholder $Placeholder -ToolTip $ToolTip) } function Complete-ScServiceThenNumeric { param( [string]$CurrentWord, [string[]]$ArgumentsBeforeCurrent, [string[]]$Hints, [string]$ToolTip ) if ($ArgumentsBeforeCurrent.Count -eq 0) { return @(Get-ScServiceNameCompletions -CurrentWord $CurrentWord) } if ($ArgumentsBeforeCurrent.Count -eq 1) { return @(Get-ScNumericCompletions -Hints $Hints -CurrentWord $CurrentWord -ToolTip $ToolTip) } @() } function Complete-ScSingleServiceName { param( [string]$CurrentWord, [string[]]$ArgumentsBeforeCurrent ) if ($ArgumentsBeforeCurrent.Count -eq 0) { return @(Get-ScServiceNameCompletions -CurrentWord $CurrentWord) } @() } function Complete-ScStart { param( [string]$CurrentWord, [string[]]$ArgumentsBeforeCurrent ) if ($ArgumentsBeforeCurrent.Count -eq 0) { return @(Get-ScServiceNameCompletions -CurrentWord $CurrentWord) } @(New-ScLiteralValueResults -CurrentValue $CurrentWord -Placeholder '<service argument>' -ToolTip 'Additional argument passed to the service start request.') } function Complete-ScStop { param( [string]$CurrentWord, [string[]]$ArgumentsBeforeCurrent ) if ($ArgumentsBeforeCurrent.Count -eq 0) { return @(Get-ScServiceNameCompletions -CurrentWord $CurrentWord) } if ($ArgumentsBeforeCurrent.Count -eq 1) { $results = @() $results += @(Get-ScStringValueCompletions -Values $script:ScCompletionCatalog.StopReasonHints -CurrentWord $CurrentWord -ToolTip 'Stop reason in flag:major:minor form.') $results += @(New-ScLiteralValueResults -CurrentValue $CurrentWord -Placeholder '<flag:major:minor>' -ToolTip 'Stop reason in flag:major:minor form.') return @(Get-ScUniqueCompletions -Results $results) } @(New-ScLiteralValueResults -CurrentValue $CurrentWord -Placeholder '<comment>' -ToolTip 'Optional stop comment.') } function Complete-ScControl { param( [string]$CurrentWord, [string[]]$ArgumentsBeforeCurrent ) if ($ArgumentsBeforeCurrent.Count -eq 0) { return @(Get-ScServiceNameCompletions -CurrentWord $CurrentWord) } if ($ArgumentsBeforeCurrent.Count -eq 1) { $results = @() $results += @(Get-ScStringValueCompletions -Values $script:ScCompletionCatalog.ControlHints -CurrentWord $CurrentWord -ToolTip 'Named control value.') $results += @(Get-ScNumericCompletions -Hints @('128', '129', '130', '200') -CurrentWord $CurrentWord -ToolTip 'User-defined control code.') return @(Get-ScUniqueCompletions -Results $results) } @() } function Complete-ScValueOnly { param( [string]$CurrentWord, [string[]]$Values, [string]$ToolTip ) Get-ScStringValueCompletions -Values $Values -CurrentWord $CurrentWord -ToolTip $ToolTip } function Complete-ScByCommand { param( [string]$CommandKey, [string]$CurrentWord, [string[]]$ArgumentsBeforeCurrent ) switch ($CommandKey) { 'query' { return @(Complete-ScQueryLikeCommand -CurrentWord $CurrentWord -ArgumentsBeforeCurrent $ArgumentsBeforeCurrent) } 'queryex' { return @(Complete-ScQueryLikeCommand -CurrentWord $CurrentWord -ArgumentsBeforeCurrent $ArgumentsBeforeCurrent) } 'start' { return @(Complete-ScStart -CurrentWord $CurrentWord -ArgumentsBeforeCurrent $ArgumentsBeforeCurrent) } 'pause' { return @(Complete-ScSingleServiceName -CurrentWord $CurrentWord -ArgumentsBeforeCurrent $ArgumentsBeforeCurrent) } 'interrogate' { return @(Complete-ScSingleServiceName -CurrentWord $CurrentWord -ArgumentsBeforeCurrent $ArgumentsBeforeCurrent) } 'continue' { return @(Complete-ScSingleServiceName -CurrentWord $CurrentWord -ArgumentsBeforeCurrent $ArgumentsBeforeCurrent) } 'stop' { return @(Complete-ScStop -CurrentWord $CurrentWord -ArgumentsBeforeCurrent $ArgumentsBeforeCurrent) } 'config' { return @(Complete-ScTaggedCommand -CurrentWord $CurrentWord -ArgumentsBeforeCurrent $ArgumentsBeforeCurrent -OptionMetadata $script:ScCompletionCatalog.ConfigOptions -FirstPositionalKind 'ServiceName' -FirstPositionalToolTip 'Service name.' -FirstPositionalPlaceholder '<service name>') } 'create' { return @(Complete-ScTaggedCommand -CurrentWord $CurrentWord -ArgumentsBeforeCurrent $ArgumentsBeforeCurrent -OptionMetadata $script:ScCompletionCatalog.CreateOptions -FirstPositionalKind 'Literal' -FirstPositionalToolTip 'New service name.' -FirstPositionalPlaceholder '<service name>') } 'description' { return @(Complete-ScServiceThenLiteral -CurrentWord $CurrentWord -ArgumentsBeforeCurrent $ArgumentsBeforeCurrent -Placeholder '<description>' -ToolTip 'Service description text.') } 'failure' { return @(Complete-ScTaggedCommand -CurrentWord $CurrentWord -ArgumentsBeforeCurrent $ArgumentsBeforeCurrent -OptionMetadata $script:ScCompletionCatalog.FailureOptions -FirstPositionalKind 'ServiceName' -FirstPositionalToolTip 'Service name.' -FirstPositionalPlaceholder '<service name>') } 'failureflag' { if ($ArgumentsBeforeCurrent.Count -eq 0) { return @(Get-ScServiceNameCompletions -CurrentWord $CurrentWord) } return @(Get-ScStringValueCompletions -Values $script:ScCompletionCatalog.FailureFlagHints -CurrentWord $CurrentWord -ToolTip 'Failure actions flag value.') } 'sidtype' { if ($ArgumentsBeforeCurrent.Count -eq 0) { return @(Get-ScServiceNameCompletions -CurrentWord $CurrentWord) } return @(Get-ScStringValueCompletions -Values $script:ScCompletionCatalog.SidTypeHints -CurrentWord $CurrentWord -ToolTip 'Service SID type.') } 'privs' { return @(Complete-ScServiceThenLiteral -CurrentWord $CurrentWord -ArgumentsBeforeCurrent $ArgumentsBeforeCurrent -Placeholder '<privilege list>' -ToolTip 'Required privileges separated by forward slashes.') } 'managedaccount' { if ($ArgumentsBeforeCurrent.Count -eq 0) { return @(Get-ScServiceNameCompletions -CurrentWord $CurrentWord) } return @(Get-ScStringValueCompletions -Values $script:ScCompletionCatalog.ManagedBoolHints -CurrentWord $CurrentWord -ToolTip 'Whether the password is managed by LSA.') } 'qc' { return @(Complete-ScServiceThenNumeric -CurrentWord $CurrentWord -ArgumentsBeforeCurrent $ArgumentsBeforeCurrent -Hints $script:ScCompletionCatalog.BufferSizeHints -ToolTip 'Optional buffer size.') } 'qdescription' { return @(Complete-ScServiceThenNumeric -CurrentWord $CurrentWord -ArgumentsBeforeCurrent $ArgumentsBeforeCurrent -Hints $script:ScCompletionCatalog.BufferSizeHints -ToolTip 'Optional buffer size.') } 'qfailure' { return @(Complete-ScServiceThenNumeric -CurrentWord $CurrentWord -ArgumentsBeforeCurrent $ArgumentsBeforeCurrent -Hints $script:ScCompletionCatalog.BufferSizeHints -ToolTip 'Optional buffer size.') } 'qfailureflag' { return @(Complete-ScSingleServiceName -CurrentWord $CurrentWord -ArgumentsBeforeCurrent $ArgumentsBeforeCurrent) } 'qsidtype' { return @(Complete-ScSingleServiceName -CurrentWord $CurrentWord -ArgumentsBeforeCurrent $ArgumentsBeforeCurrent) } 'qprivs' { return @(Complete-ScServiceThenNumeric -CurrentWord $CurrentWord -ArgumentsBeforeCurrent $ArgumentsBeforeCurrent -Hints $script:ScCompletionCatalog.BufferSizeHints -ToolTip 'Optional buffer size.') } 'qtriggerinfo' { return @(Complete-ScServiceThenNumeric -CurrentWord $CurrentWord -ArgumentsBeforeCurrent $ArgumentsBeforeCurrent -Hints $script:ScCompletionCatalog.BufferSizeHints -ToolTip 'Optional buffer size.') } 'qpreferrednode' { return @(Complete-ScSingleServiceName -CurrentWord $CurrentWord -ArgumentsBeforeCurrent $ArgumentsBeforeCurrent) } 'qmanagedaccount' { return @(Complete-ScSingleServiceName -CurrentWord $CurrentWord -ArgumentsBeforeCurrent $ArgumentsBeforeCurrent) } 'qprotection' { return @(Complete-ScSingleServiceName -CurrentWord $CurrentWord -ArgumentsBeforeCurrent $ArgumentsBeforeCurrent) } 'quserservice' { return @(Complete-ScSingleServiceName -CurrentWord $CurrentWord -ArgumentsBeforeCurrent $ArgumentsBeforeCurrent) } 'delete' { return @(Complete-ScSingleServiceName -CurrentWord $CurrentWord -ArgumentsBeforeCurrent $ArgumentsBeforeCurrent) } 'control' { return @(Complete-ScControl -CurrentWord $CurrentWord -ArgumentsBeforeCurrent $ArgumentsBeforeCurrent) } 'sdshow' { return @(Complete-ScServiceThenLiteral -CurrentWord $CurrentWord -ArgumentsBeforeCurrent $ArgumentsBeforeCurrent -Placeholder '<showrights>' -ToolTip 'showrights argument.') } 'sdset' { return @(Complete-ScServiceThenLiteral -CurrentWord $CurrentWord -ArgumentsBeforeCurrent $ArgumentsBeforeCurrent -Placeholder '<SDDL>' -ToolTip 'Security descriptor in SDDL format.') } 'showsid' { return @(Get-ScCombinedNameCompletions -CurrentWord $CurrentWord) } 'triggerinfo' { if ($ArgumentsBeforeCurrent.Count -eq 0) { return @(Get-ScServiceNameCompletions -CurrentWord $CurrentWord) } return @(Get-ScStringValueCompletions -Values $script:ScCompletionCatalog.TriggerTemplates -CurrentWord $CurrentWord -ToolTip 'Trigger template from sc triggerinfo help.') } 'preferrednode' { if ($ArgumentsBeforeCurrent.Count -eq 0) { return @(Get-ScServiceNameCompletions -CurrentWord $CurrentWord) } return @(Get-ScNumericCompletions -Hints $script:ScCompletionCatalog.NodeNumberHints -CurrentWord $CurrentWord -ToolTip 'Preferred NUMA node number.') } 'getdisplayname' { if ($ArgumentsBeforeCurrent.Count -eq 0) { return @(Get-ScServiceNameCompletions -CurrentWord $CurrentWord) } return @(Get-ScNumericCompletions -Hints $script:ScCompletionCatalog.BufferSizeHints -CurrentWord $CurrentWord -ToolTip 'Buffer size.') } 'getkeyname' { if ($ArgumentsBeforeCurrent.Count -eq 0) { return @(Get-ScDisplayNameCompletions -CurrentWord $CurrentWord) } return @(Get-ScNumericCompletions -Hints $script:ScCompletionCatalog.BufferSizeHints -CurrentWord $CurrentWord -ToolTip 'Buffer size.') } 'enumdepend' { if ($ArgumentsBeforeCurrent.Count -eq 0) { return @(Get-ScServiceNameCompletions -CurrentWord $CurrentWord) } return @(Get-ScNumericCompletions -Hints $script:ScCompletionCatalog.BufferSizeHints -CurrentWord $CurrentWord -ToolTip 'Buffer size.') } 'boot' { return @(Complete-ScValueOnly -CurrentWord $CurrentWord -Values $script:ScCompletionCatalog.BootHints -ToolTip 'Boot state value.') } 'lock' { return @() } 'querylock' { return @() } } @() } function Complete-Sc { param( [string]$wordToComplete, [System.Management.Automation.Language.CommandAst]$commandAst, [int]$cursorPosition ) if (-not (Test-ScCommandAvailable)) { return @() } Initialize-ScCompletionCatalog # $cursorPosition is line-absolute; the extent text is command-relative. $relativeCursor = [Math]::Max(0, $cursorPosition - $commandAst.Extent.StartOffset) $tokenState = Get-ScCurrentTokenState -Line $commandAst.Extent.Text -CursorPosition $relativeCursor if ([string]::IsNullOrEmpty($wordToComplete) -and $relativeCursor -gt $commandAst.Extent.Text.Length) { $allTokens = @($tokenState.TokensBeforeCurrent) if (-not [string]::IsNullOrEmpty($tokenState.CurrentToken)) { $allTokens += $tokenState.CurrentToken } $tokenState = [pscustomobject]@{ TokensBeforeCurrent = $allTokens CurrentToken = '' HasTrailingSpace = $true } } elseif (-not [string]::IsNullOrEmpty($wordToComplete)) { $tokenState = [pscustomobject]@{ TokensBeforeCurrent = $tokenState.TokensBeforeCurrent CurrentToken = $wordToComplete HasTrailingSpace = $tokenState.HasTrailingSpace } } $commandContext = Get-ScCommandContext -ArgumentsBeforeCurrent (Get-ScArgumentsFromTokenState -TokenState $tokenState).ArgumentsBeforeCurrent $currentWord = (Get-ScArgumentsFromTokenState -TokenState $tokenState).CurrentWord if (-not $commandContext.Command) { $results = @() $results += @(Get-ScCommandCompletions -CurrentWord $currentWord) if ($null -eq $commandContext.Server) { if ([string]::IsNullOrWhiteSpace($currentWord) -or $currentWord -like '\\*') { $results += @(Get-ScServerPlaceholderCompletions -CurrentWord $currentWord) } } return @(Get-ScUniqueCompletions -Results $results) } if (-not $script:ScCompletionCatalog.CommandInfoByKey.ContainsKey($commandContext.CommandKey)) { return @(Get-ScCommandCompletions -CurrentWord $currentWord) } @(Get-ScUniqueCompletions -Results (Complete-ScByCommand -CommandKey $commandContext.CommandKey -CurrentWord $currentWord -ArgumentsBeforeCurrent $commandContext.CommandArguments)) } Register-ArgumentCompleter -Native -CommandName 'sc', 'sc.exe' -ScriptBlock { param($wordToComplete, $commandAst, $cursorPosition) Complete-Sc -wordToComplete $wordToComplete -commandAst $commandAst -cursorPosition $cursorPosition } |