// Copyright 2012 Jesse van den Kieboom. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package flags import ( "errors" "reflect" "strings" "unicode/utf8" ) // ErrNotPointerToStruct indicates that a provided data container is not // a pointer to a struct. Only pointers to structs are valid data containers // for options. var ErrNotPointerToStruct = errors.New("provided data is not a pointer to struct") // Group represents an option group. Option groups can be used to logically // group options together under a description. Groups are only used to provide // more structure to options both for the user (as displayed in the help message) // and for you, since groups can be nested. type Group struct { // A short description of the group. The // short description is primarily used in the built-in generated help // message ShortDescription string // A long description of the group. The long // description is primarily used to present information on commands // (Command embeds Group) in the built-in generated help and man pages. LongDescription string // The namespace of the group Namespace string // If true, the group is not displayed in the help or man page Hidden bool // The parent of the group or nil if it has no parent parent interface{} // All the options in the group options []*Option // All the subgroups groups []*Group // Whether the group represents the built-in help group isBuiltinHelp bool data interface{} } type scanHandler func(reflect.Value, *reflect.StructField) (bool, error) // AddGroup adds a new group to the command with the given name and data. The // data needs to be a pointer to a struct from which the fields indicate which // options are in the group. func (g *Group) AddGroup(shortDescription string, longDescription string, data interface{}) (*Group, error) { group := newGroup(shortDescription, longDescription, data) group.parent = g if err := group.scan(); err != nil { return nil, err } g.groups = append(g.groups, group) return group, nil } // Groups returns the list of groups embedded in this group. func (g *Group) Groups() []*Group { return g.groups } // Options returns the list of options in this group. func (g *Group) Options() []*Option { return g.options } // Find locates the subgroup with the given short description and returns it. // If no such group can be found Find will return nil. Note that the description // is matched case insensitively. func (g *Group) Find(shortDescription string) *Group { lshortDescription := strings.ToLower(shortDescription) var ret *Group g.eachGroup(func(gg *Group) { if gg != g && strings.ToLower(gg.ShortDescription) == lshortDescription { ret = gg } }) return ret } func (g *Group) findOption(matcher func(*Option) bool) (option *Option) { g.eachGroup(func(g *Group) { for _, opt := range g.options { if option == nil && matcher(opt) { option = opt } } }) return option } // FindOptionByLongName finds an option that is part of the group, or any of its // subgroups, by matching its long name (including the option namespace). func (g *Group) FindOptionByLongName(longName string) *Option { return g.findOption(func(option *Option) bool { return option.LongNameWithNamespace() == longName }) } // FindOptionByShortName finds an option that is part of the group, or any of // its subgroups, by matching its short name. func (g *Group) FindOptionByShortName(shortName rune) *Option { return g.findOption(func(option *Option) bool { return option.ShortName == shortName }) } func newGroup(shortDescription string, longDescription string, data interface{}) *Group { return &Group{ ShortDescription: shortDescription, LongDescription: longDescription, data: data, } } func (g *Group) optionByName(name string, namematch func(*Option, string) bool) *Option { prio := 0 var retopt *Option g.eachGroup(func(g *Group) { for _, opt := range g.options { if namematch != nil && namematch(opt, name) && prio < 4 { retopt = opt prio = 4 } if name == opt.field.Name && prio < 3 { retopt = opt prio = 3 } if name == opt.LongNameWithNamespace() && prio < 2 { retopt = opt prio = 2 } if opt.ShortName != 0 && name == string(opt.ShortName) && prio < 1 { retopt = opt prio = 1 } } }) return retopt } func (g *Group) eachGroup(f func(*Group)) { f(g) for _, gg := range g.groups { gg.eachGroup(f) } } func isStringFalsy(s string) bool { return s == "" || s == "false" || s == "no" || s == "0" } func (g *Group) scanStruct(realval reflect.Value, sfield *reflect.StructField, handler scanHandler) error { stype := realval.Type() if sfield != nil { if ok, err := handler(realval, sfield); err != nil { return err } else if ok { return nil } } for i := 0; i < stype.NumField(); i++ { field := stype.Field(i) // PkgName is set only for non-exported fields, which we ignore if field.PkgPath != "" && !field.Anonymous { continue } mtag := newMultiTag(string(field.Tag)) if err := mtag.Parse(); err != nil { return err } // Skip fields with the no-flag tag if mtag.Get("no-flag") != "" { continue } // Dive deep into structs or pointers to structs kind := field.Type.Kind() fld := realval.Field(i) if kind == reflect.Struct { if err := g.scanStruct(fld, &field, handler); err != nil { return err } } else if kind == reflect.Ptr && field.Type.Elem().Kind() == reflect.Struct { flagCountBefore := len(g.options) + len(g.groups) if fld.IsNil() { fld = reflect.New(fld.Type().Elem()) } if err := g.scanStruct(reflect.Indirect(fld), &field, handler); err != nil { return err } if len(g.options)+len(g.groups) != flagCountBefore { realval.Field(i).Set(fld) } } longname := mtag.Get("long") shortname := mtag.Get("short") // Need at least either a short or long name if longname == "" && shortname == "" && mtag.Get("ini-name") == "" { continue } short := rune(0) rc := utf8.RuneCountInString(shortname) if rc > 1 { return newErrorf(ErrShortNameTooLong, "short names can only be 1 character long, not `%s'", shortname) } else if rc == 1 { short, _ = utf8.DecodeRuneInString(shortname) } description := mtag.Get("description") def := mtag.GetMany("default") optionalValue := mtag.GetMany("optional-value") valueName := mtag.Get("value-name") defaultMask := mtag.Get("default-mask") optional := !isStringFalsy(mtag.Get("optional")) required := !isStringFalsy(mtag.Get("required")) choices := mtag.GetMany("choice") hidden := !isStringFalsy(mtag.Get("hidden")) option := &Option{ Description: description, ShortName: short, LongName: longname, Default: def, EnvDefaultKey: mtag.Get("env"), EnvDefaultDelim: mtag.Get("env-delim"), OptionalArgument: optional, OptionalValue: optionalValue, Required: required, ValueName: valueName, DefaultMask: defaultMask, Choices: choices, Hidden: hidden, group: g, field: field, value: realval.Field(i), tag: mtag, } if option.isBool() && option.Default != nil { return newErrorf(ErrInvalidTag, "boolean flag `%s' may not have default values, they always default to `false' and can only be turned on", option.shortAndLongName()) } g.options = append(g.options, option) } return nil } func (g *Group) checkForDuplicateFlags() *Error { shortNames := make(map[rune]*Option) longNames := make(map[string]*Option) var duplicateError *Error g.eachGroup(func(g *Group) { for _, option := range g.options { if option.LongName != "" { longName := option.LongNameWithNamespace() if otherOption, ok := longNames[longName]; ok { duplicateError = newErrorf(ErrDuplicatedFlag, "option `%s' uses the same long name as option `%s'", option, otherOption) return } longNames[longName] = option } if option.ShortName != 0 { if otherOption, ok := shortNames[option.ShortName]; ok { duplicateError = newErrorf(ErrDuplicatedFlag, "option `%s' uses the same short name as option `%s'", option, otherOption) return } shortNames[option.ShortName] = option } } }) return duplicateError } func (g *Group) scanSubGroupHandler(realval reflect.Value, sfield *reflect.StructField) (bool, error) { mtag := newMultiTag(string(sfield.Tag)) if err := mtag.Parse(); err != nil { return true, err } subgroup := mtag.Get("group") if len(subgroup) != 0 { var ptrval reflect.Value if realval.Kind() == reflect.Ptr { ptrval = realval if ptrval.IsNil() { ptrval.Set(reflect.New(ptrval.Type())) } } else { ptrval = realval.Addr() } description := mtag.Get("description") group, err := g.AddGroup(subgroup, description, ptrval.Interface()) if err != nil { return true, err } group.Namespace = mtag.Get("namespace") group.Hidden = mtag.Get("hidden") != "" return true, nil } return false, nil } func (g *Group) scanType(handler scanHandler) error { // Get all the public fields in the data struct ptrval := reflect.ValueOf(g.data) if ptrval.Type().Kind() != reflect.Ptr { panic(ErrNotPointerToStruct) } stype := ptrval.Type().Elem() if stype.Kind() != reflect.Struct { panic(ErrNotPointerToStruct) } realval := reflect.Indirect(ptrval) if err := g.scanStruct(realval, nil, handler); err != nil { return err } if err := g.checkForDuplicateFlags(); err != nil { return err } return nil } func (g *Group) scan() error { return g.scanType(g.scanSubGroupHandler) } func (g *Group) groupByName(name string) *Group { if len(name) == 0 { return g } return g.Find(name) }