package kingpin import ( "fmt" "io" "os" "regexp" "strings" ) var ( errCommandNotSpecified = TError("command not specified") envarTransformRegexp = regexp.MustCompile(`[^a-zA-Z_]+`) ) // 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 output io.Writer // Destination for usage. errors io.Writer terminate func(status int) // See Terminate() noInterspersed bool // can flags be interspersed with args (or must they come first) defaultEnvars bool completion bool helpFlag *Clause helpCommand *CmdClause defaultUsage *UsageContext } // New creates a new Kingpin application instance. func New(name, help string) *Application { a := &Application{ Name: name, Help: help, output: os.Stdout, errors: os.Stderr, terminate: os.Exit, defaultUsage: &UsageContext{ Template: DefaultUsageTemplate, }, } a.flagGroup = newFlagGroup() a.argGroup = newArgGroup() a.cmdGroup = newCmdGroup(a) a.helpFlag = a.Flag("help", T("Show context-sensitive help.")).Action(func(a *Application, e *ParseElement, c *ParseContext) error { a.UsageForContext(c) a.terminate(0) return nil }) a.helpFlag.Bool() a.Flag("completion-bash", T("Output possible completions for the given args.")).Hidden().BoolVar(&a.completion) a.Flag("completion-script-bash", T("Generate completion script for bash.")).Hidden().PreAction(a.generateBashCompletionScript).Bool() a.Flag("completion-script-zsh", T("Generate completion script for ZSH.")).Hidden().PreAction(a.generateZSHCompletionScript).Bool() return a } // Struct allows applications to define flags with struct tags. // // Supported struct tags are: help, placeholder, default, short, long, required, hidden, env, // enum, and arg. // // The name of the flag will default to the CamelCase name transformed to camel-case. This can // be overridden with the "long" tag. // // All basic Go types are supported including floats, ints, strings, time.Duration, // and slices of same. // // For compatibility, also supports the tags used by https://github.com/jessevdk/go-flags func (a *Application) Struct(v interface{}) error { return a.fromStruct(nil, v) } func (a *Application) generateBashCompletionScript(_ *Application, e *ParseElement, c *ParseContext) error { usageContext := &UsageContext{ Template: BashCompletionTemplate, } a.Writers(os.Stdout, os.Stderr) if err := a.UsageForContextWithTemplate(usageContext, c); err != nil { return err } a.terminate(0) return nil } func (a *Application) generateZSHCompletionScript(_ *Application, e *ParseElement, c *ParseContext) error { usageContext := &UsageContext{ Template: ZshCompletionTemplate, } a.Writers(os.Stdout, os.Stderr) if err := a.UsageForContextWithTemplate(usageContext, c); err != nil { return err } a.terminate(0) return nil } // Action is an application-wide callback. It is used in two situations: first, with a nil "element" // parameter when parsing is complete, and second whenever a command, argument or flag is // encountered. func (a *Application) Action(action Action) *Application { a.addAction(action) return a } // PreAction is an application-wide callback. It is in two situations: first, with a nil "element" // parameter, and second, whenever a command, argument or flag is encountered. func (a *Application) PreAction(action Action) *Application { a.addPreAction(action) return a } // 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. func (a *Application) Writers(out, err io.Writer) *Application { a.output = out a.errors = err return a } // UsageTemplate specifies the text template to use when displaying usage // information via --help. The default is DefaultUsageTemplate. func (a *Application) UsageTemplate(template string) *Application { a.defaultUsage.Template = template return a } // UsageContext specifies the UsageContext to use when displaying usage // information via --help. func (a *Application) UsageContext(context *UsageContext) *Application { a.defaultUsage = context 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) 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 "", TError("unexpected argument '{{.Arg0}}'", V{"Arg0": 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) } a.terminate(1) } func (a *Application) maybeHelp(context *ParseContext) { for _, element := range context.Elements { if element.OneOf.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.Flag("version", T("Show application version.")). PreAction(func(*Application, *ParseElement, *ParseContext) error { fmt.Fprintln(a.output, version) a.terminate(0) return nil }). Bool() return a } // Author sets the author name for usage templates. func (a *Application) Author(author string) *Application { a.author = author 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 err := a.checkArgCommandMixing(); err != nil { return err } // If we have subcommands, add a help command at the top-level. if a.cmdGroup.have() { var command []string a.helpCommand = a.Command("help", T("Show help.")). PreAction(func(_ *Application, element *ParseElement, context *ParseContext) error { a.Usage(command) command = []string{} a.terminate(0) return nil }) a.helpCommand. Arg("command", T("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 TError("duplicate short flag -{{.Arg0}}", V{"Arg0": flag.shorthand}) } } if _, ok := flags.long[flag.name]; ok { return TError("duplicate long flag --{{.Arg0}}", V{"Arg0": 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.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 := context.Elements.FlagMap() argElements := context.Elements.ArgMap() // 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 } } else { flag.reset() } } for _, arg := range context.arguments.args { if argElements[arg.name] == nil { if err := arg.setDefault(); err != nil { return err } } else { arg.reset() } } return nil } func (a *Application) validateRequired(context *ParseContext) error { flagElements := context.Elements.FlagMap() argElements := context.Elements.ArgMap() // 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 TError("required flag --{{.Arg0}} not provided", V{"Arg0": flag.name}) } } } for _, arg := range context.arguments.args { if argElements[arg.name] == nil { if arg.needsValue() { return TError("required argument '{{.Arg0}}' not provided", V{"Arg0": 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 { case element.OneOf.Flag != nil: clause := element.OneOf.Flag if _, ok := flagSet[clause.name]; ok { if v, ok := clause.value.(cumulativeValue); !ok || !v.IsCumulative() { return nil, TError("flag '{{.Arg0}}' cannot be repeated", V{"Arg0": clause.name}) } } if err = clause.value.Set(*element.Value); err != nil { return } flagSet[clause.name] = struct{}{} case element.OneOf.Arg != nil: clause := element.OneOf.Arg if err = clause.value.Set(*element.Value); err != nil { return } case element.OneOf.Cmd != nil: clause := element.OneOf.Cmd if clause.validator != nil { if err = clause.validator(clause); err != nil { return } } selected = append(selected, clause.name) lastCmd = clause } } if lastCmd == nil || lastCmd.optionalSubcommands { return } if len(lastCmd.commands) > 0 { return nil, TError("must select a subcommand of '{{.Arg0}}'", V{"Arg0": lastCmd.FullCommand()}) } return } // Errorf prints an error message to w in the format ": error: ". func (a *Application) Errorf(format string, args ...interface{}) { fmt.Fprintf(a.errors, a.Name+T(": 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...) 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 (a *Application) applyPreActions(context *ParseContext, dispatch bool) error { if !dispatch { return nil } if err := a.actionMixin.applyPreActions(a, nil, context); err != nil { return err } for _, element := range context.Elements { if err := a.actionMixin.applyPreActions(a, element, context); err != nil { return err } var applier actionApplier switch { case element.OneOf.Arg != nil: applier = element.OneOf.Arg case element.OneOf.Flag != nil: applier = element.OneOf.Flag case element.OneOf.Cmd != nil: applier = element.OneOf.Cmd } if err := applier.applyPreActions(a, element, context); err != nil { return err } } return nil } func (a *Application) applyActions(context *ParseContext) error { if err := a.actionMixin.applyActions(a, nil, context); err != nil { return err } // Dispatch to actions. for _, element := range context.Elements { if err := a.actionMixin.applyActions(a, element, context); err != nil { return err } var applier actionApplier switch { case element.OneOf.Arg != nil: applier = element.OneOf.Arg case element.OneOf.Flag != nil: applier = element.OneOf.Flag case element.OneOf.Cmd != nil: applier = element.OneOf.Cmd } if err := applier.applyActions(a, element, context); err != nil { return err } } return nil } func envarTransform(name string) string { return strings.ToUpper(envarTransformRegexp.ReplaceAllString(name, "_")) }