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