Say where a command line's quote is left open and how a quote is written (hq issue 294)
An apostrophe inside a single-quoted value ends that quote, and the line then failed as a bare 'unclosed quote' with no position and no way out named. The splitter's rules stay the shell's; the refusal now names the character, shows the line from there and gives the two ways to write a quote.
This commit is contained in:
@@ -415,3 +415,45 @@ func TestNoVerbSetsTheOperatorsKeyOrRevealsASecret(t *testing.T) {
|
||||
t.Fatalf("behind %v: %v", behind, err)
|
||||
}
|
||||
}
|
||||
|
||||
// A value with a quote in it can be said on a `command` line, as in a shell, and a line whose quote
|
||||
// is left open is refused naming where it opened and how a quote is written (novox/hq issue 294: an
|
||||
// apostrophe inside a single-quoted JSON value cut the line, and the refusal named no cause).
|
||||
func TestAQuotedValueCanHoldAQuote(t *testing.T) {
|
||||
for _, c := range []struct{ line, want string }{
|
||||
{`settings set claude-code '{"role":"the operator'\''s laptop"}'`, `{"role":"the operator's laptop"}`},
|
||||
{`settings set claude-code "{\"role\":\"the operator's laptop\"}"`, `{"role":"the operator's laptop"}`},
|
||||
{"x \"a \\\\ b\nc\"", "a \\ b\nc"},
|
||||
{`x 'a\b'`, `a\b`},
|
||||
} {
|
||||
argv, err := splitCommandLine(c.line)
|
||||
if err != nil || argv[len(argv)-1] != c.want {
|
||||
t.Errorf("%s: read back %q %v, want %q", c.line, argv, err, c.want)
|
||||
}
|
||||
}
|
||||
|
||||
_, err := splitCommandLine(`settings set claude-code '{"role":"the operator's laptop"}' --node g14`)
|
||||
if err == nil {
|
||||
t.Fatal("an apostrophe that leaves a quote open was accepted")
|
||||
}
|
||||
for _, want := range []string{"character 57", `"}' --node g14`, `'\''`, `\"`} {
|
||||
if !strings.Contains(err.Error(), want) {
|
||||
t.Errorf("the refusal does not say %q: %v", want, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Every value reads back as it was when written the way the refusal says: single-quoted with '\”
|
||||
// for each quote, or double-quoted with \ before each " and \ — newlines and backslashes included.
|
||||
func TestAQuotedValueRoundTrips(t *testing.T) {
|
||||
for _, v := range []string{`plain`, `it's`, `say "hi"`, `back\slash\`, "two\nlines", `'"\'\"`, `''`, ``} {
|
||||
single := "x '" + strings.ReplaceAll(v, "'", `'\''`) + "'"
|
||||
double := `x "` + strings.NewReplacer(`\`, `\\`, `"`, `\"`).Replace(v) + `"`
|
||||
for _, line := range []string{single, double} {
|
||||
argv, err := splitCommandLine(line)
|
||||
if err != nil || len(argv) != 2 || argv[1] != v {
|
||||
t.Errorf("%s: read back %q %v, want %q", line, argv, err, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user