MM-71178 Allow " --" inside double quoted slash command values

Rejecting " --" inside any quoted value was how a missing closing quote
got caught, but it also rejected legitimate values such as
--header "use --foo here".

A double quoted value now ends at the next matching quote, and text
glued to a closing quote is an error, so a missing closing quote is
still reported. Single quotes double as apostrophes, so they keep the
whitespace rule and the " --" restriction, and the error message
suggests double quotes instead.
This commit is contained in:
wiggin77
2026-10-15 17:28:42 -04:00
parent 4ef71bde01
commit 797a8aa671
4 changed files with 25 additions and 8 deletions
@@ -112,7 +112,7 @@ func TestRemoteProviderDoRemove(t *testing.T) {
"empty quotes": {`/secure-connection remove --connectionID ""`, "Missing or empty `connectionID`"},
"malformed": {"/secure-connection remove --connectionID foo", "`foo` is not a valid `connectionID`"},
"stray word": {"/secure-connection remove --connectionID foo bar", "Unexpected `bar`. Wrap a value that contains spaces in quotes, and use double quotes if the value also contains an apostrophe."},
"open quote": {`/secure-connection remove --connectionID "foo`, "The quote at `\"foo` is never closed. A quoted value must end with a matching quote and cannot contain ` --`."},
"open quote": {`/secure-connection remove --connectionID "foo`, "The quote at `\"foo` is never closed. A value in single quotes cannot contain ` --`, so use double quotes for that."},
} {
t.Run(name, func(t *testing.T) {
callsBefore := wrapper.deleteCalls.Load()
+16 -3
View File
@@ -85,9 +85,8 @@ func (t commandToken) value() string {
var closingQuotes = map[rune]rune{'"': '"', '\'': '\'', '“': '”', '‘': '’'}
// tokenizeCommand splits cmd on whitespace, except inside a quoted value. A quote only opens
// a value at the start of a word and only closes it when followed by whitespace or the end
// of input, so apostrophes such as O'Brien are kept. A quoted value may not contain " --",
// so a missing closing quote is an error instead of swallowing the flags after it.
// a value at the start of a word, so apostrophes such as O'Brien are kept. A double quoted
// value ends at the next matching quote. See quotedToken for single quotes.
func tokenizeCommand(T i18n.TranslateFunc, cmd string) ([]commandToken, error) {
var tokens []commandToken
rest := strings.TrimLeftFunc(cmd, unicode.IsSpace)
@@ -99,6 +98,10 @@ func tokenizeCommand(T i18n.TranslateFunc, cmd string) ([]commandToken, error) {
word, _ := nextWord(rest)
return nil, errors.New(T("api.command.named_args.unterminated_quote", map[string]any{"Value": word}))
}
if next, _ := utf8.DecodeRuneInString(after); after != "" && !unicode.IsSpace(next) {
word, _ := nextWord(after)
return nil, errors.New(T("api.command.named_args.unexpected_value", map[string]any{"Value": word}))
}
tok, rest = quoted, after
} else {
tok.text, rest = nextWord(rest)
@@ -117,7 +120,17 @@ func nextWord(s string) (string, string) {
return s[:end], s[end:]
}
// A single quote doubles as an apostrophe, so a single quoted value only ends at a matching
// quote followed by whitespace or the end of input. It may not contain " --", so a missing
// closing quote is an error instead of swallowing the flags after it.
func quotedToken(s string, closeQuote rune) (commandToken, string, bool) {
if closeQuote == '"' || closeQuote == '”' {
end := strings.IndexRune(s, closeQuote)
if end < 0 {
return commandToken{}, "", false
}
return commandToken{text: s[:end], quoted: true}, s[end+utf8.RuneLen(closeQuote):], true
}
for i, r := range s {
after := s[i+utf8.RuneLen(r):]
if r == closeQuote {
@@ -41,7 +41,9 @@ func TestParseNamedArgs(t *testing.T) {
{"quoted spaces with embedded quote", "remote add --arg1 \"Bob O'Brien's remote\"", map[string]string{ActionKey: "add", "arg1": "Bob O'Brien's remote"}},
{"single quoted with embedded apostrophe", "remote add --arg1 'O'Brien'", map[string]string{ActionKey: "add", "arg1": "O'Brien"}},
{"quoted value that looks like a flag", "remote add --arg1 \"--arg2\"", map[string]string{ActionKey: "add", "arg1": "--arg2"}},
{"quote closes only before whitespace", "remote add --arg1 \"a\"b c\"", map[string]string{ActionKey: "add", "arg1": "a\"b c"}},
{"single quote closes only before whitespace", "remote add --arg1 'a'b c'", map[string]string{ActionKey: "add", "arg1": "a'b c"}},
{"double quoted value containing a flag", "remote add --arg1 \"use --arg2 here\" --arg3 val3", map[string]string{ActionKey: "add", "arg1": "use --arg2 here", "arg3": "val3"}},
{"curly double quoted value containing a flag", "remote add --arg1 \u201cuse --arg2 here\u201d", map[string]string{ActionKey: "add", "arg1": "use --arg2 here"}},
{"curly double quotes", "remote add --arg1 \u201cmy remote\u201d --arg2 val2", map[string]string{ActionKey: "add", "arg1": "my remote", "arg2": "val2"}},
{"curly single quotes with curly apostrophe", "remote add --arg1 \u2018Bob\u2019s remote\u2019", map[string]string{ActionKey: "add", "arg1": "Bob\u2019s remote"}},
}
@@ -68,7 +70,9 @@ func TestParseNamedArgsErrors(t *testing.T) {
{"apostrophe then space ends single quoted value", "remote add --arg1 'Engineers' Hub'", unexpectedValue},
{"unterminated quote at end", "remote add --arg1 \"my remote", unterminatedQuote},
{"unterminated quote before flag", "remote add --arg1 \"my remote --arg2 val2", unterminatedQuote},
{"unterminated quote before quoted value", "remote add --arg1 \"foo --arg2 \"Bar Baz\" --arg3 val3", unterminatedQuote},
{"unterminated quote before quoted value", "remote add --arg1 \"foo --arg2 \"Bar Baz\" --arg3 val3", unexpectedValue},
{"text after closing double quote", "remote add --arg1 \"a\"b c\"", unexpectedValue},
{"single quoted value containing a flag", "remote add --arg1 'use --arg2 here'", unterminatedQuote},
{"mismatched curly quotes", "remote add --arg1 \u201cmy remote\" --arg2 val2", unterminatedQuote},
}
+2 -2
View File
@@ -1161,8 +1161,8 @@
},
{
"id": "api.command.named_args.unterminated_quote",
"translation": "The quote at `{{.Value}}` is never closed. A quoted value must end with a matching quote and cannot contain ` --`.",
"description": "Slash command error when a quoted argument value has no closing quote before the next flag or the end of the command."
"translation": "The quote at `{{.Value}}` is never closed. A value in single quotes cannot contain ` --`, so use double quotes for that.",
"description": "Slash command error when a quoted argument value has no closing quote, or a single quoted value contains a flag."
},
{
"id": "api.command.team_mismatch.app_error",