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