diff --git a/command.go b/command.go index 2662843..2c3acef 100644 --- a/command.go +++ b/command.go @@ -8,6 +8,8 @@ import ( "unsafe" ) +const DefaultCommandType = "command" + // Command represents an application command. Commands can be added to the // parser (which itself is a command) and are selected/executed when its name // is specified on the command line. The Command type embeds a Group and @@ -19,6 +21,9 @@ type Command struct { // The name by which the command can be invoked Name string + // Commands can be classified by types + Type string + // The active sub command (set by parsing) or nil Active *Command @@ -150,6 +155,7 @@ func newCommand(name string, shortDescription string, longDescription string, da return &Command{ Group: newGroup(shortDescription, longDescription, data), Name: name, + Type: DefaultCommandType, } } @@ -395,6 +401,20 @@ func (c commandList) Swap(i, j int) { c[i], c[j] = c[j], c[i] } +// selectedVisibleCommands returns a list of commands that are of type 't' and visible. +// if t is empty, it returns all visible commands +func (c *Command) selectedVisibleCommands(t string) []*Command { + cmdList := commandList(c.sortedVisibleCommands()) + var ret commandList + for _, cmd := range cmdList { + if t != "" && cmd.Type != t { + continue + } + ret = append(ret, cmd) + } + return []*Command(ret) +} + func (c *Command) sortedVisibleCommands() []*Command { ret := commandList(c.visibleCommands()) sort.Sort(ret) diff --git a/completion.go b/completion.go index 6a0432b..4a8862a 100644 --- a/completion.go +++ b/completion.go @@ -126,8 +126,8 @@ func (c *completion) completeValue(value reflect.Value, prefix string, match str i := value.Interface() var ret []Completion - if cmp, ok := i.(Completer); ok { + ret = cmp.Complete(match) } else if value.CanAddr() { if cmp, ok = value.Addr().Interface().(Completer); ok { diff --git a/example_test.go b/example_test.go index 4321ed8..33764e5 100644 --- a/example_test.go +++ b/example_test.go @@ -39,6 +39,9 @@ func Example() { // Example of a filename (useful for completion) Filename Filename `long:"filename" description:"A filename"` + //// Example of an interface + //Interface interface{} + // Example of positional arguments Args struct { ID string @@ -56,6 +59,11 @@ func Example() { cmd.Process.Release() } + //// Set the value of interface + //opts.Interface = struct { + // Sub string `long:"sub-field" description:"An embedded interface field"` + //}{} + // Make some fake arguments to parse. args := []string{ "-vv", @@ -69,6 +77,7 @@ func Example() { "--intmap", "a:1", "--intmap", "b:5", "--filename", "hello.go", + //"--sub-field", "hello", "id", "10", "remaining1", @@ -92,6 +101,7 @@ func Example() { fmt.Printf("PtrSlice: [%v %v]\n", *opts.PtrSlice[0], *opts.PtrSlice[1]) fmt.Printf("IntMap: [a:%v b:%v]\n", opts.IntMap["a"], opts.IntMap["b"]) fmt.Printf("Filename: %v\n", opts.Filename) + //fmt.Printf("Interface: %+v\n", opts.Interface) fmt.Printf("Args.ID: %s\n", opts.Args.ID) fmt.Printf("Args.Num: %d\n", opts.Args.Num) fmt.Printf("Args.Rest: %v\n", opts.Args.Rest) diff --git a/group.go b/group.go index 6133a71..92aff0b 100644 --- a/group.go +++ b/group.go @@ -216,6 +216,21 @@ func (g *Group) scanStruct(realval reflect.Value, sfield *reflect.StructField, h if err := g.scanStruct(fld, &field, handler); err != nil { return err } + } else if kind == reflect.Interface { + if fld.IsNil() { + continue + } + v := reflect.ValueOf(fld.Interface()) + switch v.Kind() { + case reflect.Struct: + if err := g.scanStruct(v, &field, handler); err != nil { + return err + } + case reflect.Ptr: + if err := g.scanStruct(reflect.Indirect(v), &field, handler); err != nil { + return err + } + } } else if kind == reflect.Ptr && field.Type.Elem().Kind() == reflect.Struct { flagCountBefore := len(g.options) + len(g.groups) diff --git a/help.go b/help.go index d380305..20943bc 100644 --- a/help.go +++ b/help.go @@ -10,6 +10,7 @@ import ( "fmt" "io" "runtime" + "sort" "strings" "unicode/utf8" ) @@ -274,7 +275,7 @@ func maxCommandLength(s []*Command) int { // option provides a convenient way to add a -h/--help option group to the // command line parser which will automatically show the help messages using // this method. -func (p *Parser) WriteHelp(writer io.Writer) { +func (p *Parser) WriteHelp(writer io.Writer, plural func(s string) string) { if writer == nil { return } @@ -461,29 +462,36 @@ func (p *Parser) WriteHelp(writer io.Writer) { c = c.Active } + allTypes := make([]string, 0, len(p.AllTypes)) + for t, _ := range p.AllTypes { + allTypes = append(allTypes, t) + } + sort.Strings(allTypes) + for _, t := range allTypes { - scommands := cmd.sortedVisibleCommands() + scommands := cmd.selectedVisibleCommands(t) - if len(scommands) > 0 { - maxnamelen := maxCommandLength(scommands) + if len(scommands) > 0 { + maxnamelen := maxCommandLength(scommands) - fmt.Fprintln(wr) - fmt.Fprintln(wr, "Available commands:") + fmt.Fprintln(wr) + fmt.Fprintf(wr, "Available %s:\n", plural(t)) + + for _, c := range scommands { + fmt.Fprintf(wr, " %s", c.Name) - for _, c := range scommands { - fmt.Fprintf(wr, " %s", c.Name) + if len(c.ShortDescription) > 0 { + pad := strings.Repeat(" ", maxnamelen-len(c.Name)) + fmt.Fprintf(wr, "%s %s", pad, c.ShortDescription) - if len(c.ShortDescription) > 0 { - pad := strings.Repeat(" ", maxnamelen-len(c.Name)) - fmt.Fprintf(wr, "%s %s", pad, c.ShortDescription) + if len(c.Aliases) > 0 { + fmt.Fprintf(wr, " (aliases: %s)", strings.Join(c.Aliases, ", ")) + } - if len(c.Aliases) > 0 { - fmt.Fprintf(wr, " (aliases: %s)", strings.Join(c.Aliases, ", ")) } + fmt.Fprintln(wr) } - - fmt.Fprintln(wr) } } diff --git a/parser.go b/parser.go index fd2fd5f..193b621 100644 --- a/parser.go +++ b/parser.go @@ -20,6 +20,9 @@ type Parser struct { // Embedded, see Command for more information *Command + // Histogram of valid Command types + AllTypes map[string]int + // A usage string to be displayed in the help message. Usage string @@ -52,6 +55,10 @@ type Parser struct { // command to be executed when parsing has finished. CommandHandler func(command Commander, args []string) error + // PluralHandler is a function gets called to handle the formation of + // plurals in the help screen. If not specified, it just appends an 's'. + PluralHandler func(s string) string + internalError error } @@ -172,20 +179,31 @@ func NewNamedParser(appname string, options Options) *Parser { p := &Parser{ Command: newCommand(appname, "", "", nil), Options: options, + AllTypes: make(map[string]int), NamespaceDelimiter: ".", } + p.AddType(DefaultCommandType) p.Command.parent = p return p } +func (p *Parser) AddType(s string) { + p.AllTypes[s]++ +} + // Parse parses the command line arguments from os.Args using Parser.ParseArgs. // For more detailed information see ParseArgs. func (p *Parser) Parse() ([]string, error) { return p.ParseArgs(os.Args[1:]) } +func (p *Parser) Complete(match []string) []Completion { + comp := &completion{parser: p} + return comp.complete(match) +} + // ParseArgs parses the command line arguments according to the option groups that // were added to the parser. On successful parsing of the arguments, the // remaining, non-option, arguments (if any) are returned. The returned error @@ -671,7 +689,14 @@ func (p *Parser) parseNonOption(s *parseState) error { func (p *Parser) showBuiltinHelp() error { var b bytes.Buffer - p.WriteHelp(&b) + plural := func(s string) string { + return fmt.Sprintf("%ss", s) + } + if p.PluralHandler != nil { + plural = p.PluralHandler + } + + p.WriteHelp(&b, plural) return newError(ErrHelp, b.String()) }