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} 498