Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion fish.go
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,15 @@ func commandAncestry(command *Command) string {
return strings.Join(ancestry, "; and ")
}

// escapeSingleQuotes escapes a string for use inside a fish single-quoted
// string, such as a `-d '...'` description. Within single quotes fish only
// recognizes the escape sequences \\ and \', so the backslash must be escaped
// as well as the single quote. Escaping the quote without escaping the
// backslash corrupts any description that contains a backslash, and a trailing
// backslash even leaves the single-quoted string unterminated.
// See https://fishshell.com/docs/current/language.html
func escapeSingleQuotes(input string) string {
return strings.ReplaceAll(input, `'`, `\'`)
return fishSingleQuoteReplacer.Replace(input)
}

var fishSingleQuoteReplacer = strings.NewReplacer(`\`, `\\`, `'`, `\'`)
32 changes: 32 additions & 0 deletions fish_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,38 @@ func TestFishCompletion(t *testing.T) {
expectFileContent(t, "testdata/expected-fish-full.fish", res)
}

func TestFishCompletionBackslashEscaping(t *testing.T) {
// Inside fish single-quoted strings the only escape sequences are \\ and
// \', so a backslash in a description must be emitted as \\. An unescaped
// backslash silently corrupts the description, and a trailing one turns the
// closing quote into an escaped quote, leaving the string unterminated.
// Ref: https://fishshell.com/docs/current/language.html
cmd := &Command{
Name: "greet",
Flags: []Flag{
&StringFlag{
Name: "path",
Usage: `match \d+ then C:\tmp\`,
},
},
Commands: []*Command{
{
Name: "win",
Usage: `run under C:\sys\`,
},
},
}
cmd.setupCommandGraph()

res, err := cmd.ToFishCompletion()
require.NoError(t, err)

// Both the flag and the subcommand descriptions must have their backslashes
// doubled in the generated `-d '...'` tokens.
assert.Contains(t, res, `-d 'match \\d+ then C:\\tmp\\'`)
assert.Contains(t, res, `-d 'run under C:\\sys\\'`)
}

func TestFishCompletionShellComplete(t *testing.T) {
cmd := buildExtendedTestCommand()
cmd.ShellComplete = func(context.Context, *Command) {}
Expand Down