completers/psfile_completer/psfile_completer.ps1
|
# psfile tab completion for PowerShell # Static native completer for PsFile with non-enumerating remote and file-id/path hints. Set-StrictMode -Version 2.0 function New-PsFileCompletionResult { 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-PsFileSwitchCatalog { @( @{ Token = '-u'; Description = 'Specifies optional user name for login to the remote computer.'; NeedsRemote = $true; NeedsIdentifier = $false; SlashOnly = $false } @{ Token = '-p'; Description = 'Specifies password for the user name given with -u.'; NeedsRemote = $true; NeedsIdentifier = $false; SlashOnly = $false } @{ Token = '-c'; Description = 'Closes the file identified by the preceding file Id. Destructive; completion never runs it.'; NeedsRemote = $false; NeedsIdentifier = $true; SlashOnly = $false } @{ Token = '-nobanner'; Description = 'Do not display the startup banner and copyright message.'; NeedsRemote = $false; NeedsIdentifier = $false; SlashOnly = $false } @{ Token = '-accepteula'; Description = 'Suppress the first-run EULA dialog; required for unattended use.'; NeedsRemote = $false; NeedsIdentifier = $false; SlashOnly = $false } @{ Token = '-?'; Description = 'Display PsFile help.'; NeedsRemote = $false; NeedsIdentifier = $false; SlashOnly = $false } @{ Token = '/?'; Description = 'Display PsFile help.'; NeedsRemote = $false; NeedsIdentifier = $false; SlashOnly = $false } @{ Token = '/nobanner'; Description = 'Slash form of -nobanner.'; NeedsRemote = $false; NeedsIdentifier = $false; SlashOnly = $true } @{ Token = '/accepteula'; Description = 'Slash form of -accepteula.'; NeedsRemote = $false; NeedsIdentifier = $false; SlashOnly = $true } ) } function Get-PsFileSwitchKey { param([string]$Token) if ([string]::IsNullOrEmpty($Token)) { return '' } if (-not ($Token.StartsWith('-') -or $Token.StartsWith('/'))) { return '' } $Token.Substring(1).ToLowerInvariant() } function Get-PsFileCurrentToken { 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 Get-PsFileArgumentState { param([System.Management.Automation.Language.CommandAst]$CommandAst, [string]$WordToComplete, [int]$CursorPosition) $relativeCursor = $CursorPosition - $CommandAst.Extent.StartOffset $currentWord = if ([string]::IsNullOrEmpty($WordToComplete)) { '' } else { Get-PsFileCurrentToken -Line $CommandAst.Extent.Text -CursorPosition $relativeCursor -Fallback $WordToComplete } $tokens = @($CommandAst.CommandElements | Select-Object -Skip 1 | ForEach-Object { $_.Extent.Text }) $tokensBeforeCurrent = @($tokens) 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 Add-PsFileResult { param( [System.Collections.Generic.List[object]]$Results, [System.Collections.Generic.HashSet[string]]$Seen, [string]$CurrentWord, [string]$CompletionText, [string]$ResultType, [string]$ToolTip ) if (-not [string]::IsNullOrEmpty($CurrentWord) -and -not $CompletionText.StartsWith($CurrentWord, [System.StringComparison]::OrdinalIgnoreCase)) { return } if (-not $Seen.Add($CompletionText)) { return } [void]$Results.Add((New-PsFileCompletionResult -CompletionText $CompletionText -ResultType $ResultType -ToolTip $ToolTip)) } function Complete-PsFile { param( [string]$WordToComplete, [System.Management.Automation.Language.CommandAst]$CommandAst, [int]$CursorPosition ) $state = Get-PsFileArgumentState -CommandAst $CommandAst -WordToComplete $WordToComplete -CursorPosition $CursorPosition $currentWord = $state.CurrentWord $tokensBeforeCurrent = @($state.TokensBeforeCurrent) $used = @{} $valueContext = $null $remoteTarget = $null $identifier = $null $closeMode = $false for ($i = 0; $i -lt $tokensBeforeCurrent.Count; $i++) { $token = $tokensBeforeCurrent[$i] $key = Get-PsFileSwitchKey -Token $token if ($key -eq 'u') { $used['u'] = $true if ($i -eq ($tokensBeforeCurrent.Count - 1)) { $valueContext = 'User'; break } $i++ continue } if ($key -eq 'p') { $used['p'] = $true if ($i -eq ($tokensBeforeCurrent.Count - 1)) { $valueContext = 'Password'; break } $i++ continue } if ($key -eq 'c') { $used['c'] = $true $closeMode = $true continue } if ($key) { # Any other dash- or slash-prefixed token is a switch, never the file identifier. $used[$key] = $true continue } if (-not $remoteTarget -and $token.StartsWith('\\')) { $remoteTarget = $token continue } if (-not $identifier) { $identifier = $token } } switch ($valueContext) { 'User' { return @( New-PsFileCompletionResult -CompletionText '<username>' -ResultType 'ParameterValue' -ToolTip 'Remote user name.' New-PsFileCompletionResult -CompletionText '<domain\user>' -ResultType 'ParameterValue' -ToolTip 'Remote user name in Domain\User syntax.' ) } 'Password' { $value = if ([string]::IsNullOrWhiteSpace($currentWord)) { '<password>' } else { $currentWord } return @(New-PsFileCompletionResult -CompletionText $value -ResultType 'ParameterValue' -ToolTip 'Remote password value.') } } $results = [System.Collections.Generic.List[object]]::new() $seen = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) # Usage: psfile [\\RemoteComputer [-u Username [-p Password]]] [[Id | path] [-c]] # The remote target is only legal before the file identifier. if (-not $remoteTarget -and -not $identifier -and -not $closeMode) { foreach ($target in @('\\<RemoteComputer>', '\\localhost', '\\*')) { Add-PsFileResult -Results $results -Seen $seen -CurrentWord $currentWord -CompletionText $target -ResultType 'ParameterValue' -ToolTip 'Remote computer placeholder for PsFile.' } } foreach ($switchSpec in Get-PsFileSwitchCatalog) { if ($used.ContainsKey((Get-PsFileSwitchKey -Token $switchSpec.Token))) { continue } if ($switchSpec.NeedsRemote -and -not $remoteTarget) { continue } if ($switchSpec.NeedsIdentifier -and -not $identifier) { continue } if ($switchSpec.SlashOnly -and -not $currentWord.StartsWith('/')) { continue } Add-PsFileResult -Results $results -Seen $seen -CurrentWord $currentWord -CompletionText $switchSpec.Token -ResultType 'ParameterName' -ToolTip $switchSpec.Description } if (-not $identifier -and -not $closeMode) { foreach ($value in @('<file-id>', '<path>', '"C:\path\fragment*"')) { Add-PsFileResult -Results $results -Seen $seen -CurrentWord $currentWord -CompletionText $value -ResultType 'ParameterValue' -ToolTip 'PsFile file identifier or path pattern.' } } @($results.ToArray()) } Register-ArgumentCompleter -Native -CommandName @('psfile', 'psfile.exe') -ScriptBlock { param($wordToComplete, $commandAst, $cursorPosition) Complete-PsFile -WordToComplete $wordToComplete -CommandAst $commandAst -CursorPosition $cursorPosition } |