1package cli
2
3import (
4	"context"
5	"flag"
6	"fmt"
7	"io"
8	"os"
9	"path/filepath"
10	"sort"
11	"time"
12)
13
14var (
15	changeLogURL            = "https://github.com/urfave/cli/blob/master/docs/CHANGELOG.md"
16	appActionDeprecationURL = fmt.Sprintf("%s#deprecated-cli-app-action-signature", changeLogURL)
17	contactSysadmin         = "This is an error in the application.  Please contact the distributor of this application if this is not you."
18	errInvalidActionType    = NewExitError("ERROR invalid Action type. "+
19		fmt.Sprintf("Must be `func(*Context`)` or `func(*Context) error).  %s", contactSysadmin)+
20		fmt.Sprintf("See %s", appActionDeprecationURL), 2)
21)
22
23// App is the main structure of a cli application. It is recommended that
24// an app be created with the cli.NewApp() function
25type App struct {
26	// The name of the program. Defaults to path.Base(os.Args[0])
27	Name string
28	// Full name of command for help, defaults to Name
29	HelpName string
30	// Description of the program.
31	Usage string
32	// Text to override the USAGE section of help
33	UsageText string
34	// Description of the program argument format.
35	ArgsUsage string
36	// Version of the program
37	Version string
38	// Description of the program
39	Description string
40	// List of commands to execute
41	Commands []*Command
42	// List of flags to parse
43	Flags []Flag
44	// Boolean to enable bash completion commands
45	EnableBashCompletion bool
46	// Boolean to hide built-in help command and help flag
47	HideHelp bool
48	// Boolean to hide built-in help command but keep help flag.
49	// Ignored if HideHelp is true.
50	HideHelpCommand bool
51	// Boolean to hide built-in version flag and the VERSION section of help
52	HideVersion bool
53	// categories contains the categorized commands and is populated on app startup
54	categories CommandCategories
55	// An action to execute when the shell completion flag is set
56	BashComplete BashCompleteFunc
57	// An action to execute before any subcommands are run, but after the context is ready
58	// If a non-nil error is returned, no subcommands are run
59	Before BeforeFunc
60	// An action to execute after any subcommands are run, but after the subcommand has finished
61	// It is run even if Action() panics
62	After AfterFunc
63	// The action to execute when no subcommands are specified
64	Action ActionFunc
65	// Execute this function if the proper command cannot be found
66	CommandNotFound CommandNotFoundFunc
67	// Execute this function if an usage error occurs
68	OnUsageError OnUsageErrorFunc
69	// Compilation date
70	Compiled time.Time
71	// List of all authors who contributed
72	Authors []*Author
73	// Copyright of the binary if any
74	Copyright string
75	// Writer writer to write output to
76	Writer io.Writer
77	// ErrWriter writes error output
78	ErrWriter io.Writer
79	// Execute this function to handle ExitErrors. If not provided, HandleExitCoder is provided to
80	// function as a default, so this is optional.
81	ExitErrHandler ExitErrHandlerFunc
82	// Other custom info
83	Metadata map[string]interface{}
84	// Carries a function which returns app specific info.
85	ExtraInfo func() map[string]string
86	// CustomAppHelpTemplate the text template for app help topic.
87	// cli.go uses text/template to render templates. You can
88	// render custom help text by setting this variable.
89	CustomAppHelpTemplate string
90	// Boolean to enable short-option handling so user can combine several
91	// single-character bool arguments into one
92	// i.e. foobar -o -v -> foobar -ov
93	UseShortOptionHandling bool
94
95	didSetup bool
96}
97
98// Tries to find out when this binary was compiled.
99// Returns the current time if it fails to find it.
100func compileTime() time.Time {
101	info, err := os.Stat(os.Args[0])
102	if err != nil {
103		return time.Now()
104	}
105	return info.ModTime()
106}
107
108// NewApp creates a new cli Application with some reasonable defaults for Name,
109// Usage, Version and Action.
110func NewApp() *App {
111	return &App{
112		Name:         filepath.Base(os.Args[0]),
113		HelpName:     filepath.Base(os.Args[0]),
114		Usage:        "A new cli application",
115		UsageText:    "",
116		BashComplete: DefaultAppComplete,
117		Action:       helpCommand.Action,
118		Compiled:     compileTime(),
119		Writer:       os.Stdout,
120	}
121}
122
123// Setup runs initialization code to ensure all data structures are ready for
124// `Run` or inspection prior to `Run`.  It is internally called by `Run`, but
125// will return early if setup has already happened.
126func (a *App) Setup() {
127	if a.didSetup {
128		return
129	}
130
131	a.didSetup = true
132
133	if a.Name == "" {
134		a.Name = filepath.Base(os.Args[0])
135	}
136
137	if a.HelpName == "" {
138		a.HelpName = filepath.Base(os.Args[0])
139	}
140
141	if a.Usage == "" {
142		a.Usage = "A new cli application"
143	}
144
145	if a.Version == "" {
146		a.HideVersion = true
147	}
148
149	if a.BashComplete == nil {
150		a.BashComplete = DefaultAppComplete
151	}
152
153	if a.Action == nil {
154		a.Action = helpCommand.Action
155	}
156
157	if a.Compiled == (time.Time{}) {
158		a.Compiled = compileTime()
159	}
160
161	if a.Writer == nil {
162		a.Writer = os.Stdout
163	}
164
165	var newCommands []*Command
166
167	for _, c := range a.Commands {
168		if c.HelpName == "" {
169			c.HelpName = fmt.Sprintf("%s %s", a.HelpName, c.Name)
170		}
171		newCommands = append(newCommands, c)
172	}
173	a.Commands = newCommands
174
175	if a.Command(helpCommand.Name) == nil && !a.HideHelp {
176		if !a.HideHelpCommand {
177			a.appendCommand(helpCommand)
178		}
179
180		if HelpFlag != nil {
181			a.appendFlag(HelpFlag)
182		}
183	}
184
185	if !a.HideVersion {
186		a.appendFlag(VersionFlag)
187	}
188
189	a.categories = newCommandCategories()
190	for _, command := range a.Commands {
191		a.categories.AddCommand(command.Category, command)
192	}
193	sort.Sort(a.categories.(*commandCategories))
194
195	if a.Metadata == nil {
196		a.Metadata = make(map[string]interface{})
197	}
198
199	if a.Writer == nil {
200		a.Writer = os.Stdout
201	}
202}
203
204func (a *App) newFlagSet() (*flag.FlagSet, error) {
205	return flagSet(a.Name, a.Flags)
206}
207
208func (a *App) useShortOptionHandling() bool {
209	return a.UseShortOptionHandling
210}
211
212// Run is the entry point to the cli app. Parses the arguments slice and routes
213// to the proper flag/args combination
214func (a *App) Run(arguments []string) (err error) {
215	return a.RunContext(context.Background(), arguments)
216}
217
218// RunContext is like Run except it takes a Context that will be
219// passed to its commands and sub-commands. Through this, you can
220// propagate timeouts and cancellation requests
221func (a *App) RunContext(ctx context.Context, arguments []string) (err error) {
222	a.Setup()
223
224	// handle the completion flag separately from the flagset since
225	// completion could be attempted after a flag, but before its value was put
226	// on the command line. this causes the flagset to interpret the completion
227	// flag name as the value of the flag before it which is undesirable
228	// note that we can only do this because the shell autocomplete function
229	// always appends the completion flag at the end of the command
230	shellComplete, arguments := checkShellCompleteFlag(a, arguments)
231
232	set, err := a.newFlagSet()
233	if err != nil {
234		return err
235	}
236
237	err = parseIter(set, a, arguments[1:], shellComplete)
238	nerr := normalizeFlags(a.Flags, set)
239	context := NewContext(a, set, &Context{Context: ctx})
240	if nerr != nil {
241		_, _ = fmt.Fprintln(a.Writer, nerr)
242		_ = ShowAppHelp(context)
243		return nerr
244	}
245	context.shellComplete = shellComplete
246
247	if checkCompletions(context) {
248		return nil
249	}
250
251	if err != nil {
252		if a.OnUsageError != nil {
253			err := a.OnUsageError(context, err, false)
254			a.handleExitCoder(context, err)
255			return err
256		}
257		_, _ = fmt.Fprintf(a.Writer, "%s %s\n\n", "Incorrect Usage.", err.Error())
258		_ = ShowAppHelp(context)
259		return err
260	}
261
262	if !a.HideHelp && checkHelp(context) {
263		_ = ShowAppHelp(context)
264		return nil
265	}
266
267	if !a.HideVersion && checkVersion(context) {
268		ShowVersion(context)
269		return nil
270	}
271
272	cerr := checkRequiredFlags(a.Flags, context)
273	if cerr != nil {
274		_ = ShowAppHelp(context)
275		return cerr
276	}
277
278	if a.After != nil {
279		defer func() {
280			if afterErr := a.After(context); afterErr != nil {
281				if err != nil {
282					err = newMultiError(err, afterErr)
283				} else {
284					err = afterErr
285				}
286			}
287		}()
288	}
289
290	if a.Before != nil {
291		beforeErr := a.Before(context)
292		if beforeErr != nil {
293			_, _ = fmt.Fprintf(a.Writer, "%v\n\n", beforeErr)
294			_ = ShowAppHelp(context)
295			a.handleExitCoder(context, beforeErr)
296			err = beforeErr
297			return err
298		}
299	}
300
301	args := context.Args()
302	if args.Present() {
303		name := args.First()
304		c := a.Command(name)
305		if c != nil {
306			return c.Run(context)
307		}
308	}
309
310	if a.Action == nil {
311		a.Action = helpCommand.Action
312	}
313
314	// Run default Action
315	err = a.Action(context)
316
317	a.handleExitCoder(context, err)
318	return err
319}
320
321// RunAndExitOnError calls .Run() and exits non-zero if an error was returned
322//
323// Deprecated: instead you should return an error that fulfills cli.ExitCoder
324// to cli.App.Run. This will cause the application to exit with the given eror
325// code in the cli.ExitCoder
326func (a *App) RunAndExitOnError() {
327	if err := a.Run(os.Args); err != nil {
328		_, _ = fmt.Fprintln(a.errWriter(), err)
329		OsExiter(1)
330	}
331}
332
333// RunAsSubcommand invokes the subcommand given the context, parses ctx.Args() to
334// generate command-specific flags
335func (a *App) RunAsSubcommand(ctx *Context) (err error) {
336	// Setup also handles HideHelp and HideHelpCommand
337	a.Setup()
338
339	var newCmds []*Command
340	for _, c := range a.Commands {
341		if c.HelpName == "" {
342			c.HelpName = fmt.Sprintf("%s %s", a.HelpName, c.Name)
343		}
344		newCmds = append(newCmds, c)
345	}
346	a.Commands = newCmds
347
348	set, err := a.newFlagSet()
349	if err != nil {
350		return err
351	}
352
353	err = parseIter(set, a, ctx.Args().Tail(), ctx.shellComplete)
354	nerr := normalizeFlags(a.Flags, set)
355	context := NewContext(a, set, ctx)
356
357	if nerr != nil {
358		_, _ = fmt.Fprintln(a.Writer, nerr)
359		_, _ = fmt.Fprintln(a.Writer)
360		if len(a.Commands) > 0 {
361			_ = ShowSubcommandHelp(context)
362		} else {
363			_ = ShowCommandHelp(ctx, context.Args().First())
364		}
365		return nerr
366	}
367
368	if checkCompletions(context) {
369		return nil
370	}
371
372	if err != nil {
373		if a.OnUsageError != nil {
374			err = a.OnUsageError(context, err, true)
375			a.handleExitCoder(context, err)
376			return err
377		}
378		_, _ = fmt.Fprintf(a.Writer, "%s %s\n\n", "Incorrect Usage.", err.Error())
379		_ = ShowSubcommandHelp(context)
380		return err
381	}
382
383	if len(a.Commands) > 0 {
384		if checkSubcommandHelp(context) {
385			return nil
386		}
387	} else {
388		if checkCommandHelp(ctx, context.Args().First()) {
389			return nil
390		}
391	}
392
393	cerr := checkRequiredFlags(a.Flags, context)
394	if cerr != nil {
395		_ = ShowSubcommandHelp(context)
396		return cerr
397	}
398
399	if a.After != nil {
400		defer func() {
401			afterErr := a.After(context)
402			if afterErr != nil {
403				a.handleExitCoder(context, err)
404				if err != nil {
405					err = newMultiError(err, afterErr)
406				} else {
407					err = afterErr
408				}
409			}
410		}()
411	}
412
413	if a.Before != nil {
414		beforeErr := a.Before(context)
415		if beforeErr != nil {
416			a.handleExitCoder(context, beforeErr)
417			err = beforeErr
418			return err
419		}
420	}
421
422	args := context.Args()
423	if args.Present() {
424		name := args.First()
425		c := a.Command(name)
426		if c != nil {
427			return c.Run(context)
428		}
429	}
430
431	// Run default Action
432	err = a.Action(context)
433
434	a.handleExitCoder(context, err)
435	return err
436}
437
438// Command returns the named command on App. Returns nil if the command does not exist
439func (a *App) Command(name string) *Command {
440	for _, c := range a.Commands {
441		if c.HasName(name) {
442			return c
443		}
444	}
445
446	return nil
447}
448
449// VisibleCategories returns a slice of categories and commands that are
450// Hidden=false
451func (a *App) VisibleCategories() []CommandCategory {
452	ret := []CommandCategory{}
453	for _, category := range a.categories.Categories() {
454		if visible := func() CommandCategory {
455			if len(category.VisibleCommands()) > 0 {
456				return category
457			}
458			return nil
459		}(); visible != nil {
460			ret = append(ret, visible)
461		}
462	}
463	return ret
464}
465
466// VisibleCommands returns a slice of the Commands with Hidden=false
467func (a *App) VisibleCommands() []*Command {
468	var ret []*Command
469	for _, command := range a.Commands {
470		if !command.Hidden {
471			ret = append(ret, command)
472		}
473	}
474	return ret
475}
476
477// VisibleFlags returns a slice of the Flags with Hidden=false
478func (a *App) VisibleFlags() []Flag {
479	return visibleFlags(a.Flags)
480}
481
482func (a *App) errWriter() io.Writer {
483	// When the app ErrWriter is nil use the package level one.
484	if a.ErrWriter == nil {
485		return ErrWriter
486	}
487
488	return a.ErrWriter
489}
490
491func (a *App) appendFlag(fl Flag) {
492	if !hasFlag(a.Flags, fl) {
493		a.Flags = append(a.Flags, fl)
494	}
495}
496
497func (a *App) appendCommand(c *Command) {
498	if !hasCommand(a.Commands, c) {
499		a.Commands = append(a.Commands, c)
500	}
501}
502
503func (a *App) handleExitCoder(context *Context, err error) {
504	if a.ExitErrHandler != nil {
505		a.ExitErrHandler(context, err)
506	} else {
507		HandleExitCoder(err)
508	}
509}
510
511// Author represents someone who has contributed to a cli project.
512type Author struct {
513	Name  string // The Authors name
514	Email string // The Authors email
515}
516
517// String makes Author comply to the Stringer interface, to allow an easy print in the templating process
518func (a *Author) String() string {
519	e := ""
520	if a.Email != "" {
521		e = " <" + a.Email + ">"
522	}
523
524	return fmt.Sprintf("%v%v", a.Name, e)
525}
526
527// HandleAction attempts to figure out which Action signature was used.  If
528// it's an ActionFunc or a func with the legacy signature for Action, the func
529// is run!
530func HandleAction(action interface{}, context *Context) (err error) {
531	switch a := action.(type) {
532	case ActionFunc:
533		return a(context)
534	case func(*Context) error:
535		return a(context)
536	case func(*Context): // deprecated function signature
537		a(context)
538		return nil
539	}
540
541	return errInvalidActionType
542}
543