completers/xargs_completer/xargs_completer.ps1
|
# xargs tab completion for PowerShell # Registers a native PowerShell argument completer for xargs.exe using the documented # coreutils option surface and lightweight value-aware completion. Set-StrictMode -Version 2.0 function New-XargsCompletionResult { 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-XargsOptionSpecs { @( [pscustomobject]@{ Token = '-0'; LongToken = '--null'; Description = 'Items are separated by a null, not whitespace'; ValueKind = 'NoValue' }, [pscustomobject]@{ Token = '-a'; LongToken = '--arg-file'; Description = 'Read arguments from FILE, not standard input'; ValueKind = 'FilePath' }, [pscustomobject]@{ Token = '-d'; LongToken = '--delimiter'; Description = 'Items in input stream are separated by CHARACTER'; ValueKind = 'Delimiter' }, [pscustomobject]@{ Token = '-E'; LongToken = ''; Description = 'Set logical EOF string'; ValueKind = 'EofString' }, [pscustomobject]@{ Token = '-e'; LongToken = '--eof'; Description = 'Equivalent to -E END if END is specified'; ValueKind = 'EofString' }, [pscustomobject]@{ Token = '-I'; LongToken = ''; Description = 'Same as --replace=R'; ValueKind = 'ReplaceText' }, [pscustomobject]@{ Token = '-i'; LongToken = '--replace'; Description = 'Replace R in INITIAL-ARGS with names read from standard input (R only in the attached form -iR / --replace=R; otherwise {})'; ValueKind = 'ReplaceTextAttached' }, [pscustomobject]@{ Token = '-L'; LongToken = '--max-lines'; Description = 'Use at most MAX-LINES non-blank input lines per command line'; ValueKind = 'Integer' }, [pscustomobject]@{ Token = '-l'; LongToken = ''; Description = 'Similar to -L but defaults to at most one non-blank input line'; ValueKind = 'OptionalInteger' }, [pscustomobject]@{ Token = '-n'; LongToken = '--max-args'; Description = 'Use at most MAX-ARGS arguments per command line'; ValueKind = 'Integer' }, [pscustomobject]@{ Token = '-o'; LongToken = '--open-tty'; Description = 'Reopen stdin as /dev/tty in the child process'; ValueKind = 'NoValue' }, [pscustomobject]@{ Token = '-P'; LongToken = '--max-procs'; Description = 'Run at most MAX-PROCS processes at a time'; ValueKind = 'Integer' }, [pscustomobject]@{ Token = '-p'; LongToken = '--interactive'; Description = 'Prompt before running commands'; ValueKind = 'NoValue' }, [pscustomobject]@{ Token = ''; LongToken = '--process-slot-var'; Description = 'Set environment variable VAR in child processes'; ValueKind = 'VarName' }, [pscustomobject]@{ Token = '-r'; LongToken = '--no-run-if-empty'; Description = 'If there are no arguments, then do not run COMMAND'; ValueKind = 'NoValue' }, [pscustomobject]@{ Token = '-s'; LongToken = '--max-chars'; Description = 'Limit length of command line to MAX-CHARS'; ValueKind = 'Integer' }, [pscustomobject]@{ Token = ''; LongToken = '--show-limits'; Description = 'Show limits on command-line length'; ValueKind = 'NoValue' }, [pscustomobject]@{ Token = '-t'; LongToken = '--verbose'; Description = 'Print commands before executing them'; ValueKind = 'NoValue' }, [pscustomobject]@{ Token = '-x'; LongToken = '--exit'; Description = 'Exit if the size (see -s) is exceeded'; ValueKind = 'NoValue' }, [pscustomobject]@{ Token = ''; LongToken = '--help'; Description = 'Display help and exit'; ValueKind = 'NoValue' }, [pscustomobject]@{ Token = ''; LongToken = '--version'; Description = 'Output version information and exit'; ValueKind = 'NoValue' } ) } function Get-XargsOptionSpecByToken { param([string]$Token) $cleanToken = if ([string]::IsNullOrWhiteSpace($Token)) { '' } else { $Token.Trim() } if ([string]::IsNullOrWhiteSpace($cleanToken)) { return $null } # Ordinal: -P/-p, -L/-l, -I/-i and -E/-e are distinct options. foreach ($option in Get-XargsOptionSpecs) { if ([string]::Equals($option.Token, $cleanToken, [System.StringComparison]::Ordinal) -or [string]::Equals($option.LongToken, $cleanToken, [System.StringComparison]::Ordinal)) { return $option } } $null } function Get-XargsOptionSuggestions { param( [string]$CurrentToken, [string]$Prefix = '' ) $results = New-Object System.Collections.Generic.List[object] $typed = if ($null -eq $CurrentToken) { '' } else { $CurrentToken } if ([string]::IsNullOrWhiteSpace($Prefix)) { $Prefix = '' } foreach ($option in Get-XargsOptionSpecs) { $matchingText = if ($typed.StartsWith('--')) { $option.LongToken } else { $option.Token } if ([string]::IsNullOrWhiteSpace($matchingText)) { continue } if ($matchingText -clike ([System.Management.Automation.WildcardPattern]::Escape($typed) + '*')) { $completionText = if ($Prefix) { $Prefix + $matchingText } else { $matchingText } [void]$results.Add((New-XargsCompletionResult -CompletionText $completionText -ResultType 'ParameterName' -ToolTip $option.Description -ListItemText $matchingText)) } } @($results.ToArray()) } function Get-XargsPathCompletions { param( [string]$CurrentValue, [string]$Prefix = '' ) $results = New-Object System.Collections.Generic.List[object] $cleanValue = if ($null -eq $CurrentValue) { '' } else { $CurrentValue.Trim([char[]]@([char]34, [char]39)) } $alwaysQuote = -not [string]::IsNullOrEmpty($CurrentValue) -and $CurrentValue.StartsWith('"') if ([string]::IsNullOrWhiteSpace($cleanValue)) { $parent = '.' $leaf = '' } elseif ($cleanValue.EndsWith('\') -or $cleanValue.EndsWith('/')) { $parent = $cleanValue $leaf = '' } else { $parent = Split-Path -Path $cleanValue -Parent if ([string]::IsNullOrWhiteSpace($parent)) { $parent = '.' } $leaf = Split-Path -Path $cleanValue -Leaf } $filter = if ([string]::IsNullOrWhiteSpace($leaf)) { '*' } else { "$leaf*" } foreach ($item in @(Get-ChildItem -Path $parent -Filter $filter -ErrorAction SilentlyContinue)) { $completionText = if ($cleanValue -and -not [System.IO.Path]::IsPathRooted($cleanValue)) { if ($parent -eq '.') { $item.Name } else { Join-Path -Path $parent -ChildPath $item.Name } } else { $item.FullName } if ($item.PSIsContainer -and -not $completionText.EndsWith([System.IO.Path]::DirectorySeparatorChar)) { $completionText += [System.IO.Path]::DirectorySeparatorChar } if ($alwaysQuote -and -not $completionText.StartsWith('"')) { $completionText = '"' + $completionText + '"' } $completionText = $Prefix + $completionText [void]$results.Add((New-XargsCompletionResult -CompletionText $completionText -ResultType 'ParameterValue' -ToolTip $item.FullName -ListItemText $item.Name)) } @($results.ToArray()) } function Get-XargsValueSuggestions { param( [pscustomobject]$OptionSpec, [string]$CurrentValue, [string]$Prefix = '' ) if ($null -eq $OptionSpec) { return @() } $typed = if ($null -eq $CurrentValue) { '' } else { $CurrentValue } $toolTip = if ([string]::IsNullOrWhiteSpace($OptionSpec.Description)) { $OptionSpec.Token } else { $OptionSpec.Description } if ($OptionSpec.ValueKind -eq 'FilePath') { $results = @(Get-XargsPathCompletions -CurrentValue $typed -Prefix $Prefix) if ($results.Count -gt 0) { return $results } } if (-not [string]::IsNullOrWhiteSpace($typed)) { return @() } $placeholder = switch ($OptionSpec.ValueKind) { 'FilePath' { '<arg-file>' } 'Delimiter' { '<delimiter>' } 'Integer' { '<max>' } 'OptionalInteger' { '<max-lines>' } 'ReplaceText' { '<R>' } 'ReplaceTextAttached' { '<R>' } 'EofString' { '<eof-string>' } 'VarName' { '<var>' } default { $null } } if ($null -eq $placeholder) { return @() } @(New-XargsCompletionResult -CompletionText ($Prefix + $placeholder) -ResultType 'ParameterValue' -ToolTip $toolTip -ListItemText $placeholder) } function Get-XargsCommandNames { $cache = Get-Variable -Name 'XargsCommandCache' -Scope Script -ErrorAction Ignore if ($null -ne $cache -and $null -ne $cache.Value -and $cache.Value.Path -eq $env:PATH) { return $cache.Value.Names } $names = @( Get-Command -CommandType Application -ErrorAction SilentlyContinue | ForEach-Object { [System.IO.Path]::GetFileNameWithoutExtension($_.Name) } | Where-Object { $_ -and $_ -ne 'xargs' } | Sort-Object -Unique ) Set-Variable -Name 'XargsCommandCache' -Value @{ Path = $env:PATH; Names = $names } -Scope Script $names } function Get-XargsCommandSuggestions { param([string]$CurrentValue) $typed = if ($null -eq $CurrentValue) { '' } else { $CurrentValue } @( foreach ($name in Get-XargsCommandNames) { if ($name.StartsWith($typed, [System.StringComparison]::OrdinalIgnoreCase)) { New-XargsCompletionResult -CompletionText $name -ResultType 'Command' -ToolTip 'Command to execute' -ListItemText $name } } ) } function Test-XargsCommandOperandSeen { param([string[]]$TokensBeforeCurrent) $skipNext = $false foreach ($token in @($TokensBeforeCurrent)) { if ($skipNext) { $skipNext = $false continue } if ($token -match '^--[A-Za-z0-9-]+=') { continue } # -i/--replace only take R attached, so the token after them is the COMMAND. if ($token -match '^--[A-Za-z0-9-]+$') { $spec = Get-XargsOptionSpecByToken -Token $token if ($spec -and $spec.ValueKind -notin @('NoValue', 'OptionalInteger', 'ReplaceTextAttached')) { $skipNext = $true } continue } if ($token -match '^-[A-Za-z0-9]') { $spec = Get-XargsOptionSpecByToken -Token $token.Substring(0, 2) if ($spec -and $token.Length -eq 2 -and $spec.ValueKind -notin @('NoValue', 'OptionalInteger', 'ReplaceTextAttached')) { $skipNext = $true } continue } return $true } $false } function Get-XargsCompletionContext { param( [string]$CurrentToken, [string[]]$TokensBeforeCurrent ) $currentValue = $CurrentToken if ($currentValue -match '^(?<option>--[A-Za-z0-9][A-Za-z0-9\-]*)=(?<value>.*)$') { $optionSpec = Get-XargsOptionSpecByToken -Token $matches['option'] if ($optionSpec -and $optionSpec.ValueKind -ne 'NoValue') { return [pscustomobject]@{ OptionSpec = $optionSpec ValueText = $matches['value'] Prefix = $matches['option'] + '=' } } } if ($TokensBeforeCurrent.Count -gt 0) { $lastToken = $TokensBeforeCurrent[-1] $optionSpec = Get-XargsOptionSpecByToken -Token $lastToken if ($optionSpec -and $optionSpec.ValueKind -notin @('NoValue', 'ReplaceTextAttached')) { return [pscustomobject]@{ OptionSpec = $optionSpec ValueText = $currentValue Prefix = '' } } } $null } Register-ArgumentCompleter -Native -CommandName 'xargs', 'xargs.exe' -ScriptBlock { param($wordToComplete, $commandAst, $cursorPosition) $tokensBeforeCurrent = @() foreach ($element in $commandAst.CommandElements | Select-Object -Skip 1) { if ($element.Extent.EndOffset -lt $cursorPosition) { $tokensBeforeCurrent += $element.Extent.Text } } $context = Get-XargsCompletionContext -CurrentToken $wordToComplete -TokensBeforeCurrent $tokensBeforeCurrent if ($context) { return Get-XargsValueSuggestions -OptionSpec $context.OptionSpec -CurrentValue $context.ValueText -Prefix $context.Prefix } if (-not [string]::IsNullOrWhiteSpace($wordToComplete) -and $wordToComplete.StartsWith('-')) { return Get-XargsOptionSuggestions -CurrentToken $wordToComplete } if (-not (Test-XargsCommandOperandSeen -TokensBeforeCurrent $tokensBeforeCurrent)) { return Get-XargsCommandSuggestions -CurrentValue $wordToComplete } @() } |