1package cli
2
3import (
4	"fmt"
5	"io"
6	"os"
7	"strings"
8	"text/tabwriter"
9	"text/template"
10	"unicode/utf8"
11)
12
13var helpCommand = Command{
14	Name:      "help",
15	Aliases:   []string{"h"},
16	Usage:     "Shows a list of commands or help for one command",
17	ArgsUsage: "[command]",
18	Action: func(c *Context) error {
19		args := c.Args()
20		if args.Present() {
21			return ShowCommandHelp(c, args.First())
22		}
23
24		_ = ShowAppHelp(c)
25		return nil
26	},
27}
28
29var helpSubcommand = Command{
30	Name:      "help",
31	Aliases:   []string{"h"},
32	Usage:     "Shows a list of commands or help for one command",
33	ArgsUsage: "[command]",
34	Action: func(c *Context) error {
35		args := c.Args()
36		if args.Present() {
37			return ShowCommandHelp(c, args.First())
38		}
39
40		return ShowSubcommandHelp(c)
41	},
42}
43
44// Prints help for the App or Command
45type helpPrinter func(w io.Writer, templ string, data interface{})
46
47// Prints help for the App or Command with custom template function.
48type helpPrinterCustom func(w io.Writer, templ string, data interface{}, customFunc map[string]interface{})
49
50// HelpPrinter is a function that writes the help output. If not set explicitly,
51// this calls HelpPrinterCustom using only the default template functions.
52//
53// If custom logic for printing help is required, this function can be
54// overridden. If the ExtraInfo field is defined on an App, this function
55// should not be modified, as HelpPrinterCustom will be used directly in order
56// to capture the extra information.
57var HelpPrinter helpPrinter = printHelp
58
59// HelpPrinterCustom is a function that writes the help output. It is used as
60// the default implementation of HelpPrinter, and may be called directly if
61// the ExtraInfo field is set on an App.
62var HelpPrinterCustom helpPrinterCustom = printHelpCustom
63
64// VersionPrinter prints the version for the App
65var VersionPrinter = printVersion
66
67// ShowAppHelpAndExit - Prints the list of subcommands for the app and exits with exit code.
68func ShowAppHelpAndExit(c *Context, exitCode int) {
69	_ = ShowAppHelp(c)
70	os.Exit(exitCode)
71}
72
73// ShowAppHelp is an action that displays the help.
74func ShowAppHelp(c *Context) error {
75	template := c.App.CustomAppHelpTemplate
76	if template == "" {
77		template = AppHelpTemplate
78	}
79
80	if c.App.ExtraInfo == nil {
81		HelpPrinter(c.App.Writer, template, c.App)
82		return nil
83	}
84
85	customAppData := func() map[string]interface{} {
86		return map[string]interface{}{
87			"ExtraInfo": c.App.ExtraInfo,
88		}
89	}
90	HelpPrinterCustom(c.App.Writer, template, c.App, customAppData())
91
92	return nil
93}
94
95// DefaultAppComplete prints the list of subcommands as the default app completion method
96func DefaultAppComplete(c *Context) {
97	DefaultCompleteWithFlags(nil)(c)
98}
99
100func printCommandSuggestions(commands []Command, writer io.Writer) {
101	for _, command := range commands {
102		if command.Hidden {
103			continue
104		}
105		if os.Getenv("_CLI_ZSH_AUTOCOMPLETE_HACK") == "1" {
106			for _, name := range command.Names() {
107				_, _ = fmt.Fprintf(writer, "%s:%s\n", name, command.Usage)
108			}
109		} else {
110			for _, name := range command.Names() {
111				_, _ = fmt.Fprintf(writer, "%s\n", name)
112			}
113		}
114	}
115}
116
117func cliArgContains(flagName string) bool {
118	for _, name := range strings.Split(flagName, ",") {
119		name = strings.TrimSpace(name)
120		count := utf8.RuneCountInString(name)
121		if count > 2 {
122			count = 2
123		}
124		flag := fmt.Sprintf("%s%s", strings.Repeat("-", count), name)
125		for _, a := range os.Args {
126			if a == flag {
127				return true
128			}
129		}
130	}
131	return false
132}
133
134func printFlagSuggestions(lastArg string, flags []Flag, writer io.Writer) {
135	cur := strings.TrimPrefix(lastArg, "-")
136	cur = strings.TrimPrefix(cur, "-")
137	for _, flag := range flags {
138		if bflag, ok := flag.(BoolFlag); ok && bflag.Hidden {
139			continue
140		}
141		for _, name := range strings.Split(flag.GetName(), ",") {
142			name = strings.TrimSpace(name)
143			// this will get total count utf8 letters in flag name
144			count := utf8.RuneCountInString(name)
145			if count > 2 {
146				count = 2 // resuse this count to generate single - or -- in flag completion
147			}
148			// if flag name has more than one utf8 letter and last argument in cli has -- prefix then
149			// skip flag completion for short flags example -v or -x
150			if strings.HasPrefix(lastArg, "--") && count == 1 {
151				continue
152			}
153			// match if last argument matches this flag and it is not repeated
154			if strings.HasPrefix(name, cur) && cur != name && !cliArgContains(flag.GetName()) {
155				flagCompletion := fmt.Sprintf("%s%s", strings.Repeat("-", count), name)
156				_, _ = fmt.Fprintln(writer, flagCompletion)
157			}
158		}
159	}
160}
161
162func DefaultCompleteWithFlags(cmd *Command) func(c *Context) {
163	return func(c *Context) {
164		if len(os.Args) > 2 {
165			lastArg := os.Args[len(os.Args)-2]
166			if strings.HasPrefix(lastArg, "-") {
167				printFlagSuggestions(lastArg, c.App.Flags, c.App.Writer)
168				if cmd != nil {
169					printFlagSuggestions(lastArg, cmd.Flags, c.App.Writer)
170				}
171				return
172			}
173		}
174		if cmd != nil {
175			printCommandSuggestions(cmd.Subcommands, c.App.Writer)
176		} else {
177			printCommandSuggestions(c.App.Commands, c.App.Writer)
178		}
179	}
180}
181
182// ShowCommandHelpAndExit - exits with code after showing help
183func ShowCommandHelpAndExit(c *Context, command string, code int) {
184	_ = ShowCommandHelp(c, command)
185	os.Exit(code)
186}
187
188// ShowCommandHelp prints help for the given command
189func ShowCommandHelp(ctx *Context, command string) error {
190	// show the subcommand help for a command with subcommands
191	if command == "" {
192		HelpPrinter(ctx.App.Writer, SubcommandHelpTemplate, ctx.App)
193		return nil
194	}
195
196	for _, c := range ctx.App.Commands {
197		if c.HasName(command) {
198			templ := c.CustomHelpTemplate
199			if templ == "" {
200				templ = CommandHelpTemplate
201			}
202
203			HelpPrinter(ctx.App.Writer, templ, c)
204
205			return nil
206		}
207	}
208
209	if ctx.App.CommandNotFound == nil {
210		return NewExitError(fmt.Sprintf("No help topic for '%v'", command), 3)
211	}
212
213	ctx.App.CommandNotFound(ctx, command)
214	return nil
215}
216
217// ShowSubcommandHelp prints help for the given subcommand
218func ShowSubcommandHelp(c *Context) error {
219	return ShowCommandHelp(c, c.Command.Name)
220}
221
222// ShowVersion prints the version number of the App
223func ShowVersion(c *Context) {
224	VersionPrinter(c)
225}
226
227func printVersion(c *Context) {
228	_, _ = fmt.Fprintf(c.App.Writer, "%v version %v\n", c.App.Name, c.App.Version)
229}
230
231// ShowCompletions prints the lists of commands within a given context
232func ShowCompletions(c *Context) {
233	a := c.App
234	if a != nil && a.BashComplete != nil {
235		a.BashComplete(c)
236	}
237}
238
239// ShowCommandCompletions prints the custom completions for a given command
240func ShowCommandCompletions(ctx *Context, command string) {
241	c := ctx.App.Command(command)
242	if c != nil {
243		if c.BashComplete != nil {
244			c.BashComplete(ctx)
245		} else {
246			DefaultCompleteWithFlags(c)(ctx)
247		}
248	}
249
250}
251
252// printHelpCustom is the default implementation of HelpPrinterCustom.
253//
254// The customFuncs map will be combined with a default template.FuncMap to
255// allow using arbitrary functions in template rendering.
256func printHelpCustom(out io.Writer, templ string, data interface{}, customFuncs map[string]interface{}) {
257	funcMap := template.FuncMap{
258		"join": strings.Join,
259	}
260	for key, value := range customFuncs {
261		funcMap[key] = value
262	}
263
264	w := tabwriter.NewWriter(out, 1, 8, 2, ' ', 0)
265	t := template.Must(template.New("help").Funcs(funcMap).Parse(templ))
266	err := t.Execute(w, data)
267	if err != nil {
268		// If the writer is closed, t.Execute will fail, and there's nothing
269		// we can do to recover.
270		if os.Getenv("CLI_TEMPLATE_ERROR_DEBUG") != "" {
271			_, _ = fmt.Fprintf(ErrWriter, "CLI TEMPLATE ERROR: %#v\n", err)
272		}
273		return
274	}
275	_ = w.Flush()
276}
277
278func printHelp(out io.Writer, templ string, data interface{}) {
279	HelpPrinterCustom(out, templ, data, nil)
280}
281
282func checkVersion(c *Context) bool {
283	found := false
284	if VersionFlag.GetName() != "" {
285		eachName(VersionFlag.GetName(), func(name string) {
286			if c.GlobalBool(name) || c.Bool(name) {
287				found = true
288			}
289		})
290	}
291	return found
292}
293
294func checkHelp(c *Context) bool {
295	found := false
296	if HelpFlag.GetName() != "" {
297		eachName(HelpFlag.GetName(), func(name string) {
298			if c.GlobalBool(name) || c.Bool(name) {
299				found = true
300			}
301		})
302	}
303	return found
304}
305
306func checkCommandHelp(c *Context, name string) bool {
307	if c.Bool("h") || c.Bool("help") {
308		_ = ShowCommandHelp(c, name)
309		return true
310	}
311
312	return false
313}
314
315func checkSubcommandHelp(c *Context) bool {
316	if c.Bool("h") || c.Bool("help") {
317		_ = ShowSubcommandHelp(c)
318		return true
319	}
320
321	return false
322}
323
324func checkShellCompleteFlag(a *App, arguments []string) (bool, []string) {
325	if !a.EnableBashCompletion {
326		return false, arguments
327	}
328
329	pos := len(arguments) - 1
330	lastArg := arguments[pos]
331
332	if lastArg != "--"+BashCompletionFlag.GetName() {
333		return false, arguments
334	}
335
336	return true, arguments[:pos]
337}
338
339func checkCompletions(c *Context) bool {
340	if !c.shellComplete {
341		return false
342	}
343
344	if args := c.Args(); args.Present() {
345		name := args.First()
346		if cmd := c.App.Command(name); cmd != nil {
347			// let the command handle the completion
348			return false
349		}
350	}
351
352	ShowCompletions(c)
353	return true
354}
355
356func checkCommandCompletions(c *Context, name string) bool {
357	if !c.shellComplete {
358		return false
359	}
360
361	ShowCommandCompletions(c, name)
362	return true
363}
364