completers/shellrunas_completer/shellrunas_completer.ps1
|
# shellrunas tab completion for PowerShell # Static native completer based on official Sysinternals web documentation. Set-StrictMode -Version 2.0 function New-ShellRunasCompletionResult { 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 Get-ShellRunasCurrentToken { 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 Remove-ShellRunasOuterQuotes { 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 ConvertTo-ShellRunasQuotedValue { param([string]$Value, [bool]$AlwaysQuote = $false) if ([string]::IsNullOrWhiteSpace($Value)) { return $Value } if (($AlwaysQuote -or $Value -match '\s') -and -not ($Value.StartsWith('"') -and $Value.EndsWith('"'))) { return '"' + $Value.Replace('`', '``').Replace('"', '`"') + '"' } $Value } function Get-ShellRunasArgumentState { param([System.Management.Automation.Language.CommandAst]$CommandAst, [string]$WordToComplete, [int]$CursorPosition) $currentWord = if ([string]::IsNullOrEmpty($WordToComplete)) { '' } else { Get-ShellRunasCurrentToken -Line $CommandAst.Extent.Text -CursorPosition ($CursorPosition - $CommandAst.Extent.StartOffset) -Fallback $WordToComplete } $tokensBeforeCurrent = @( $CommandAst.CommandElements | Select-Object -Skip 1 | Where-Object { $_.Extent.EndOffset -le $CursorPosition } | ForEach-Object { $_.Extent.Text } ) if (-not [string]::IsNullOrEmpty($currentWord) -and $tokensBeforeCurrent.Count -gt 0 -and $tokensBeforeCurrent[-1] -eq $currentWord) { $tokensBeforeCurrent = @($tokensBeforeCurrent | Select-Object -First ($tokensBeforeCurrent.Count - 1)) } [pscustomobject]@{ CurrentWord = $currentWord TokensBeforeCurrent = $tokensBeforeCurrent } } function Get-ShellRunasProgramCompletions { param([string]$CurrentWord) $trimmed = Remove-ShellRunasOuterQuotes -Value $CurrentWord $alwaysQuote = -not [string]::IsNullOrEmpty($CurrentWord) -and $CurrentWord.StartsWith('"') $results = New-Object System.Collections.Generic.List[object] if ([string]::IsNullOrWhiteSpace($trimmed)) { foreach ($sample in @('notepad.exe', 'cmd.exe', 'powershell.exe', 'pwsh.exe')) { [void]$results.Add((New-ShellRunasCompletionResult -CompletionText $sample -ResultType 'ParameterValue' -ToolTip 'Program to run with alternate credentials.')) } } if ($trimmed -match '[\\/]|^\.' -or $trimmed -match '^[A-Za-z]:') { if ($trimmed -match '[\\/]$') { $parent = $trimmed $filter = '*' } else { $parent = Split-Path -Path $trimmed -Parent if ([string]::IsNullOrWhiteSpace($parent)) { $parent = '.' } $leaf = Split-Path -Path $trimmed -Leaf $filter = if ([string]::IsNullOrWhiteSpace($leaf)) { '*' } else { "$leaf*" } } foreach ($item in @(Get-ChildItem -Path $parent -Filter $filter -ErrorAction SilentlyContinue)) { $completionPath = if ($trimmed -and -not [System.IO.Path]::IsPathRooted($trimmed) -and $parent -ne '.') { Join-Path -Path $parent -ChildPath $item.Name } elseif ($parent -eq '.') { $item.Name } else { $item.FullName } if ($item.PSIsContainer -and -not $completionPath.EndsWith([System.IO.Path]::DirectorySeparatorChar)) { $completionPath += [System.IO.Path]::DirectorySeparatorChar } [void]$results.Add((New-ShellRunasCompletionResult -CompletionText (ConvertTo-ShellRunasQuotedValue -Value $completionPath -AlwaysQuote:$alwaysQuote) -ResultType $(if ($item.PSIsContainer) { 'ProviderContainer' } else { 'ParameterValue' }) -ToolTip $item.FullName)) } } else { foreach ($command in @(Get-Command -Name "$trimmed*" -CommandType Application -ErrorAction SilentlyContinue | Sort-Object -Property Name -Unique | Select-Object -First 20)) { [void]$results.Add((New-ShellRunasCompletionResult -CompletionText $command.Name -ResultType 'ParameterValue' -ToolTip $command.Name)) } } if ($results.Count -eq 0) { return @(New-ShellRunasCompletionResult -CompletionText $(if ([string]::IsNullOrWhiteSpace($CurrentWord)) { '<program>' } else { $CurrentWord }) -ResultType 'ParameterValue' -ToolTip 'Program to run with alternate credentials.') } $seen = @{} $unique = foreach ($item in $results) { if ($seen.ContainsKey($item.CompletionText)) { continue } $seen[$item.CompletionText] = $true $item } @($unique) } function Complete-ShellRunas { param( [string]$WordToComplete, [System.Management.Automation.Language.CommandAst]$CommandAst, [int]$CursorPosition ) $state = Get-ShellRunasArgumentState -CommandAst $CommandAst -WordToComplete $WordToComplete -CursorPosition $CursorPosition $currentWord = $state.CurrentWord $tokensBeforeCurrent = @($state.TokensBeforeCurrent) $mode = $null $quiet = $false $netOnly = $false $program = $null foreach ($token in $tokensBeforeCurrent) { $lowerToken = $token.ToLowerInvariant() if ($lowerToken -eq '/reg') { $mode = 'Register'; continue } if ($lowerToken -eq '/regnetonly') { $mode = 'RegisterNetOnly'; continue } if ($lowerToken -eq '/unreg') { $mode = 'Unregister'; continue } if ($lowerToken -eq '/quiet') { $quiet = $true; continue } if ($lowerToken -eq '/netonly' -and -not $mode) { $netOnly = $true; continue } if (-not $program) { $program = $token continue } } if ($program) { return @(New-ShellRunasCompletionResult -CompletionText $(if ([string]::IsNullOrWhiteSpace($currentWord)) { '<argument>' } else { $currentWord }) -ResultType 'ParameterValue' -ToolTip 'ShellRunas passes later arguments through without local enumeration.') } if (-not $mode -and -not [string]::IsNullOrWhiteSpace($currentWord) -and -not $currentWord.StartsWith('/')) { return Get-ShellRunasProgramCompletions -CurrentWord $currentWord } $results = New-Object System.Collections.Generic.List[object] if (-not $mode) { if (-not $netOnly) { foreach ($item in @( @{ Token = '/reg'; Description = 'Register the ShellRunas shell context-menu entry.' } @{ Token = '/regnetonly'; Description = 'Register the Shell /netonly context-menu entry.' } @{ Token = '/unreg'; Description = 'Unregister the ShellRunas shell context-menu entry.' } @{ Token = '/netonly'; Description = 'Use specified credentials for remote access only when launching a program.' } )) { if ([string]::IsNullOrWhiteSpace($currentWord) -or $item.Token.StartsWith($currentWord, [System.StringComparison]::OrdinalIgnoreCase)) { [void]$results.Add((New-ShellRunasCompletionResult -CompletionText $item.Token -ResultType 'ParameterName' -ToolTip $item.Description)) } } } if ([string]::IsNullOrWhiteSpace($currentWord) -or -not $currentWord.StartsWith('/')) { foreach ($item in @(Get-ShellRunasProgramCompletions -CurrentWord $currentWord)) { [void]$results.Add($item) } } return @($results.ToArray()) } if ($mode -in @('Register', 'RegisterNetOnly', 'Unregister')) { if (-not $quiet -and ([string]::IsNullOrWhiteSpace($currentWord) -or '/quiet'.StartsWith($currentWord, [System.StringComparison]::OrdinalIgnoreCase))) { [void]$results.Add((New-ShellRunasCompletionResult -CompletionText '/quiet' -ResultType 'ParameterName' -ToolTip 'Register or unregister without showing a result dialog.')) } return @($results.ToArray()) } Get-ShellRunasProgramCompletions -CurrentWord $currentWord } Register-ArgumentCompleter -Native -CommandName @('shellrunas', 'shellrunas.exe') -ScriptBlock { param($wordToComplete, $commandAst, $cursorPosition) Complete-ShellRunas -WordToComplete $wordToComplete -CommandAst $commandAst -CursorPosition $cursorPosition } |