1package kingpin 2 3import ( 4 "fmt" 5 "io" 6 "os" 7 "regexp" 8 "strings" 9) 10 11var ( 12 ErrCommandNotSpecified = fmt.Errorf("command not specified") 13) 14 15var ( 16 envarTransformRegexp = regexp.MustCompile(`[^a-zA-Z0-9_]+`) 17) 18 19type ApplicationValidator func(*Application) error 20 21// An Application contains the definitions of flags, arguments and commands 22// for an application. 23type Application struct { 24 cmdMixin 25 initialized bool 26 27 Name string 28 Help string 29 30 author string 31 version string 32 errorWriter io.Writer // Destination for errors. 33 usageWriter io.Writer // Destination for usage 34 usageTemplate string 35 validator ApplicationValidator 36 terminate func(status int) // See Terminate() 37 noInterspersed bool // can flags be interspersed with args (or must they come first) 38 defaultEnvars bool 39 completion bool 40 41 // Help flag. Exposed for user customisation. 42 HelpFlag *FlagClause 43 // Help command. Exposed for user customisation. May be nil. 44 HelpCommand *CmdClause 45 // Version flag. Exposed for user customisation. May be nil. 46 VersionFlag *FlagClause 47} 48 49// New creates a new Kingpin application instance. 50func New(name, help string) *Application { 51 a := &Application{ 52 Name: name, 53 Help: help, 54 errorWriter: os.Stderr, // Left for backwards compatibility purposes. 55 usageWriter: os.Stderr, 56 usageTemplate: DefaultUsageTemplate, 57 terminate: os.Exit, 58 } 59 a.flagGroup = newFlagGroup() 60 a.argGroup = newArgGroup() 61 a.cmdGroup = newCmdGroup(a) 62 a.HelpFlag = a.Flag("help", "Show context-sensitive help (also try --help-long and --help-man).") 63 a.HelpFlag.Bool() 64 a.Flag("help-long", "Generate long help.").Hidden().PreAction(a.generateLongHelp).Bool() 65 a.Flag("help-man", "Generate a man page.").Hidden().PreAction(a.generateManPage).Bool() 66 a.Flag("completion-bash", "Output possible completions for the given args.").Hidden().BoolVar(&a.completion) 67 a.Flag("completion-script-bash", "Generate completion script for bash.").Hidden().PreAction(a.generateBashCompletionScript).Bool() 68 a.Flag("completion-script-zsh", "Generate completion script for ZSH.").Hidden().PreAction(a.generateZSHCompletionScript).Bool() 69 70 return a 71} 72 73func (a *Application) generateLongHelp(c *ParseContext) error { 74 a.Writer(os.Stdout) 75 if err := a.UsageForContextWithTemplate(c, 2, LongHelpTemplate); err != nil { 76 return err 77 } 78 a.terminate(0) 79 return nil 80} 81 82func (a *Application) generateManPage(c *ParseContext) error { 83 a.Writer(os.Stdout) 84 if err := a.UsageForContextWithTemplate(c, 2, ManPageTemplate); err != nil { 85 return err 86 } 87 a.terminate(0) 88 return nil 89} 90 91func (a *Application) generateBashCompletionScript(c *ParseContext) error { 92 a.Writer(os.Stdout) 93 if err := a.UsageForContextWithTemplate(c, 2, BashCompletionTemplate); err != nil { 94 return err 95 } 96 a.terminate(0) 97 return nil 98} 99 100func (a *Application) generateZSHCompletionScript(c *ParseContext) error { 101 a.Writer(os.Stdout) 102 if err := a.UsageForContextWithTemplate(c, 2, ZshCompletionTemplate); err != nil { 103 return err 104 } 105 a.terminate(0) 106 return nil 107} 108 109// DefaultEnvars configures all flags (that do not already have an associated 110// envar) to use a default environment variable in the form "<app>_<flag>". 111// 112// For example, if the application is named "foo" and a flag is named "bar- 113// waz" the environment variable: "FOO_BAR_WAZ". 114func (a *Application) DefaultEnvars() *Application { 115 a.defaultEnvars = true 116 return a 117} 118 119// Terminate specifies the termination handler. Defaults to os.Exit(status). 120// If nil is passed, a no-op function will be used. 121func (a *Application) Terminate(terminate func(int)) *Application { 122 if terminate == nil { 123 terminate = func(int) {} 124 } 125 a.terminate = terminate 126 return a 127} 128 129// Writer specifies the writer to use for usage and errors. Defaults to os.Stderr. 130// DEPRECATED: See ErrorWriter and UsageWriter. 131func (a *Application) Writer(w io.Writer) *Application { 132 a.errorWriter = w 133 a.usageWriter = w 134 return a 135} 136 137// ErrorWriter sets the io.Writer to use for errors. 138func (a *Application) ErrorWriter(w io.Writer) *Application { 139 a.errorWriter = w 140 return a 141} 142 143// UsageWriter sets the io.Writer to use for errors. 144func (a *Application) UsageWriter(w io.Writer) *Application { 145 a.usageWriter = w 146 return a 147} 148 149// UsageTemplate specifies the text template to use when displaying usage 150// information. The default is UsageTemplate. 151func (a *Application) UsageTemplate(template string) *Application { 152 a.usageTemplate = template 153 return a 154} 155 156// Validate sets a validation function to run when parsing. 157func (a *Application) Validate(validator ApplicationValidator) *Application { 158 a.validator = validator 159 return a 160} 161 162// ParseContext parses the given command line and returns the fully populated 163// ParseContext. 164func (a *Application) ParseContext(args []string) (*ParseContext, error) { 165 return a.parseContext(false, args) 166} 167 168func (a *Application) parseContext(ignoreDefault bool, args []string) (*ParseContext, error) { 169 if err := a.init(); err != nil { 170 return nil, err 171 } 172 context := tokenize(args, ignoreDefault) 173 err := parse(context, a) 174 return context, err 175} 176 177// Parse parses command-line arguments. It returns the selected command and an 178// error. The selected command will be a space separated subcommand, if 179// subcommands have been configured. 180// 181// This will populate all flag and argument values, call all callbacks, and so 182// on. 183func (a *Application) Parse(args []string) (command string, err error) { 184 185 context, parseErr := a.ParseContext(args) 186 selected := []string{} 187 var setValuesErr error 188 189 if context == nil { 190 // Since we do not throw error immediately, there could be a case 191 // where a context returns nil. Protect against that. 192 return "", parseErr 193 } 194 195 if err = a.setDefaults(context); err != nil { 196 return "", err 197 } 198 199 selected, setValuesErr = a.setValues(context) 200 201 if err = a.applyPreActions(context, !a.completion); err != nil { 202 return "", err 203 } 204 205 if a.completion { 206 a.generateBashCompletion(context) 207 a.terminate(0) 208 } else { 209 if parseErr != nil { 210 return "", parseErr 211 } 212 213 a.maybeHelp(context) 214 if !context.EOL() { 215 return "", fmt.Errorf("unexpected argument '%s'", context.Peek()) 216 } 217 218 if setValuesErr != nil { 219 return "", setValuesErr 220 } 221 222 command, err = a.execute(context, selected) 223 if err == ErrCommandNotSpecified { 224 a.writeUsage(context, nil) 225 } 226 } 227 return command, err 228} 229 230func (a *Application) writeUsage(context *ParseContext, err error) { 231 if err != nil { 232 a.Errorf("%s", err) 233 } 234 if err := a.UsageForContext(context); err != nil { 235 panic(err) 236 } 237 if err != nil { 238 a.terminate(1) 239 } else { 240 a.terminate(0) 241 } 242} 243 244func (a *Application) maybeHelp(context *ParseContext) { 245 for _, element := range context.Elements { 246 if flag, ok := element.Clause.(*FlagClause); ok && flag == a.HelpFlag { 247 // Re-parse the command-line ignoring defaults, so that help works correctly. 248 context, _ = a.parseContext(true, context.rawArgs) 249 a.writeUsage(context, nil) 250 } 251 } 252} 253 254// Version adds a --version flag for displaying the application version. 255func (a *Application) Version(version string) *Application { 256 a.version = version 257 a.VersionFlag = a.Flag("version", "Show application version.").PreAction(func(*ParseContext) error { 258 fmt.Fprintln(a.usageWriter, version) 259 a.terminate(0) 260 return nil 261 }) 262 a.VersionFlag.Bool() 263 return a 264} 265 266// Author sets the author output by some help templates. 267func (a *Application) Author(author string) *Application { 268 a.author = author 269 return a 270} 271 272// Action callback to call when all values are populated and parsing is 273// complete, but before any command, flag or argument actions. 274// 275// All Action() callbacks are called in the order they are encountered on the 276// command line. 277func (a *Application) Action(action Action) *Application { 278 a.addAction(action) 279 return a 280} 281 282// Action called after parsing completes but before validation and execution. 283func (a *Application) PreAction(action Action) *Application { 284 a.addPreAction(action) 285 return a 286} 287 288// Command adds a new top-level command. 289func (a *Application) Command(name, help string) *CmdClause { 290 return a.addCommand(name, help) 291} 292 293// Interspersed control if flags can be interspersed with positional arguments 294// 295// true (the default) means that they can, false means that all the flags must appear before the first positional arguments. 296func (a *Application) Interspersed(interspersed bool) *Application { 297 a.noInterspersed = !interspersed 298 return a 299} 300 301func (a *Application) defaultEnvarPrefix() string { 302 if a.defaultEnvars { 303 return a.Name 304 } 305 return "" 306} 307 308func (a *Application) init() error { 309 if a.initialized { 310 return nil 311 } 312 if a.cmdGroup.have() && a.argGroup.have() { 313 return fmt.Errorf("can't mix top-level Arg()s with Command()s") 314 } 315 316 // If we have subcommands, add a help command at the top-level. 317 if a.cmdGroup.have() { 318 var command []string 319 a.HelpCommand = a.Command("help", "Show help.").PreAction(func(context *ParseContext) error { 320 a.Usage(command) 321 a.terminate(0) 322 return nil 323 }) 324 a.HelpCommand.Arg("command", "Show help on command.").StringsVar(&command) 325 // Make help first command. 326 l := len(a.commandOrder) 327 a.commandOrder = append(a.commandOrder[l-1:l], a.commandOrder[:l-1]...) 328 } 329 330 if err := a.flagGroup.init(a.defaultEnvarPrefix()); err != nil { 331 return err 332 } 333 if err := a.cmdGroup.init(); err != nil { 334 return err 335 } 336 if err := a.argGroup.init(); err != nil { 337 return err 338 } 339 for _, cmd := range a.commands { 340 if err := cmd.init(); err != nil { 341 return err 342 } 343 } 344 flagGroups := []*flagGroup{a.flagGroup} 345 for _, cmd := range a.commandOrder { 346 if err := checkDuplicateFlags(cmd, flagGroups); err != nil { 347 return err 348 } 349 } 350 a.initialized = true 351 return nil 352} 353 354// Recursively check commands for duplicate flags. 355func checkDuplicateFlags(current *CmdClause, flagGroups []*flagGroup) error { 356 // Check for duplicates. 357 for _, flags := range flagGroups { 358 for _, flag := range current.flagOrder { 359 if flag.shorthand != 0 { 360 if _, ok := flags.short[string(flag.shorthand)]; ok { 361 return fmt.Errorf("duplicate short flag -%c", flag.shorthand) 362 } 363 } 364 if _, ok := flags.long[flag.name]; ok { 365 return fmt.Errorf("duplicate long flag --%s", flag.name) 366 } 367 } 368 } 369 flagGroups = append(flagGroups, current.flagGroup) 370 // Check subcommands. 371 for _, subcmd := range current.commandOrder { 372 if err := checkDuplicateFlags(subcmd, flagGroups); err != nil { 373 return err 374 } 375 } 376 return nil 377} 378 379func (a *Application) execute(context *ParseContext, selected []string) (string, error) { 380 var err error 381 382 if err = a.validateRequired(context); err != nil { 383 return "", err 384 } 385 386 if err = a.applyValidators(context); err != nil { 387 return "", err 388 } 389 390 if err = a.applyActions(context); err != nil { 391 return "", err 392 } 393 394 command := strings.Join(selected, " ") 395 if command == "" && a.cmdGroup.have() { 396 return "", ErrCommandNotSpecified 397 } 398 return command, err 399} 400 401func (a *Application) setDefaults(context *ParseContext) error { 402 flagElements := map[string]*ParseElement{} 403 for _, element := range context.Elements { 404 if flag, ok := element.Clause.(*FlagClause); ok { 405 if flag.name == "help" { 406 return nil 407 } 408 flagElements[flag.name] = element 409 } 410 } 411 412 argElements := map[string]*ParseElement{} 413 for _, element := range context.Elements { 414 if arg, ok := element.Clause.(*ArgClause); ok { 415 argElements[arg.name] = element 416 } 417 } 418 419 // Check required flags and set defaults. 420 for _, flag := range context.flags.long { 421 if flagElements[flag.name] == nil { 422 if err := flag.setDefault(); err != nil { 423 return err 424 } 425 } 426 } 427 428 for _, arg := range context.arguments.args { 429 if argElements[arg.name] == nil { 430 if err := arg.setDefault(); err != nil { 431 return err 432 } 433 } 434 } 435 436 return nil 437} 438 439func (a *Application) validateRequired(context *ParseContext) error { 440 flagElements := map[string]*ParseElement{} 441 for _, element := range context.Elements { 442 if flag, ok := element.Clause.(*FlagClause); ok { 443 flagElements[flag.name] = element 444 } 445 } 446 447 argElements := map[string]*ParseElement{} 448 for _, element := range context.Elements { 449 if arg, ok := element.Clause.(*ArgClause); ok { 450 argElements[arg.name] = element 451 } 452 } 453 454 // Check required flags and set defaults. 455 for _, flag := range context.flags.long { 456 if flagElements[flag.name] == nil { 457 // Check required flags were provided. 458 if flag.needsValue() { 459 return fmt.Errorf("required flag --%s not provided", flag.name) 460 } 461 } 462 } 463 464 for _, arg := range context.arguments.args { 465 if argElements[arg.name] == nil { 466 if arg.needsValue() { 467 return fmt.Errorf("required argument '%s' not provided", arg.name) 468 } 469 } 470 } 471 return nil 472} 473 474func (a *Application) setValues(context *ParseContext) (selected []string, err error) { 475 // Set all arg and flag values. 476 var ( 477 lastCmd *CmdClause 478 flagSet = map[string]struct{}{} 479 ) 480 for _, element := range context.Elements { 481 switch clause := element.Clause.(type) { 482 case *FlagClause: 483 if _, ok := flagSet[clause.name]; ok { 484 if v, ok := clause.value.(repeatableFlag); !ok || !v.IsCumulative() { 485 return nil, fmt.Errorf("flag '%s' cannot be repeated", clause.name) 486 } 487 } 488 if err = clause.value.Set(*element.Value); err != nil { 489 return 490 } 491 flagSet[clause.name] = struct{}{} 492 493 case *ArgClause: 494 if err = clause.value.Set(*element.Value); err != nil { 495 return 496 } 497 498 case *CmdClause: 499 if clause.validator != nil { 500 if err = clause.validator(clause); err != nil { 501 return 502 } 503 } 504 selected = append(selected, clause.name) 505 lastCmd = clause 506 } 507 } 508 509 if lastCmd != nil && len(lastCmd.commands) > 0 { 510 return nil, fmt.Errorf("must select a subcommand of '%s'", lastCmd.FullCommand()) 511 } 512 513 return 514} 515 516func (a *Application) applyValidators(context *ParseContext) (err error) { 517 // Call command validation functions. 518 for _, element := range context.Elements { 519 if cmd, ok := element.Clause.(*CmdClause); ok && cmd.validator != nil { 520 if err = cmd.validator(cmd); err != nil { 521 return err 522 } 523 } 524 } 525 526 if a.validator != nil { 527 err = a.validator(a) 528 } 529 return err 530} 531 532func (a *Application) applyPreActions(context *ParseContext, dispatch bool) error { 533 if err := a.actionMixin.applyPreActions(context); err != nil { 534 return err 535 } 536 // Dispatch to actions. 537 if dispatch { 538 for _, element := range context.Elements { 539 if applier, ok := element.Clause.(actionApplier); ok { 540 if err := applier.applyPreActions(context); err != nil { 541 return err 542 } 543 } 544 } 545 } 546 547 return nil 548} 549 550func (a *Application) applyActions(context *ParseContext) error { 551 if err := a.actionMixin.applyActions(context); err != nil { 552 return err 553 } 554 // Dispatch to actions. 555 for _, element := range context.Elements { 556 if applier, ok := element.Clause.(actionApplier); ok { 557 if err := applier.applyActions(context); err != nil { 558 return err 559 } 560 } 561 } 562 return nil 563} 564 565// Errorf prints an error message to w in the format "<appname>: error: <message>". 566func (a *Application) Errorf(format string, args ...interface{}) { 567 fmt.Fprintf(a.errorWriter, a.Name+": error: "+format+"\n", args...) 568} 569 570// Fatalf writes a formatted error to w then terminates with exit status 1. 571func (a *Application) Fatalf(format string, args ...interface{}) { 572 a.Errorf(format, args...) 573 a.terminate(1) 574} 575 576// FatalUsage prints an error message followed by usage information, then 577// exits with a non-zero status. 578func (a *Application) FatalUsage(format string, args ...interface{}) { 579 a.Errorf(format, args...) 580 // Force usage to go to error output. 581 a.usageWriter = a.errorWriter 582 a.Usage([]string{}) 583 a.terminate(1) 584} 585 586// FatalUsageContext writes a printf formatted error message to w, then usage 587// information for the given ParseContext, before exiting. 588func (a *Application) FatalUsageContext(context *ParseContext, format string, args ...interface{}) { 589 a.Errorf(format, args...) 590 if err := a.UsageForContext(context); err != nil { 591 panic(err) 592 } 593 a.terminate(1) 594} 595 596// FatalIfError prints an error and exits if err is not nil. The error is printed 597// with the given formatted string, if any. 598func (a *Application) FatalIfError(err error, format string, args ...interface{}) { 599 if err != nil { 600 prefix := "" 601 if format != "" { 602 prefix = fmt.Sprintf(format, args...) + ": " 603 } 604 a.Errorf(prefix+"%s", err) 605 a.terminate(1) 606 } 607} 608 609func (a *Application) completionOptions(context *ParseContext) []string { 610 args := context.rawArgs 611 612 var ( 613 currArg string 614 prevArg string 615 target cmdMixin 616 ) 617 618 numArgs := len(args) 619 if numArgs > 1 { 620 args = args[1:] 621 currArg = args[len(args)-1] 622 } 623 if numArgs > 2 { 624 prevArg = args[len(args)-2] 625 } 626 627 target = a.cmdMixin 628 if context.SelectedCommand != nil { 629 // A subcommand was in use. We will use it as the target 630 target = context.SelectedCommand.cmdMixin 631 } 632 633 if (currArg != "" && strings.HasPrefix(currArg, "--")) || strings.HasPrefix(prevArg, "--") { 634 // Perform completion for A flag. The last/current argument started with "-" 635 var ( 636 flagName string // The name of a flag if given (could be half complete) 637 flagValue string // The value assigned to a flag (if given) (could be half complete) 638 ) 639 640 if strings.HasPrefix(prevArg, "--") && !strings.HasPrefix(currArg, "--") { 641 // Matches: ./myApp --flag value 642 // Wont Match: ./myApp --flag -- 643 flagName = prevArg[2:] // Strip the "--" 644 flagValue = currArg 645 } else if strings.HasPrefix(currArg, "--") { 646 // Matches: ./myApp --flag -- 647 // Matches: ./myApp --flag somevalue -- 648 // Matches: ./myApp -- 649 flagName = currArg[2:] // Strip the "--" 650 } 651 652 options, flagMatched, valueMatched := target.FlagCompletion(flagName, flagValue) 653 if valueMatched { 654 // Value Matched. Show cmdCompletions 655 return target.CmdCompletion(context) 656 } 657 658 // Add top level flags if we're not at the top level and no match was found. 659 if context.SelectedCommand != nil && !flagMatched { 660 topOptions, topFlagMatched, topValueMatched := a.FlagCompletion(flagName, flagValue) 661 if topValueMatched { 662 // Value Matched. Back to cmdCompletions 663 return target.CmdCompletion(context) 664 } 665 666 if topFlagMatched { 667 // Top level had a flag which matched the input. Return it's options. 668 options = topOptions 669 } else { 670 // Add top level flags 671 options = append(options, topOptions...) 672 } 673 } 674 return options 675 } 676 677 // Perform completion for sub commands and arguments. 678 return target.CmdCompletion(context) 679} 680 681func (a *Application) generateBashCompletion(context *ParseContext) { 682 options := a.completionOptions(context) 683 fmt.Printf("%s", strings.Join(options, "\n")) 684} 685 686func envarTransform(name string) string { 687 return strings.ToUpper(envarTransformRegexp.ReplaceAllString(name, "_")) 688} 689