package kingpin import ( "fmt" "strings" ) type cmdMixin struct { *flagGroup *argGroup *cmdGroup actionMixin } // CmdCompletion returns completion options for arguments, if that's where // parsing left off, or commands if there aren't any unsatisfied args. func (c *cmdMixin) CmdCompletion(context *ParseContext) []string { var options []string // Count args already satisfied - we won't complete those, and add any // default commands' alternatives, since they weren't listed explicitly // and the user may want to explicitly list something else. argsSatisfied := 0 allSatisfied := false ElementLoop: for _, el := range context.Elements { switch clause := el.Clause.(type) { case *ArgClause: // Each new element should reset the previous state allSatisfied = false options = nil if el.Value != nil && *el.Value != "" { // Get the list of valid options for the last argument validOptions := c.argGroup.args[argsSatisfied].resolveCompletions() if len(validOptions) == 0 { // If there are no options for this argument, // mark is as allSatisfied as we can't suggest anything if !clause.consumesRemainder() { argsSatisfied++ allSatisfied = true } continue ElementLoop } for _, opt := range validOptions { if opt == *el.Value { // We have an exact match // We don't need to suggest any option if !clause.consumesRemainder() { argsSatisfied++ } continue ElementLoop } if strings.HasPrefix(opt, *el.Value) { // If the option match the partially entered argument, add it to the list options = append(options, opt) } } // Avoid further completion as we have done everything we could if !clause.consumesRemainder() { argsSatisfied++ allSatisfied = true } } case *CmdClause: options = append(options, clause.completionAlts...) default: } } if argsSatisfied < len(c.argGroup.args) && !allSatisfied { // Since not all args have been satisfied, show options for the current one options = append(options, c.argGroup.args[argsSatisfied].resolveCompletions()...) } else { // If all args are satisfied, then go back to completing commands for _, cmd := range c.cmdGroup.commandOrder { if !cmd.hidden { options = append(options, cmd.name) } } } return options } func (c *cmdMixin) FlagCompletion(flagName string, flagValue string) (choices []string, flagMatch bool, optionMatch bool) { // Check if flagName matches a known flag. // If it does, show the options for the flag // Otherwise, show all flags options := []string{} for _, flag := range c.flagGroup.flagOrder { // Loop through each flag and determine if a match exists if flag.name == flagName { // User typed entire flag. Need to look for flag options. options = flag.resolveCompletions() if len(options) == 0 { // No Options to Choose From, Assume Match. return options, true, true } // Loop options to find if the user specified value matches isPrefix := false matched := false for _, opt := range options { if flagValue == opt { matched = true } else if strings.HasPrefix(opt, flagValue) { isPrefix = true } } // Matched Flag Directly // Flag Value Not Prefixed, and Matched Directly return options, true, !isPrefix && matched } if !flag.hidden { options = append(options, "--"+flag.name) } } // No Flag directly matched. return options, false, false } type cmdGroup struct { app *Application parent *CmdClause commands map[string]*CmdClause commandOrder []*CmdClause } func (c *cmdGroup) defaultSubcommand() *CmdClause { for _, cmd := range c.commandOrder { if cmd.isDefault { return cmd } } return nil } func (c *cmdGroup) cmdNames() []string { names := make([]string, 0, len(c.commandOrder)) for _, cmd := range c.commandOrder { names = append(names, cmd.name) } return names } // GetArg gets a command definition. // // This allows existing commands to be modified after definition but before parsing. Useful for // modular applications. func (c *cmdGroup) GetCommand(name string) *CmdClause { return c.commands[name] } func newCmdGroup(app *Application) *cmdGroup { return &cmdGroup{ app: app, commands: make(map[string]*CmdClause), } } func (c *cmdGroup) flattenedCommands() (out []*CmdClause) { for _, cmd := range c.commandOrder { if len(cmd.commands) == 0 { out = append(out, cmd) } out = append(out, cmd.flattenedCommands()...) } return } func (c *cmdGroup) addCommand(name, help string) *CmdClause { cmd := newCommand(c.app, name, help) c.commands[name] = cmd c.commandOrder = append(c.commandOrder, cmd) return cmd } func (c *cmdGroup) init() error { seen := map[string]bool{} if c.defaultSubcommand() != nil && !c.have() { return fmt.Errorf("default subcommand %q provided but no subcommands defined", c.defaultSubcommand().name) } defaults := []string{} for _, cmd := range c.commandOrder { if cmd.isDefault { defaults = append(defaults, cmd.name) } if seen[cmd.name] { return fmt.Errorf("duplicate command %q", cmd.name) } seen[cmd.name] = true for _, alias := range cmd.aliases { if seen[alias] { return fmt.Errorf("alias duplicates existing command %q", alias) } c.commands[alias] = cmd } if err := cmd.init(); err != nil { return err } } if len(defaults) > 1 { return fmt.Errorf("more than one default subcommand exists: %s", strings.Join(defaults, ", ")) } return nil } func (c *cmdGroup) have() bool { return len(c.commands) > 0 } type CmdClauseValidator func(*CmdClause) error // A CmdClause is a single top-level command. It encapsulates a set of flags // and either subcommands or positional arguments. type CmdClause struct { cmdMixin app *Application name string aliases []string help string helpLong string isDefault bool validator CmdClauseValidator hidden bool completionAlts []string } func newCommand(app *Application, name, help string) *CmdClause { c := &CmdClause{ app: app, name: name, help: help, } c.flagGroup = newFlagGroup() c.argGroup = newArgGroup() c.cmdGroup = newCmdGroup(app) return c } // Add an Alias for this command. func (c *CmdClause) Alias(name string) *CmdClause { c.aliases = append(c.aliases, name) return c } // Validate sets a validation function to run when parsing. func (c *CmdClause) Validate(validator CmdClauseValidator) *CmdClause { c.validator = validator return c } func (c *CmdClause) FullCommand() string { out := []string{c.name} for p := c.parent; p != nil; p = p.parent { out = append([]string{p.name}, out...) } return strings.Join(out, " ") } // Command adds a new sub-command. func (c *CmdClause) Command(name, help string) *CmdClause { cmd := c.addCommand(name, help) cmd.parent = c return cmd } // Default makes this command the default if commands don't match. func (c *CmdClause) Default() *CmdClause { c.isDefault = true return c } func (c *CmdClause) Action(action Action) *CmdClause { c.addAction(action) return c } func (c *CmdClause) PreAction(action Action) *CmdClause { c.addPreAction(action) return c } // Help sets the help message. func (c *CmdClause) Help(help string) *CmdClause { c.help = help return c } func (c *CmdClause) init() error { if err := c.flagGroup.init(c.app.defaultEnvarPrefix()); err != nil { return err } if c.argGroup.have() && c.cmdGroup.have() { return fmt.Errorf("can't mix Arg()s with Command()s") } if err := c.argGroup.init(); err != nil { return err } if err := c.cmdGroup.init(); err != nil { return err } return nil } func (c *CmdClause) Hidden() *CmdClause { c.hidden = true return c } // HelpLong adds a long help text, which can be used in usage templates. // For example, to use a longer help text in the command-specific help // than in the apps root help. func (c *CmdClause) HelpLong(help string) *CmdClause { c.helpLong = help return c }