completers/curl_completer/curl_completer.ps1
|
# curl tab completion for PowerShell # Builds a help-driven option catalog for curl.exe and adds value-aware completion. Set-StrictMode -Version 2.0 function New-CurlCompletionResult { 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-CurlUniqueStrings { param([string[]]$Items) $seen = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) $results = New-Object System.Collections.Generic.List[string] foreach ($item in @($Items)) { if ([string]::IsNullOrWhiteSpace($item)) { continue } if ($seen.Add($item)) { [void]$results.Add($item) } } @($results.ToArray()) } function Get-CurlDefaultHelpSubjects { @( 'all', 'auth', 'category', 'connection', 'curl', 'deprecated', 'dns', 'file', 'ftp', 'global', 'http', 'imap', 'ldap', 'output', 'pop3', 'post', 'proxy', 'scp', 'sftp', 'smtp', 'ssh', 'telnet', 'tftp', 'timeout', 'tls', 'upload', 'verbose', 'manual' ) } function Get-CurlCompletionCatalog { if (Get-Variable -Name CurlCompletionCatalog -Scope Script -ErrorAction Ignore) { return $script:CurlCompletionCatalog } $script:CurlCompletionCatalog = @{ Initialized = $false CommandName = $null HelpSubjects = Get-CurlDefaultHelpSubjects Protocols = @() Options = @() OptionByToken = [System.Collections.Generic.Dictionary[string, object]]::new([System.StringComparer]::Ordinal) } $script:CurlCompletionCatalog } function Resolve-CurlCommandName { $catalog = Get-CurlCompletionCatalog if ($catalog.CommandName) { return $catalog.CommandName } $command = Get-Command -Name curl.exe, curl -ErrorAction Ignore | Select-Object -First 1 if ($command) { $catalog.CommandName = if ($command.Source) { $command.Source } else { $command.Name } } $catalog.CommandName } function Invoke-CurlCapture { param([string[]]$Arguments) $commandName = Resolve-CurlCommandName if (-not $commandName) { return @() } try { @(& $commandName @Arguments 2>$null | ForEach-Object { $_ -replace '\e\[[0-9;?]*[ -/]*[@-~]', '' }) } catch { @() } } function Remove-CurlOuterQuotes { param([string]$Value) if ($null -eq $Value) { return '' } $Value.Trim([char[]]@([char]34, [char]39)) } function ConvertTo-CurlQuotedValue { 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('"'))) { $escaped = $Value.Replace('`', '``').Replace('"', '`"') return '"' + $escaped + '"' } $Value } function ConvertTo-CurlQuotedArgument { # Renders a value as one quoted PowerShell argument in the given quote style. param( [string]$Value, [string]$Quote ) if ($Quote -eq '"') { return '"' + ($Value -replace '([`"$\u201C-\u201E])', '`$1') + '"' } "'" + ($Value -replace '([''\u2018-\u201B])', '$1$1') + "'" } function Test-CurlPathLikeInput { param([string]$Value) if ([string]::IsNullOrWhiteSpace($Value)) { return $false } $cleanValue = Remove-CurlOuterQuotes -Value $Value $cleanValue -match '^(?:\.{1,2}[\\/]|[\\/]|~[\\/]|[A-Za-z]:|\\\\)' } function Get-CurlTokenText { param([System.Management.Automation.Language.Ast]$Element) if ($Element -is [System.Management.Automation.Language.StringConstantExpressionAst]) { return $Element.Value } if ($Element -is [System.Management.Automation.Language.CommandParameterAst]) { return $Element.Extent.Text } $Element.Extent.Text } function Get-CurlCurrentToken { 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-CurlArgumentTokens { param( [System.Management.Automation.Language.CommandAst]$CommandAst, [int]$CursorPosition ) $tokens = @() foreach ($element in $CommandAst.CommandElements | Select-Object -Skip 1) { if ($element.Extent.EndOffset -lt $CursorPosition) { $tokens += Get-CurlTokenText -Element $element } } $tokens } function Get-CurlAtFileWord { # Reads an '@file' value under the cursor from the parsed command elements: # $wordToComplete loses the '@' when PowerShell splits a bare '@.\x' into # '@' and '.\x', and it keeps a typed quote. Returns $null for other words. param( [System.Management.Automation.Language.CommandAst]$CommandAst, [int]$CursorPosition ) $elements = @($CommandAst.CommandElements | Select-Object -Skip 1) for ($index = 0; $index -lt $elements.Count; $index++) { $extent = $elements[$index].Extent if ($CursorPosition -le $extent.StartOffset -or $CursorPosition -gt $extent.EndOffset) { continue } $text = $extent.Text.Substring(0, $CursorPosition - $extent.StartOffset) $quote = '' $splitAt = $false if ($text -match '^[''"]') { $quote = $text.Substring(0, 1) $text = $text.Substring(1) if ($text.EndsWith($quote)) { $text = $text.Substring(0, $text.Length - 1) } $text = if ($quote -eq "'") { $text.Replace("''", "'") } else { $text -replace '`(.)', '$1' } } elseif ($index -gt 0 -and $elements[$index - 1].Extent.Text -eq '@' -and $elements[$index - 1].Extent.EndOffset -eq $extent.StartOffset) { $text = '@' + $text $splitAt = $true } else { $text = $text -replace '`(.)', '$1' } if (-not $text.StartsWith('@')) { return $null } return [pscustomobject]@{ Value = $text; Quote = $quote; SplitAt = $splitAt } } $null } function Get-CurlValueKind { param( [string]$Token, [string]$Placeholder ) $placeholderKey = if ([string]::IsNullOrWhiteSpace($Placeholder)) { '' } else { $Placeholder.ToLowerInvariant() } switch -CaseSensitive ($Token) { '-h' { return 'HelpSubject' } '--help' { return 'HelpSubject' } '--proto' { return 'ProtocolList' } '--proto-redir' { return 'ProtocolList' } '--proto-default' { return 'Protocol' } '--proxy' { return 'ProxyUrl' } '--preproxy' { return 'ProxyUrl' } '--proxy1.0' { return 'ProxyUrl' } '--url' { return 'Url' } '--doh-url' { return 'Url' } '--ipfs-gateway' { return 'Url' } '--cert-type' { return 'CertType' } '--proxy-cert-type' { return 'CertType' } '--key-type' { return 'KeyType' } '--proxy-key-type' { return 'KeyType' } '--delegation' { return 'Delegation' } '--ftp-method' { return 'FtpMethod' } '--ftp-ssl-ccc-mode' { return 'FtpSslCccMode' } '--tlsauthtype' { return 'TlsAuthType' } '--proxy-tlsauthtype' { return 'TlsAuthType' } '--tls-max' { return 'TlsVersion' } '--create-file-mode' { return 'FileMode' } '--upload-flags' { return 'UploadFlags' } '--krb' { return 'KerberosLevel' } '--variable' { return 'VariableSpec' } '-d' { return 'DataValue' } '--data' { return 'DataValue' } '--data-ascii' { return 'DataValue' } '--data-binary' { return 'DataValue' } '-H' { return 'HeaderOrFile' } '--header' { return 'HeaderOrFile' } '--proxy-header' { return 'HeaderOrFile' } '-b' { return 'CookieValue' } '--cookie' { return 'CookieValue' } '-E' { return 'CertificatePath' } '--cert' { return 'CertificatePath' } '--proxy-cert' { return 'CertificatePath' } } switch ($placeholderKey) { 'subject' { return 'HelpSubject' } 'protocols' { return 'ProtocolList' } 'protocol' { return 'Protocol' } 'url' { return 'Url' } 'url/file' { return 'Url' } '[protocol://]host[:port]' { return 'ProxyUrl' } 'header/@file' { return 'HeaderOrFile' } '[%]name=text/@file' { return 'VariableSpec' } 'data|filename' { return 'DataOrFilename' } 'file' { return 'FilePath' } 'filename' { return 'FilePath' } 'key' { return 'FilePath' } 'dir' { return 'DirectoryPath' } 'path' { return 'Path' } 'certificate[:password]' { return 'CertificatePath' } 'cert[:passwd]' { return 'CertificatePath' } 'type' { return 'Text' } 'version' { return 'TlsVersion' } 'seconds' { return 'Number' } 'ms' { return 'Number' } 'num' { return 'Number' } 'integer' { return 'Number' } 'bytes' { return 'Number' } 'offset' { return 'Number' } 'priority' { return 'Number' } 'value' { return 'Number' } 'speed' { return 'Number' } } if ($placeholderKey) { return 'Text' } $null } function Get-CurlOptionSpecByToken { param([string]$Token) if ([string]::IsNullOrWhiteSpace($Token)) { return $null } $catalog = Get-CurlCompletionCatalog if ($catalog.OptionByToken.ContainsKey($Token)) { return $catalog.OptionByToken[$Token] } # curl spells every long option in lower case, so a long option may be matched case-insensitively. if ($Token.StartsWith('--')) { $lower = $Token.ToLowerInvariant() if ($catalog.OptionByToken.ContainsKey($lower)) { return $catalog.OptionByToken[$lower] } } $null } function Add-CurlOptionSpec { param( [string]$Token, [string]$Description, [string]$ValuePlaceholder ) if ([string]::IsNullOrWhiteSpace($Token)) { return } $catalog = Get-CurlCompletionCatalog $key = $Token if ($catalog.OptionByToken.ContainsKey($key)) { return } $displayText = if ([string]::IsNullOrWhiteSpace($ValuePlaceholder)) { $Token } else { "$Token <$ValuePlaceholder>" } $catalog.OptionByToken[$key] = [pscustomobject]@{ Token = $Token DisplayText = $displayText Description = $Description ValuePlaceholder = $ValuePlaceholder ValueKind = Get-CurlValueKind -Token $Token -Placeholder $ValuePlaceholder } $catalog.Options += $catalog.OptionByToken[$key] } function Initialize-CurlCompletionCatalog { $catalog = Get-CurlCompletionCatalog if ($catalog.Initialized) { return } $categoryLines = Invoke-CurlCapture -Arguments @('--help', 'category') $subjects = foreach ($line in @($categoryLines)) { if ($line -match '^\s*([a-z][a-z0-9-]+)\s{2,}') { $matches[1] } } $catalog.HelpSubjects = if ($subjects) { Get-CurlUniqueStrings -Items (@($subjects) + @(Get-CurlDefaultHelpSubjects)) } else { Get-CurlDefaultHelpSubjects } $versionLines = Invoke-CurlCapture -Arguments @('--version') $protocolLine = $versionLines | Where-Object { $_ -like 'Protocols:*' } | Select-Object -First 1 if ($protocolLine -and $protocolLine -match '^Protocols:\s+(.+)$') { $catalog.Protocols = Get-CurlUniqueStrings -Items ($matches[1] -split '\s+') } $helpLines = Invoke-CurlCapture -Arguments @('--help', 'all') foreach ($line in @($helpLines)) { if ($line -match '^\s*(?:(-\S+),\s+)?(--[A-Za-z0-9][A-Za-z0-9.\-]*)(?:\s+<([^>]+)>)?\s{2,}(.*)$') { $shortToken = $matches[1] $longToken = $matches[2] $valuePlaceholder = $matches[3] $description = $matches[4].Trim() if ($shortToken) { Add-CurlOptionSpec -Token $shortToken -Description $description -ValuePlaceholder $valuePlaceholder } Add-CurlOptionSpec -Token $longToken -Description $description -ValuePlaceholder $valuePlaceholder } } $catalog.Initialized = $true } function Get-CurlPathCompletions { param( [string]$InputPath, [string]$Prefix = '', [switch]$DirectoriesOnly, [string]$Quote = '' ) $cleanInput = if ([string]::IsNullOrWhiteSpace($InputPath)) { '' } else { $InputPath.Trim('"') } $alwaysQuote = -not [string]::IsNullOrEmpty($InputPath) -and $InputPath.StartsWith('"') if ([string]::IsNullOrWhiteSpace($cleanInput)) { $parent = '.' $leaf = '' } elseif ($cleanInput.EndsWith('\') -or $cleanInput.EndsWith('/')) { $parent = $cleanInput $leaf = '' } else { $parent = Split-Path -Path $cleanInput -Parent if ([string]::IsNullOrWhiteSpace($parent)) { $parent = '.' } $leaf = Split-Path -Path $cleanInput -Leaf } $filter = if ([string]::IsNullOrWhiteSpace($leaf)) { '*' } else { "$leaf*" } $items = @(Get-ChildItem -Path $parent -Filter $filter -ErrorAction Ignore) if ($DirectoriesOnly) { $items = @($items | Where-Object { $_.PSIsContainer }) } foreach ($item in $items) { $completionText = if ($cleanInput -and -not [System.IO.Path]::IsPathRooted($cleanInput)) { 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 ($Quote) { # The prefix and path form one argument quoted as a whole. $completionText = ConvertTo-CurlQuotedArgument -Value ($Prefix + $completionText) -Quote $Quote } else { $completionText = ConvertTo-CurlQuotedValue -Value $completionText -AlwaysQuote $alwaysQuote $completionText = $Prefix + $completionText } New-CurlCompletionResult -CompletionText $completionText -ListItemText $item.Name -ResultType 'ParameterValue' -ToolTip $item.FullName } } function New-CurlLiteralValueResults { param( [string]$CurrentValue, [string]$Placeholder, [string]$ToolTip, [string]$Prefix = '' ) if ([string]::IsNullOrWhiteSpace($CurrentValue)) { return @( New-CurlCompletionResult -CompletionText ($Prefix + $Placeholder) -ListItemText $Placeholder -ResultType 'ParameterValue' -ToolTip $ToolTip ) } @( New-CurlCompletionResult -CompletionText ($Prefix + $CurrentValue) -ListItemText $CurrentValue -ResultType 'ParameterValue' -ToolTip $ToolTip ) } function Get-CurlEnumValueResults { param( [string[]]$Values, [string]$CurrentValue, [string]$ToolTip, [string]$Prefix = '' ) $typedValue = if ($null -eq $CurrentValue) { '' } else { $CurrentValue } foreach ($value in @($Values)) { # An empty value list (curl absent, so no protocols) binds as a single $null. if ([string]::IsNullOrEmpty($value)) { continue } if ($value -like ([System.Management.Automation.WildcardPattern]::Escape($typedValue) + '*')) { New-CurlCompletionResult -CompletionText ($Prefix + $value) -ResultType 'ParameterValue' -ToolTip $ToolTip } } } function Get-CurlEnumOrLiteralValueResults { param( [string[]]$Values, [string]$CurrentValue, [string]$Placeholder, [string]$ToolTip, [string]$Prefix = '' ) $results = @(Get-CurlEnumValueResults -Values $Values -CurrentValue $CurrentValue -ToolTip $ToolTip -Prefix $Prefix) if ($results.Count -gt 0) { return $results } New-CurlLiteralValueResults -CurrentValue $CurrentValue -Placeholder $Placeholder -ToolTip $ToolTip -Prefix $Prefix } function Get-CurlPathValueResults { param( [string]$CurrentValue, [string]$Placeholder, [string]$ToolTip, [string]$Prefix = '', [switch]$DirectoriesOnly ) $results = @(Get-CurlPathCompletions -InputPath $CurrentValue -Prefix $Prefix -DirectoriesOnly:$DirectoriesOnly) if ($results.Count -gt 0) { return $results } New-CurlLiteralValueResults -CurrentValue $CurrentValue -Placeholder $Placeholder -ToolTip $ToolTip -Prefix $Prefix } function Get-CurlUrlValueResults { param( [string]$CurrentValue, [string]$Placeholder, [string]$ToolTip, [string]$Prefix = '', [switch]$Proxy ) $catalog = Get-CurlCompletionCatalog $typedValue = if ($null -eq $CurrentValue) { '' } else { $CurrentValue } $values = if ($Proxy) { @('http://', 'https://', 'socks4://', 'socks4a://', 'socks5://', 'socks5h://') } else { @($catalog.Protocols | ForEach-Object { "${_}://" }) } $results = @(Get-CurlEnumValueResults -Values (Get-CurlUniqueStrings -Items $values) -CurrentValue $typedValue -ToolTip $ToolTip -Prefix $Prefix) if ($results.Count -gt 0) { return $results } New-CurlLiteralValueResults -CurrentValue $typedValue -Placeholder $Placeholder -ToolTip $ToolTip -Prefix $Prefix } function Get-CurlProtocolListValueResults { param( [string]$CurrentValue, [string]$ToolTip, [string]$Prefix = '' ) $catalog = Get-CurlCompletionCatalog $typedValue = if ($null -eq $CurrentValue) { '' } else { $CurrentValue } $basePrefix = '' $segment = $typedValue if ($typedValue -match '^(.*?,)([^,]*)$') { $basePrefix = $matches[1] $segment = $matches[2] } $modifier = '' $namePrefix = $segment if ($segment -match '^([+=-])(.*)$') { $modifier = $matches[1] $namePrefix = $matches[2] } $results = New-Object System.Collections.Generic.List[object] foreach ($value in @('all') + @($catalog.Protocols)) { if ($value -notlike "$namePrefix*") { continue } $completionText = $Prefix + $basePrefix + $modifier + $value [void]$results.Add((New-CurlCompletionResult -CompletionText $completionText -ResultType 'ParameterValue' -ToolTip $ToolTip)) } if ($results.Count -gt 0) { return @($results.ToArray()) } New-CurlLiteralValueResults -CurrentValue $typedValue -Placeholder '<protocols>' -ToolTip $ToolTip -Prefix $Prefix } function Get-CurlUploadFlagValueResults { param( [string]$CurrentValue, [string]$ToolTip, [string]$Prefix = '' ) $typedValue = if ($null -eq $CurrentValue) { '' } else { $CurrentValue } if ($typedValue -match '^[''"]') { return New-CurlLiteralValueResults -CurrentValue $typedValue -Placeholder '<flags>' -ToolTip $ToolTip -Prefix $Prefix } $segment = $typedValue if ($typedValue -match ',([^,]*)$') { # PowerShell parses 'a,b' as an array and replaces only the segment after the last comma. $segment = $matches[1] $Prefix = '' } # curl negates a flag with a leading '-'; flag names are case-sensitive. $modifier = '' $namePrefix = $segment if ($segment.StartsWith('-')) { $modifier = '-' $namePrefix = $segment.Substring(1) } $results = @(foreach ($value in @('answered', 'deleted', 'draft', 'flagged', 'seen')) { if ($value.StartsWith($namePrefix, [System.StringComparison]::Ordinal)) { New-CurlCompletionResult -CompletionText ($Prefix + $modifier + $value) -ResultType 'ParameterValue' -ToolTip $ToolTip } }) if ($results.Count -gt 0) { return $results } New-CurlLiteralValueResults -CurrentValue $segment -Placeholder '<flags>' -ToolTip $ToolTip -Prefix $Prefix } function Get-CurlVariableValueResults { param( [string]$CurrentValue, [string]$ToolTip, [string]$Prefix = '' ) $typedValue = if ($null -eq $CurrentValue) { '' } else { $CurrentValue } if ($typedValue -match '^(.*@)([^@]*)$') { $variablePrefix = $matches[1] $pathPrefix = $matches[2] $results = @(Get-CurlPathCompletions -InputPath $pathPrefix -Prefix ($Prefix + $variablePrefix)) if ($results.Count -gt 0) { return $results } } $results = New-Object System.Collections.Generic.List[object] if ([string]::IsNullOrWhiteSpace($typedValue) -or '%' -like ([System.Management.Automation.WildcardPattern]::Escape($typedValue) + '*')) { foreach ($envName in (Get-ChildItem Env: | Select-Object -ExpandProperty Name | Sort-Object -Unique)) { $candidate = "%$envName" if ($candidate -like ([System.Management.Automation.WildcardPattern]::Escape($typedValue) + '*')) { [void]$results.Add((New-CurlCompletionResult -CompletionText ($Prefix + $candidate) -ResultType 'ParameterValue' -ToolTip 'Import environment variable into a curl variable.')) } } } foreach ($item in @(New-CurlLiteralValueResults -CurrentValue $typedValue -Placeholder 'name=text' -ToolTip $ToolTip -Prefix $Prefix)) { [void]$results.Add($item) } if ([string]::IsNullOrWhiteSpace($typedValue) -or 'name@file' -like ([System.Management.Automation.WildcardPattern]::Escape($typedValue) + '*')) { [void]$results.Add((New-CurlCompletionResult -CompletionText ($Prefix + 'name@file') -ResultType 'ParameterValue' -ToolTip 'Set a curl variable from a file.')) } @($results.ToArray()) } function Get-CurlAtFileValueResults { param( [string]$CurrentValue, [string]$Placeholder, [string]$ToolTip, [string]$Prefix = '', [string]$Quote = '', [switch]$SplitAt ) $typedValue = if ($null -eq $CurrentValue) { '' } else { $CurrentValue } if ($typedValue.StartsWith('@')) { $pathInput = $typedValue.Substring(1) $results = @(if ($SplitAt) { # The '@' already sits in the buffer as a token of its own; complete the path after it. Get-CurlPathCompletions -InputPath $pathInput } elseif ($Prefix) { Get-CurlPathCompletions -InputPath $pathInput -Prefix ($Prefix + '@') } else { # A leading '@' would start a splat, so the whole value is emitted quoted. $valueQuote = if ($Quote) { $Quote } else { "'" } Get-CurlPathCompletions -InputPath $pathInput -Prefix '@' -Quote $valueQuote }) if ($results.Count -gt 0) { return $results } if ($SplitAt) { return New-CurlLiteralValueResults -CurrentValue $pathInput -Placeholder $Placeholder -ToolTip $ToolTip } if (-not $Prefix) { return @(New-CurlCompletionResult -CompletionText (ConvertTo-CurlQuotedArgument -Value $typedValue -Quote $valueQuote) -ListItemText $typedValue -ResultType 'ParameterValue' -ToolTip $ToolTip) } } $results = New-Object System.Collections.Generic.List[object] if ([string]::IsNullOrWhiteSpace($typedValue) -or '@' -like ([System.Management.Automation.WildcardPattern]::Escape($typedValue) + '*')) { [void]$results.Add((New-CurlCompletionResult -CompletionText ($Prefix + '@') -ResultType 'ParameterValue' -ToolTip 'Prefix a file path with @ to load content from disk.')) } foreach ($item in @(New-CurlLiteralValueResults -CurrentValue $typedValue -Placeholder $Placeholder -ToolTip $ToolTip -Prefix $Prefix)) { [void]$results.Add($item) } @($results.ToArray()) } function Get-CurlValueCompletions { param( [pscustomobject]$OptionSpec, [string]$CurrentValue, [string]$Prefix = '', [string]$Quote = '', [switch]$SplitAt ) $typedValue = if ($null -eq $CurrentValue) { '' } else { $CurrentValue } $toolTip = if ([string]::IsNullOrWhiteSpace($OptionSpec.Description)) { $OptionSpec.DisplayText } else { $OptionSpec.Description } switch ($OptionSpec.ValueKind) { 'HelpSubject' { return @(Get-CurlEnumOrLiteralValueResults -Values (Get-CurlCompletionCatalog).HelpSubjects -CurrentValue $typedValue -Placeholder '<subject>' -ToolTip $toolTip -Prefix $Prefix) } 'Protocol' { return @(Get-CurlEnumOrLiteralValueResults -Values ((Get-CurlUniqueStrings -Items (@('all') + @((Get-CurlCompletionCatalog).Protocols)))) -CurrentValue $typedValue -Placeholder '<protocol>' -ToolTip $toolTip -Prefix $Prefix) } 'ProtocolList' { return @(Get-CurlProtocolListValueResults -CurrentValue $typedValue -ToolTip $toolTip -Prefix $Prefix) } 'Url' { return @(Get-CurlUrlValueResults -CurrentValue $typedValue -Placeholder '<url>' -ToolTip $toolTip -Prefix $Prefix) } 'ProxyUrl' { return @(Get-CurlUrlValueResults -CurrentValue $typedValue -Placeholder '<proxy-url>' -ToolTip $toolTip -Prefix $Prefix -Proxy) } 'DirectoryPath' { return @(Get-CurlPathValueResults -CurrentValue $typedValue -Placeholder '<dir>' -ToolTip $toolTip -Prefix $Prefix -DirectoriesOnly) } 'FilePath' { return @(Get-CurlPathValueResults -CurrentValue $typedValue -Placeholder '<file>' -ToolTip $toolTip -Prefix $Prefix) } 'Path' { return @(Get-CurlPathValueResults -CurrentValue $typedValue -Placeholder '<path>' -ToolTip $toolTip -Prefix $Prefix) } 'CertificatePath' { if (Test-CurlPathLikeInput -Value $typedValue) { return @(Get-CurlPathValueResults -CurrentValue $typedValue -Placeholder '<certificate>' -ToolTip $toolTip -Prefix $Prefix) } if ($typedValue -match '^([^:]+):(.*)$') { return New-CurlLiteralValueResults -CurrentValue $typedValue -Placeholder '<certificate[:password]>' -ToolTip $toolTip -Prefix $Prefix } return @(Get-CurlPathValueResults -CurrentValue $typedValue -Placeholder '<certificate>' -ToolTip $toolTip -Prefix $Prefix) } 'HeaderOrFile' { return @(Get-CurlAtFileValueResults -CurrentValue $typedValue -Placeholder '<header-or-@file>' -ToolTip $toolTip -Prefix $Prefix -Quote $Quote -SplitAt:$SplitAt) } 'VariableSpec' { return @(Get-CurlVariableValueResults -CurrentValue $typedValue -ToolTip $toolTip -Prefix $Prefix) } 'DataValue' { return @(Get-CurlAtFileValueResults -CurrentValue $typedValue -Placeholder '<data-or-@file>' -ToolTip $toolTip -Prefix $Prefix -Quote $Quote -SplitAt:$SplitAt) } 'DataOrFilename' { if (Test-CurlPathLikeInput -Value $typedValue) { return @(Get-CurlPathValueResults -CurrentValue $typedValue -Placeholder '<data-or-file>' -ToolTip $toolTip -Prefix $Prefix) } return New-CurlLiteralValueResults -CurrentValue $typedValue -Placeholder '<data-or-file>' -ToolTip $toolTip -Prefix $Prefix } 'CookieValue' { if (Test-CurlPathLikeInput -Value $typedValue) { return @(Get-CurlPathValueResults -CurrentValue $typedValue -Placeholder '<cookie-data-or-file>' -ToolTip $toolTip -Prefix $Prefix) } return New-CurlLiteralValueResults -CurrentValue $typedValue -Placeholder '<cookie-data-or-file>' -ToolTip $toolTip -Prefix $Prefix } 'CertType' { return @(Get-CurlEnumOrLiteralValueResults -Values @('DER', 'PEM', 'ENG', 'PROV', 'P12') -CurrentValue $typedValue -Placeholder '<type>' -ToolTip $toolTip -Prefix $Prefix) } 'KeyType' { return @(Get-CurlEnumOrLiteralValueResults -Values @('DER', 'PEM', 'ENG') -CurrentValue $typedValue -Placeholder '<type>' -ToolTip $toolTip -Prefix $Prefix) } 'Delegation' { return @(Get-CurlEnumOrLiteralValueResults -Values @('none', 'policy', 'always') -CurrentValue $typedValue -Placeholder '<level>' -ToolTip $toolTip -Prefix $Prefix) } 'FtpMethod' { return @(Get-CurlEnumOrLiteralValueResults -Values @('multicwd', 'nocwd', 'singlecwd') -CurrentValue $typedValue -Placeholder '<method>' -ToolTip $toolTip -Prefix $Prefix) } 'FtpSslCccMode' { return @(Get-CurlEnumOrLiteralValueResults -Values @('active', 'passive') -CurrentValue $typedValue -Placeholder '<mode>' -ToolTip $toolTip -Prefix $Prefix) } 'TlsAuthType' { return @(Get-CurlEnumOrLiteralValueResults -Values @('SRP') -CurrentValue $typedValue -Placeholder '<type>' -ToolTip $toolTip -Prefix $Prefix) } 'TlsVersion' { return @(Get-CurlEnumOrLiteralValueResults -Values @('default', '1.0', '1.1', '1.2', '1.3') -CurrentValue $typedValue -Placeholder '<VERSION>' -ToolTip $toolTip -Prefix $Prefix) } 'UploadFlags' { return @(Get-CurlUploadFlagValueResults -CurrentValue $typedValue -ToolTip $toolTip -Prefix $Prefix) } 'KerberosLevel' { return @(Get-CurlEnumOrLiteralValueResults -Values @('clear', 'safe', 'confidential', 'private') -CurrentValue $typedValue -Placeholder '<level>' -ToolTip $toolTip -Prefix $Prefix) } 'FileMode' { return @(Get-CurlEnumOrLiteralValueResults -Values @('0600', '0644', '0660', '0755') -CurrentValue $typedValue -Placeholder '<mode>' -ToolTip $toolTip -Prefix $Prefix) } 'Number' { return @(Get-CurlEnumOrLiteralValueResults -Values @('0', '1', '5', '10', '30', '60', '300') -CurrentValue $typedValue -Placeholder '<number>' -ToolTip $toolTip -Prefix $Prefix) } default { return New-CurlLiteralValueResults -CurrentValue $typedValue -Placeholder ('<' + $OptionSpec.ValuePlaceholder + '>') -ToolTip $toolTip -Prefix $Prefix } } } function Get-CurlPendingOption { param([string[]]$TokensBeforeCurrent) Initialize-CurlCompletionCatalog $pendingOption = $null foreach ($token in @($TokensBeforeCurrent)) { $cleanToken = Remove-CurlOuterQuotes -Value $token if ([string]::IsNullOrWhiteSpace($cleanToken)) { continue } if ($pendingOption) { $pendingOption = $null continue } if ($cleanToken -match '^(--[^=]+)=') { continue } $optionSpec = Get-CurlOptionSpecByToken -Token $cleanToken if ($optionSpec -and $optionSpec.ValueKind) { $pendingOption = $optionSpec } } $pendingOption } function Get-CurlOptionCompletions { param([string]$CurrentWord) Initialize-CurlCompletionCatalog $catalog = Get-CurlCompletionCatalog $cleanCurrent = Remove-CurlOuterQuotes -Value $CurrentWord # Short options are case-significant (-s and -S are different options), so they must match ordinally. $ordinalMatch = $cleanCurrent -match '^-[^-]' foreach ($option in $catalog.Options) { $isMatch = if ($ordinalMatch) { $option.Token.StartsWith($cleanCurrent, [System.StringComparison]::Ordinal) } else { $option.Token -like ([System.Management.Automation.WildcardPattern]::Escape($cleanCurrent) + '*') } if ($isMatch) { $toolTip = if ([string]::IsNullOrWhiteSpace($option.Description)) { $option.DisplayText } else { $option.Description } New-CurlCompletionResult -CompletionText $option.Token -ListItemText $option.DisplayText -ResultType 'ParameterName' -ToolTip $toolTip } } } function Get-CurlPositionalCompletions { param([string]$CurrentWord) Initialize-CurlCompletionCatalog Get-CurlUrlValueResults -CurrentValue $CurrentWord -Placeholder '<url>' -ToolTip 'URL to transfer.' } Register-ArgumentCompleter -Native -CommandName 'curl', 'curl.exe' -ScriptBlock { param($wordToComplete, $commandAst, $cursorPosition) if ($wordToComplete -isnot [string]) { $wordToComplete = [string]$wordToComplete } Initialize-CurlCompletionCatalog $currentToken = Get-CurlCurrentToken -Line $commandAst.Extent.Text -CursorPosition ($cursorPosition - $commandAst.Extent.StartOffset) -Fallback $wordToComplete $tokensBeforeCurrent = Get-CurlArgumentTokens -CommandAst $commandAst -CursorPosition $cursorPosition if ($currentToken -match '^(--[^=]+)=(.*)$') { $optionName = $matches[1] $valuePrefix = $matches[2] $optionSpec = Get-CurlOptionSpecByToken -Token $optionName if ($optionSpec -and $optionSpec.ValueKind) { return @(Get-CurlValueCompletions -OptionSpec $optionSpec -CurrentValue $valuePrefix -Prefix ($optionName + '=')) } } $atWord = Get-CurlAtFileWord -CommandAst $commandAst -CursorPosition $cursorPosition if ($atWord) { # A split-off '@' is part of the value being typed, not a value of its own. $atTokens = if ($atWord.SplitAt) { @($tokensBeforeCurrent | Select-Object -First (@($tokensBeforeCurrent).Count - 1)) } else { $tokensBeforeCurrent } $atOption = Get-CurlPendingOption -TokensBeforeCurrent $atTokens if ($atOption -and $atOption.ValueKind -in 'DataValue', 'HeaderOrFile') { return @(Get-CurlValueCompletions -OptionSpec $atOption -CurrentValue $atWord.Value -Quote $atWord.Quote -SplitAt:$atWord.SplitAt) } } $pendingOption = Get-CurlPendingOption -TokensBeforeCurrent $tokensBeforeCurrent if ($pendingOption) { return @(Get-CurlValueCompletions -OptionSpec $pendingOption -CurrentValue $wordToComplete) } $results = New-Object System.Collections.Generic.List[System.Management.Automation.CompletionResult] if ([string]::IsNullOrWhiteSpace($currentToken) -or $currentToken.StartsWith('-')) { foreach ($result in @(Get-CurlOptionCompletions -CurrentWord $wordToComplete)) { [void]$results.Add($result) } } if (-not $currentToken.StartsWith('-')) { foreach ($result in @(Get-CurlPositionalCompletions -CurrentWord $wordToComplete)) { [void]$results.Add($result) } } @($results.ToArray()) } |