From 7506b11da746beef287831f805f5b0e49264b400 Mon Sep 17 00:00:00 2001 From: Sascha Grunert Date: Thu, 8 Aug 2019 15:50:36 +0200 Subject: [PATCH] Add fish shell completion support This commit adds a new method `ToFishCompletion` to the `*App` which can be used to generate a fish completion string for the application. Relates to: #351 Signed-off-by: Sascha Grunert --- fish.go | 171 +++++++++++++++++++++++++++++++ fish_test.go | 17 +++ template.go | 14 +++ testdata/expected-fish-full.fish | 28 +++++ 4 files changed, 230 insertions(+) create mode 100644 fish.go create mode 100644 fish_test.go create mode 100644 testdata/expected-fish-full.fish diff --git a/fish.go b/fish.go new file mode 100644 index 0000000..0f51065 --- /dev/null +++ b/fish.go @@ -0,0 +1,171 @@ +package cli + +import ( + "bytes" + "fmt" + "io" + "strings" + "text/template" +) + +// ToFishCompletion creates a fish completion string for the `*App` +// The function errors if either parsing or writing of the string fails. +func (a *App) ToFishCompletion() (string, error) { + var w bytes.Buffer + if err := a.writeFishCompletionTemplate(&w); err != nil { + return "", err + } + return w.String(), nil +} + +type fishCompletionTemplate struct { + App *App + Completions []string + AllCommands []string +} + +func (a *App) writeFishCompletionTemplate(w io.Writer) error { + const name = "cli" + t, err := template.New(name).Parse(FishCompletionTemplate) + if err != nil { + return err + } + allCommands := []string{} + + // Add global flags + completions := a.prepareFishFlags(a.VisibleFlags(), allCommands) + + // Add help flag + if !a.HideHelp { + completions = append( + completions, + a.prepareFishFlags([]Flag{HelpFlag}, allCommands)..., + ) + } + + // Add version flag + if !a.HideVersion { + completions = append( + completions, + a.prepareFishFlags([]Flag{VersionFlag}, allCommands)..., + ) + } + + // Add commands and their flags + completions = append( + completions, + a.prepareFishCommands(a.VisibleCommands(), &allCommands, []string{})..., + ) + + return t.ExecuteTemplate(w, name, &fishCompletionTemplate{ + App: a, + Completions: completions, + AllCommands: allCommands, + }) +} + +func (a *App) prepareFishCommands( + commands []Command, + allCommands *[]string, + previousCommands []string, +) []string { + completions := []string{} + for i := range commands { + command := &commands[i] + + var completion strings.Builder + completion.WriteString(fmt.Sprintf( + "complete -c %s -f -n '%s' -a '%s'", + a.Name, + a.fishSubcommandHelper(previousCommands), + strings.Join(command.Names(), " "), + )) + + if command.Usage != "" { + completion.WriteString(fmt.Sprintf(" -d '%s'", command.Usage)) + } + + if !command.HideHelp { + completions = append( + completions, + a.prepareFishFlags([]Flag{HelpFlag}, command.Names())..., + ) + } + + *allCommands = append(*allCommands, command.Names()...) + completions = append(completions, completion.String()) + completions = append( + completions, + a.prepareFishFlags(command.Flags, command.Names())..., + ) + + // recursevly iterate subcommands + if len(command.Subcommands) > 0 { + completions = append( + completions, + a.prepareFishCommands( + command.Subcommands, allCommands, command.Names(), + )..., + ) + } + } + + return completions +} + +func (a *App) prepareFishFlags( + flags []Flag, + previousCommands []string, +) []string { + completions := []string{} + for _, f := range flags { + flag, ok := f.(DocGenerationFlag) + if !ok { + continue + } + + var completion strings.Builder + completion.WriteString(fmt.Sprintf( + "complete -c %s -f -n '%s'", + a.Name, + a.fishSubcommandHelper(previousCommands), + )) + + for idx, opt := range strings.Split(flag.GetName(), ",") { + if idx == 0 { + completion.WriteString(fmt.Sprintf( + " -l %s", strings.TrimSpace(opt), + )) + } else { + completion.WriteString(fmt.Sprintf( + " -s %s", strings.TrimSpace(opt), + )) + + } + } + + if flag.TakesValue() { + completion.WriteString(" -r") + } + + if flag.GetUsage() != "" { + completion.WriteString(fmt.Sprintf(" -d '%s'", flag.GetUsage())) + } + + completions = append(completions, completion.String()) + } + + return completions +} + +func (a *App) fishSubcommandHelper(allCommands []string) string { + fishHelper := fmt.Sprintf("__fish_%s_no_subcommand", a.Name) + if len(allCommands) > 0 { + fishHelper = fmt.Sprintf( + "__fish_seen_subcommand_from %s", + strings.Join(allCommands, " "), + ) + } + return fishHelper + +} diff --git a/fish_test.go b/fish_test.go new file mode 100644 index 0000000..a4c1871 --- /dev/null +++ b/fish_test.go @@ -0,0 +1,17 @@ +package cli + +import ( + "testing" +) + +func TestFishCompletion(t *testing.T) { + // Given + app := testApp() + + // When + res, err := app.ToFishCompletion() + + // Then + expect(t, err, nil) + expectFileContent(t, "testdata/expected-fish-full.fish", res) +} diff --git a/template.go b/template.go index 24c44c8..c631fb9 100644 --- a/template.go +++ b/template.go @@ -105,3 +105,17 @@ var MarkdownDocTemplate = `% {{ .App.Name }}(8) {{ .App.Description }} # COMMANDS {{ range $v := .Commands }} {{ $v }}{{ end }}{{ end }}` + +var FishCompletionTemplate = `# {{ .App.Name }} fish shell completion + +function __fish_{{ .App.Name }}_no_subcommand --description 'Test if there has been any subcommand yet' + for i in (commandline -opc) + if contains -- $i{{ range $v := .AllCommands }} {{ $v }}{{ end }} + return 1 + end + end + return 0 +end + +{{ range $v := .Completions }}{{ $v }} +{{ end }}` diff --git a/testdata/expected-fish-full.fish b/testdata/expected-fish-full.fish new file mode 100644 index 0000000..3538c19 --- /dev/null +++ b/testdata/expected-fish-full.fish @@ -0,0 +1,28 @@ +# greet fish shell completion + +function __fish_greet_no_subcommand --description 'Test if there has been any subcommand yet' + for i in (commandline -opc) + if contains -- $i config c sub-config s ss info i in some-command + return 1 + end + end + return 0 +end + +complete -c greet -f -n '__fish_greet_no_subcommand' -l socket -s s -r -d 'some usage text' +complete -c greet -f -n '__fish_greet_no_subcommand' -l flag -s fl -s f -r +complete -c greet -f -n '__fish_greet_no_subcommand' -l another-flag -s b -d 'another usage text' +complete -c greet -f -n '__fish_greet_no_subcommand' -l help -s h -d 'show help' +complete -c greet -f -n '__fish_greet_no_subcommand' -l version -s v -d 'print the version' +complete -c greet -f -n '__fish_seen_subcommand_from config c' -l help -s h -d 'show help' +complete -c greet -f -n '__fish_greet_no_subcommand' -a 'config c' -d 'another usage test' +complete -c greet -f -n '__fish_seen_subcommand_from config c' -l flag -s fl -s f -r +complete -c greet -f -n '__fish_seen_subcommand_from config c' -l another-flag -s b -d 'another usage text' +complete -c greet -f -n '__fish_seen_subcommand_from sub-config s ss' -l help -s h -d 'show help' +complete -c greet -f -n '__fish_seen_subcommand_from config c' -a 'sub-config s ss' -d 'another usage test' +complete -c greet -f -n '__fish_seen_subcommand_from sub-config s ss' -l sub-flag -s sub-fl -s s -r +complete -c greet -f -n '__fish_seen_subcommand_from sub-config s ss' -l sub-command-flag -s s -d 'some usage text' +complete -c greet -f -n '__fish_seen_subcommand_from info i in' -l help -s h -d 'show help' +complete -c greet -f -n '__fish_greet_no_subcommand' -a 'info i in' -d 'retrieve generic information' +complete -c greet -f -n '__fish_seen_subcommand_from some-command' -l help -s h -d 'show help' +complete -c greet -f -n '__fish_greet_no_subcommand' -a 'some-command'