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 _, command := range commands { if command.Hidden { continue } var completion strings.Builder completion.WriteString(fmt.Sprintf( "complete -r -c %s -n '%s' -a '%s'", a.Name, a.fishSubcommandHelper(previousCommands), strings.Join(command.Names(), " "), )) if command.Usage != "" { completion.WriteString(fmt.Sprintf(" -d '%s'", escapeSingleQuotes(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.VisibleFlags(), command.Names())..., ) // recursively 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 } completion := &strings.Builder{} completion.WriteString(fmt.Sprintf( "complete -c %s -n '%s'", a.Name, a.fishSubcommandHelper(previousCommands), )) fishAddFileFlag(f, completion) for idx, opt := range flag.Names() { 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'", escapeSingleQuotes(flag.GetUsage()))) } completions = append(completions, completion.String()) } return completions } func fishAddFileFlag(flag Flag, completion *strings.Builder) { switch f := flag.(type) { case *GenericFlag: if f.TakesFile { return } case *StringFlag: if f.TakesFile { return } case *StringSliceFlag: if f.TakesFile { return } case *PathFlag: if f.TakesFile { return } } completion.WriteString(" -f") } 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 } func escapeSingleQuotes(input string) string { return strings.Replace(input, `'`, `\'`, -1) }