Skip to content

Instantly share code, notes, and snippets.

@msftrncs
Last active November 5, 2019 07:28
Show Gist options
  • Select an option

  • Save msftrncs/bff8c6c5e28ff92a19efb8a5556a4238 to your computer and use it in GitHub Desktop.

Select an option

Save msftrncs/bff8c6c5e28ff92a19efb8a5556a4238 to your computer and use it in GitHub Desktop.
PowerShell (v6.2+) filter to quote arguments in order to use them in command line completions
using namespace System.Management.Automation.Language
function CmdRequiresQuote ([string]$in) {
[QuoteCheck]::CmdRequiresQuote($in, $false)
}
function CmdRequiresQuote ([string]$in, [bool]$IsExpandable = $false) {
[Token[]]$_tokens = $null
(_CommonRequiresQuote $in $IsExpandable ([ref]$_tokens)) -or
-not $IsExpandable -and ($_tokens[0].Kind -in (
[TokenKind]::Number, [TokenKind]::Semi) -or
$_tokens[0].TokenFlags -band ([TokenFlags]::UnaryOperator -bor [TokenFlags]::Keyword))
}
function ArgRequiresQuote ([string]$in) {
[Token[]]$_tokens = $null
(_CommonRequiresQuote $in $true ([ref]$_tokens)) -or $_tokens[1].Kind -in (
[TokenKind]::Redirection,
[TokenKind]::RedirectInStd,
[TokenKind]::Parameter)
}
function _CommonRequiresQuote([string]$in, [bool]$IsExpandable, [ref]$_tokens_ref) {
[ParseError[]]$_parseerrors = $null
$tokenToCheck = if ($IsExpandable) { 1 } else { 0 }
[Parser]::ParseInput("$(if ($IsExpandable) {'&'})$in", $_tokens_ref, [ref]$_parseerrors) | Out-Null
$_tokens = $_tokens_ref.Value
$_parseerrors.Length -ne 0 -or $_tokens.Length -ne ($tokenToCheck + 2) -or $_tokens[$tokenToCheck].Kind -in (
[TokenKind]::Variable,
[TokenKind]::SplattedVariable,
[TokenKind]::StringExpandable,
[TokenKind]::StringLiteral,
[TokenKind]::HereStringExpandable,
[TokenKind]::HereStringLiteral,
[TokenKind]::Comment) -or ($IsExpandable -and $_tokens[1] -is [StringExpandableToken]) -or
($_tokens[$tokenToCheck] -is [StringToken] -and $_tokens[$tokenToCheck].Value.Length -ne $in.Length) -or
$_tokens[$tokenToCheck + 1].Kind -ne [TokenKind]::EndOfInput
}
filter quoteArgWithSpecChars {
param(
[ValidateSet([char]0us, [char]34us, [char]39us, [char]0x2018us, [char]0x2019us, [char]0x201Aus, [char]0x201Bus, [char]0x201Cus, [char]0x201Dus, [char]0x201Eus)]
[char]$QuotedWith = [char]0us, # specifies quote character argument was previously quoted with
[bool]$IsLiteralPath = $true, # specifies argument is a literal and needs no wildcard escaping
[string]$PrefixText = '' # portion of argument that has already been completed, in its raw (unescaped) form
)
# filter a list of potential command argument completions, altering them for compatibility with PowerShell's tokenizer
# return a hash table of the original item (ListItemText) and the completion text that would be inserted
# this resembles the System.Management.Automation.CompletionResult class
[pscustomobject]@{
ListItemText = $_
CompletionText = "$($(
# first, force to a literal if argument isn't automatically literal
if (-not $IsLiteralPath) {
# must escape certain wildcard patterns
# kludge, WildcardPattern.Escape doesn't escape the escape character
[WildcardPattern]::Escape("$PrefixText$_".Replace('`','``'))
} else {
"$PrefixText$_"
}
).foreach{
# escape according to type of quoting completion will use
if ($QuotedWith -eq [char]0us) {
# bareword, check if completion must be forced to be quoted
if ($_ -match '^(?:[@#<>]|[1-6]>|[-\u2013-\u2015](?:[-\u2013-\u2015]$|[_\p{L}]))|[\s`|&;,''"\u2018-\u201E{}()]|\$[{(\w:$^?]') { #)
# needs to be single-quoted
"'$($_ -replace '[''\u2018-\u201B]', '$0$0')'"
} else {
# is fine as is
$_
}
} elseif ($QuotedWith -notin [char]34us, [char]0x201Cus, [char]0x201Dus, [char]0x201Eus) {
# single-quoted
"$QuotedWith$($_ -replace '[''\u2018-\u201B]', '$0$0')$QuotedWith"
} else {
# double-quoted
"$QuotedWith$($_ -replace '["\u201C-\u201E`]', '$0$0' -replace '\$(?=[{(\w:$^?])'<#)#>, '`$0')$QuotedWith"
}
})"
}
# see https://github.com/PowerShell/PowerShell/issues/4543 regarding the commented `)`, they are neccessary.
}
@msftrncs
Copy link
Author

msftrncs commented Aug 1, 2019

Revised again to kludge [WildCardPattern]::Escape() which fails to escape the escape character.

@msftrncs
Copy link
Author

Added PSRequiresQuotes to demonstrate a method that uses the PowerShell parser and evaluates its results to determine if quoting is needed. This better supports command name quoting.

@msftrncs
Copy link
Author

msftrncs commented Nov 5, 2019

Corrected PSRequiresQuotes, TokenKind does not include a Keyword, use TokenFlags instead.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment