1package cli 2 3import ( 4 "bytes" 5 "fmt" 6 "io" 7 "strings" 8 "text/template" 9) 10 11// ToFishCompletion creates a fish completion string for the `*App` 12// The function errors if either parsing or writing of the string fails. 13func (a *App) ToFishCompletion() (string, error) { 14 var w bytes.Buffer 15 if err := a.writeFishCompletionTemplate(&w); err != nil { 16 return "", err 17 } 18 return w.String(), nil 19} 20 21type fishCompletionTemplate struct { 22 App *App 23 Completions []string 24 AllCommands []string 25} 26 27func (a *App) writeFishCompletionTemplate(w io.Writer) error { 28 const name = "cli" 29 t, err := template.New(name).Parse(FishCompletionTemplate) 30 if err != nil { 31 return err 32 } 33 allCommands := []string{} 34 35 // Add global flags 36 completions := a.prepareFishFlags(a.VisibleFlags(), allCommands) 37 38 // Add help flag 39 if !a.HideHelp { 40 completions = append( 41 completions, 42 a.prepareFishFlags([]Flag{HelpFlag}, allCommands)..., 43 ) 44 } 45 46 // Add version flag 47 if !a.HideVersion { 48 completions = append( 49 completions, 50 a.prepareFishFlags([]Flag{VersionFlag}, allCommands)..., 51 ) 52 } 53 54 // Add commands and their flags 55 completions = append( 56 completions, 57 a.prepareFishCommands(a.VisibleCommands(), &allCommands, []string{})..., 58 ) 59 60 return t.ExecuteTemplate(w, name, &fishCompletionTemplate{ 61 App: a, 62 Completions: completions, 63 AllCommands: allCommands, 64 }) 65} 66 67func (a *App) prepareFishCommands(commands []*Command, allCommands *[]string, previousCommands []string) []string { 68 completions := []string{} 69 for _, command := range commands { 70 if command.Hidden { 71 continue 72 } 73 74 var completion strings.Builder 75 completion.WriteString(fmt.Sprintf( 76 "complete -r -c %s -n '%s' -a '%s'", 77 a.Name, 78 a.fishSubcommandHelper(previousCommands), 79 strings.Join(command.Names(), " "), 80 )) 81 82 if command.Usage != "" { 83 completion.WriteString(fmt.Sprintf(" -d '%s'", 84 escapeSingleQuotes(command.Usage))) 85 } 86 87 if !command.HideHelp { 88 completions = append( 89 completions, 90 a.prepareFishFlags([]Flag{HelpFlag}, command.Names())..., 91 ) 92 } 93 94 *allCommands = append(*allCommands, command.Names()...) 95 completions = append(completions, completion.String()) 96 completions = append( 97 completions, 98 a.prepareFishFlags(command.Flags, command.Names())..., 99 ) 100 101 // recursevly iterate subcommands 102 if len(command.Subcommands) > 0 { 103 completions = append( 104 completions, 105 a.prepareFishCommands( 106 command.Subcommands, allCommands, command.Names(), 107 )..., 108 ) 109 } 110 } 111 112 return completions 113} 114 115func (a *App) prepareFishFlags(flags []Flag, previousCommands []string) []string { 116 completions := []string{} 117 for _, f := range flags { 118 flag, ok := f.(DocGenerationFlag) 119 if !ok { 120 continue 121 } 122 123 completion := &strings.Builder{} 124 completion.WriteString(fmt.Sprintf( 125 "complete -c %s -n '%s'", 126 a.Name, 127 a.fishSubcommandHelper(previousCommands), 128 )) 129 130 fishAddFileFlag(f, completion) 131 132 for idx, opt := range flag.Names() { 133 if idx == 0 { 134 completion.WriteString(fmt.Sprintf( 135 " -l %s", strings.TrimSpace(opt), 136 )) 137 } else { 138 completion.WriteString(fmt.Sprintf( 139 " -s %s", strings.TrimSpace(opt), 140 )) 141 142 } 143 } 144 145 if flag.TakesValue() { 146 completion.WriteString(" -r") 147 } 148 149 if flag.GetUsage() != "" { 150 completion.WriteString(fmt.Sprintf(" -d '%s'", 151 escapeSingleQuotes(flag.GetUsage()))) 152 } 153 154 completions = append(completions, completion.String()) 155 } 156 157 return completions 158} 159 160func fishAddFileFlag(flag Flag, completion *strings.Builder) { 161 switch f := flag.(type) { 162 case *GenericFlag: 163 if f.TakesFile { 164 return 165 } 166 case *StringFlag: 167 if f.TakesFile { 168 return 169 } 170 case *StringSliceFlag: 171 if f.TakesFile { 172 return 173 } 174 } 175 completion.WriteString(" -f") 176} 177 178func (a *App) fishSubcommandHelper(allCommands []string) string { 179 fishHelper := fmt.Sprintf("__fish_%s_no_subcommand", a.Name) 180 if len(allCommands) > 0 { 181 fishHelper = fmt.Sprintf( 182 "__fish_seen_subcommand_from %s", 183 strings.Join(allCommands, " "), 184 ) 185 } 186 return fishHelper 187 188} 189 190func escapeSingleQuotes(input string) string { 191 return strings.Replace(input, `'`, `\'`, -1) 192} 193