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>
This commit is contained in:
parent
2e0e39a03b
commit
7506b11da7
171
fish.go
Normal file
171
fish.go
Normal file
@ -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
|
||||||
|
|
||||||
|
}
|
17
fish_test.go
Normal file
17
fish_test.go
Normal file
@ -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)
|
||||||
|
}
|
14
template.go
14
template.go
@ -105,3 +105,17 @@ var MarkdownDocTemplate = `% {{ .App.Name }}(8) {{ .App.Description }}
|
|||||||
# COMMANDS
|
# COMMANDS
|
||||||
{{ range $v := .Commands }}
|
{{ range $v := .Commands }}
|
||||||
{{ $v }}{{ end }}{{ end }}`
|
{{ $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 }}`
|
||||||
|
28
testdata/expected-fish-full.fish
vendored
Normal file
28
testdata/expected-fish-full.fish
vendored
Normal file
@ -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…
Reference in New Issue
Block a user