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 <sgrunert@suse.com>
main
Sascha Grunert 5 years ago
parent 2e0e39a03b
commit 7506b11da7
No known key found for this signature in database
GPG Key ID: 8CE029DD1A866E52

@ -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
}

@ -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)
}

@ -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 }}`

@ -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'
Loading…
Cancel
Save