package kingpin import ( "fmt" "io" "os" "regexp" "strings" ) var ( ErrCommandNotSpecified = fmt.Errorf("command not specified") ) var ( envarTransformRegexp = regexp.MustCompile(`[^a-zA-Z0-9_]+`) ) type ApplicationValidator func(*Application) error // An Application contains the definitions of flags, arguments and commands // for an application. type Application struct { cmdMixin initialized bool Name string Help string author string version string errorWriter io.Writer // Destination for errors. usageWriter io.Writer // Destination for usage usageTemplate string validator ApplicationValidator terminate func(status int) // See Terminate() noInterspersed bool // can flags be interspersed with args (or must they come first) defaultEnvars bool completion bool // Help flag. Exposed for user customisation. HelpFlag *FlagClause // Help command. Exposed for user customisation. May be nil. HelpCommand *CmdClause // Version flag. Exposed for user customisation. May be nil. VersionFlag *FlagClause } // New creates a new Kingpin application instance. func New(name, help string) *Application { a := &Application{ Name: name, Help: help, errorWriter: os.Stderr, // Left for backwards compatibility purposes. usageWriter: os.Stderr, usageTemplate: DefaultUsageTemplate, terminate: os.Exit, } a.flagGroup = newFlagGroup() a.argGroup = newArgGroup() a.cmdGroup = newCmdGroup(a) a.HelpFlag = a.Flag("help", "Show context-sensitive help (also try --help-long and --help-man).") a.HelpFlag.Bool() a.Flag("help-long", "Generate long help.").Hidden().PreAction(a.generateLongHelp).Bool() a.Flag("help-man", "Generate a man page.").Hidden().PreAction(a.generateManPage).Bool() a.Flag("completion-bash", "Output possible completions for the given args.").Hidden().BoolVar(&a.completion) a.Flag("completion-script-bash", "Generate completion script for bash.").Hidden().PreAction(a.generateBashCompletionScript).Bool() a.Flag("completion-script-zsh", "Generate completion script for ZSH.").Hidden().PreAction(a.generateZSHCompletionScript).Bool() return a } func (a *Application) generateLongHelp(c *ParseContext) error { a.Writer(os.Stdout) if err := a.UsageForContextWithTemplate(c, 2, LongHelpTemplate); err != nil { return err } a.terminate(0) return nil } func (a *Application) generateManPage(c *ParseContext) error { a.Writer(os.Stdout) if err := a.UsageForContextWithTemplate(c, 2, ManPageTemplate); err != nil { return err } a.terminate(0) return nil } func (a *Application) generateBashCompletionScript(c *ParseContext) error { a.Writer(os.Stdout) if err := a.UsageForContextWithTemplate(c, 2, BashCompletionTemplate); err != nil { return err } a.terminate(0) return nil } func (a *Application) generateZSHCompletionScript(c *ParseContext) error { a.Writer(os.Stdout) if err := a.UsageForContextWithTemplate(c, 2, ZshCompletionTemplate); err != nil { return err } a.terminate(0) return nil } // DefaultEnvars configures all flags (that do not already have an associated // envar) to use a default environment variable in the form "_". // // For example, if the application is named "foo" and a flag is named "bar- // waz" the environment variable: "FOO_BAR_WAZ". func (a *Application) DefaultEnvars() *Application { a.defaultEnvars = true return a } // Terminate specifies the termination handler. Defaults to os.Exit(status). // If nil is passed, a no-op function will be used. func (a *Application) Terminate(terminate func(int)) *Application { if terminate == nil { terminate = func(int) {} } a.terminate = terminate return a } // Writer specifies the writer to use for usage and errors. Defaults to os.Stderr. // DEPRECATED: See ErrorWriter and UsageWriter. func (a *Application) Writer(w io.Writer) *Application { a.errorWriter = w a.usageWriter = w return a } // ErrorWriter sets the io.Writer to use for errors. func (a *Application) ErrorWriter(w io.Writer) *Application { a.usageWriter = w return a } // UsageWriter sets the io.Writer to use for errors. func (a *Application) UsageWriter(w io.Writer) *Application { a.usageWriter = w return a } // UsageTemplate specifies the text template to use when displaying usage // information. The default is UsageTemplate. func (a *Application) UsageTemplate(template string) *Application { a.usageTemplate = template return a } // Validate sets a validation function to run when parsing. func (a *Application) Validate(validator ApplicationValidator) *Application { a.validator = validator return a } // ParseContext parses the given command line and returns the fully populated // ParseContext. func (a *Application) ParseContext(args []string) (*ParseContext, error) { return a.parseContext(false, args) } func (a *Application) parseContext(ignoreDefault bool, args []string) (*ParseContext, error) { if err := a.init(); err != nil { return nil, err } context := tokenize(args, ignoreDefault) err := parse(context, a) return context, err } // Parse parses command-line arguments. It returns the selected command and an // error. The selected command will be a space separated subcommand, if // subcommands have been configured. // // This will populate all flag and argument values, call all callbacks, and so // on. func (a *Application) Parse(args []string) (command string, err error) { context, parseErr := a.ParseContext(args) selected := []string{} var setValuesErr error if context == nil { // Since we do not throw error immediately, there could be a case // where a context returns nil. Protect against that. return "", parseErr } if err = a.setDefaults(context); err != nil { return "", err } selected, setValuesErr = a.setValues(context) if err = a.applyPreActions(context, !a.completion); err != nil { return "", err } if a.completion { a.generateBashCompletion(context) a.terminate(0) } else { if parseErr != nil { return "", parseErr } a.maybeHelp(context) if !context.EOL() { return "", fmt.Errorf("unexpected argument '%s'", context.Peek()) } if setValuesErr != nil { return "", setValuesErr } command, err = a.execute(context, selected) if err == ErrCommandNotSpecified { a.writeUsage(context, nil) } } return command, err } func (a *Application) writeUsage(context *ParseContext, err error) { if err != nil { a.Errorf("%s", err) } if err := a.UsageForContext(context); err != nil { panic(err) } if err != nil { a.terminate(1) } else { a.terminate(0) } } func (a *Application) maybeHelp(context *ParseContext) { for _, element := range context.Elements { if flag, ok := element.Clause.(*FlagClause); ok && flag == a.HelpFlag { // Re-parse the command-line ignoring defaults, so that help works correctly. context, _ = a.parseContext(true, context.rawArgs) a.writeUsage(context, nil) } } } // Version adds a --version flag for displaying the application version. func (a *Application) Version(version string) *Application { a.version = version a.VersionFlag = a.Flag("version", "Show application version.").PreAction(func(*ParseContext) error { fmt.Fprintln(a.usageWriter, version) a.terminate(0) return nil }) a.VersionFlag.Bool() return a } // Author sets the author output by some help templates. func (a *Application) Author(author string) *Application { a.author = author return a } // Action callback to call when all values are populated and parsing is // complete, but before any command, flag or argument actions. // // All Action() callbacks are called in the order they are encountered on the // command line. func (a *Application) Action(action Action) *Application { a.addAction(action) return a } // Action called after parsing completes but before validation and execution. func (a *Application) PreAction(action Action) *Application { a.addPreAction(action) return a } // Command adds a new top-level command. func (a *Application) Command(name, help string) *CmdClause { return a.addCommand(name, help) } // Interspersed control if flags can be interspersed with positional arguments // // true (the default) means that they can, false means that all the flags must appear before the first positional arguments. func (a *Application) Interspersed(interspersed bool) *Application { a.noInterspersed = !interspersed return a } func (a *Application) defaultEnvarPrefix() string { if a.defaultEnvars { return a.Name } return "" } func (a *Application) init() error { if a.initialized { return nil } if a.cmdGroup.have() && a.argGroup.have() { return fmt.Errorf("can't mix top-level Arg()s with Command()s") } // If we have subcommands, add a help command at the top-level. if a.cmdGroup.have() { var command []string a.HelpCommand = a.Command("help", "Show help.").PreAction(func(context *ParseContext) error { a.Usage(command) a.terminate(0) return nil }) a.HelpCommand.Arg("command", "Show help on command.").StringsVar(&command) // Make help first command. l := len(a.commandOrder) a.commandOrder = append(a.commandOrder[l-1:l], a.commandOrder[:l-1]...) } if err := a.flagGroup.init(a.defaultEnvarPrefix()); err != nil { return err } if err := a.cmdGroup.init(); err != nil { return err } if err := a.argGroup.init(); err != nil { return err } for _, cmd := range a.commands { if err := cmd.init(); err != nil { return err } } flagGroups := []*flagGroup{a.flagGroup} for _, cmd := range a.commandOrder { if err := checkDuplicateFlags(cmd, flagGroups); err != nil { return err } } a.initialized = true return nil } // Recursively check commands for duplicate flags. func checkDuplicateFlags(current *CmdClause, flagGroups []*flagGroup) error { // Check for duplicates. for _, flags := range flagGroups { for _, flag := range current.flagOrder { if flag.shorthand != 0 { if _, ok := flags.short[string(flag.shorthand)]; ok { return fmt.Errorf("duplicate short flag -%c", flag.shorthand) } } if _, ok := flags.long[flag.name]; ok { return fmt.Errorf("duplicate long flag --%s", flag.name) } } } flagGroups = append(flagGroups, current.flagGroup) // Check subcommands. for _, subcmd := range current.commandOrder { if err := checkDuplicateFlags(subcmd, flagGroups); err != nil { return err } } return nil } func (a *Application) execute(context *ParseContext, selected []string) (string, error) { var err error if err = a.validateRequired(context); err != nil { return "", err } if err = a.applyValidators(context); err != nil { return "", err } if err = a.applyActions(context); err != nil { return "", err } command := strings.Join(selected, " ") if command == "" && a.cmdGroup.have() { return "", ErrCommandNotSpecified } return command, err } func (a *Application) setDefaults(context *ParseContext) error { flagElements := map[string]*ParseElement{} for _, element := range context.Elements { if flag, ok := element.Clause.(*FlagClause); ok { flagElements[flag.name] = element } } argElements := map[string]*ParseElement{} for _, element := range context.Elements { if arg, ok := element.Clause.(*ArgClause); ok { argElements[arg.name] = element } } // Check required flags and set defaults. for _, flag := range context.flags.long { if flagElements[flag.name] == nil { if err := flag.setDefault(); err != nil { return err } } } for _, arg := range context.arguments.args { if argElements[arg.name] == nil { if err := arg.setDefault(); err != nil { return err } } } return nil } func (a *Application) validateRequired(context *ParseContext) error { flagElements := map[string]*ParseElement{} for _, element := range context.Elements { if flag, ok := element.Clause.(*FlagClause); ok { flagElements[flag.name] = element } } argElements := map[string]*ParseElement{} for _, element := range context.Elements { if arg, ok := element.Clause.(*ArgClause); ok { argElements[arg.name] = element } } // Check required flags and set defaults. for _, flag := range context.flags.long { if flagElements[flag.name] == nil { // Check required flags were provided. if flag.needsValue() { return fmt.Errorf("required flag --%s not provided", flag.name) } } } for _, arg := range context.arguments.args { if argElements[arg.name] == nil { if arg.needsValue() { return fmt.Errorf("required argument '%s' not provided", arg.name) } } } return nil } func (a *Application) setValues(context *ParseContext) (selected []string, err error) { // Set all arg and flag values. var ( lastCmd *CmdClause flagSet = map[string]struct{}{} ) for _, element := range context.Elements { switch clause := element.Clause.(type) { case *FlagClause: if _, ok := flagSet[clause.name]; ok { if v, ok := clause.value.(repeatableFlag); !ok || !v.IsCumulative() { return nil, fmt.Errorf("flag '%s' cannot be repeated", clause.name) } } if err = clause.value.Set(*element.Value); err != nil { return } flagSet[clause.name] = struct{}{} case *ArgClause: if err = clause.value.Set(*element.Value); err != nil { return } case *CmdClause: if clause.validator != nil { if err = clause.validator(clause); err != nil { return } } selected = append(selected, clause.name) lastCmd = clause } } if lastCmd != nil && len(lastCmd.commands) > 0 { return nil, fmt.Errorf("must select a subcommand of '%s'", lastCmd.FullCommand()) } return } func (a *Application) applyValidators(context *ParseContext) (err error) { // Call command validation functions. for _, element := range context.Elements { if cmd, ok := element.Clause.(*CmdClause); ok && cmd.validator != nil { if err = cmd.validator(cmd); err != nil { return err } } } if a.validator != nil { err = a.validator(a) } return err } func (a *Application) applyPreActions(context *ParseContext, dispatch bool) error { if err := a.actionMixin.applyPreActions(context); err != nil { return err } // Dispatch to actions. if dispatch { for _, element := range context.Elements { if applier, ok := element.Clause.(actionApplier); ok { if err := applier.applyPreActions(context); err != nil { return err } } } } return nil } func (a *Application) applyActions(context *ParseContext) error { if err := a.actionMixin.applyActions(context); err != nil { return err } // Dispatch to actions. for _, element := range context.Elements { if applier, ok := element.Clause.(actionApplier); ok { if err := applier.applyActions(context); err != nil { return err } } } return nil } // Errorf prints an error message to w in the format ": error: ". func (a *Application) Errorf(format string, args ...interface{}) { fmt.Fprintf(a.errorWriter, a.Name+": error: "+format+"\n", args...) } // Fatalf writes a formatted error to w then terminates with exit status 1. func (a *Application) Fatalf(format string, args ...interface{}) { a.Errorf(format, args...) a.terminate(1) } // FatalUsage prints an error message followed by usage information, then // exits with a non-zero status. func (a *Application) FatalUsage(format string, args ...interface{}) { a.Errorf(format, args...) // Force usage to go to error output. a.usageWriter = a.errorWriter a.Usage([]string{}) a.terminate(1) } // FatalUsageContext writes a printf formatted error message to w, then usage // information for the given ParseContext, before exiting. func (a *Application) FatalUsageContext(context *ParseContext, format string, args ...interface{}) { a.Errorf(format, args...) if err := a.UsageForContext(context); err != nil { panic(err) } a.terminate(1) } // FatalIfError prints an error and exits if err is not nil. The error is printed // with the given formatted string, if any. func (a *Application) FatalIfError(err error, format string, args ...interface{}) { if err != nil { prefix := "" if format != "" { prefix = fmt.Sprintf(format, args...) + ": " } a.Errorf(prefix+"%s", err) a.terminate(1) } } func (a *Application) completionOptions(context *ParseContext) []string { args := context.rawArgs var ( currArg string prevArg string target cmdMixin ) numArgs := len(args) if numArgs > 1 { args = args[1:] currArg = args[len(args)-1] } if numArgs > 2 { prevArg = args[len(args)-2] } target = a.cmdMixin if context.SelectedCommand != nil { // A subcommand was in use. We will use it as the target target = context.SelectedCommand.cmdMixin } if (currArg != "" && strings.HasPrefix(currArg, "--")) || strings.HasPrefix(prevArg, "--") { // Perform completion for A flag. The last/current argument started with "-" var ( flagName string // The name of a flag if given (could be half complete) flagValue string // The value assigned to a flag (if given) (could be half complete) ) if strings.HasPrefix(prevArg, "--") && !strings.HasPrefix(currArg, "--") { // Matches: ./myApp --flag value // Wont Match: ./myApp --flag -- flagName = prevArg[2:] // Strip the "--" flagValue = currArg } else if strings.HasPrefix(currArg, "--") { // Matches: ./myApp --flag -- // Matches: ./myApp --flag somevalue -- // Matches: ./myApp -- flagName = currArg[2:] // Strip the "--" } options, flagMatched, valueMatched := target.FlagCompletion(flagName, flagValue) if valueMatched { // Value Matched. Show cmdCompletions return target.CmdCompletion(context) } // Add top level flags if we're not at the top level and no match was found. if context.SelectedCommand != nil && !flagMatched { topOptions, topFlagMatched, topValueMatched := a.FlagCompletion(flagName, flagValue) if topValueMatched { // Value Matched. Back to cmdCompletions return target.CmdCompletion(context) } if topFlagMatched { // Top level had a flag which matched the input. Return it's options. options = topOptions } else { // Add top level flags options = append(options, topOptions...) } } return options } // Perform completion for sub commands and arguments. return target.CmdCompletion(context) } func (a *Application) generateBashCompletion(context *ParseContext) { options := a.completionOptions(context) fmt.Printf("%s", strings.Join(options, "\n")) } func envarTransform(name string) string { return strings.ToUpper(envarTransformRegexp.ReplaceAllString(name, "_")) }