Skip to content

Commit

Permalink
Add fish shell completion support
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
saschagrunert committed Aug 9, 2019
1 parent 946f918 commit 8bf6714
Show file tree
Hide file tree
Showing 4 changed files with 230 additions and 0 deletions.
171 changes: 171 additions & 0 deletions 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

}
17 changes: 17 additions & 0 deletions 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)
}
14 changes: 14 additions & 0 deletions template.go
Expand Up @@ -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 changes: 28 additions & 0 deletions 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'

0 comments on commit 8bf6714

Please sign in to comment.