Merge pull request #848 from saschagrunert/fish-shell
Add fish shell completion support
This commit is contained in:
commit
7e49cc210a
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
|
||||
{{ 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 }}`
|
||||
|
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