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