| // 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" |
| "unsafe" |
| ) |
| |
| // 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 { |
| ptrval := reflect.NewAt(realval.Type(), unsafe.Pointer(realval.UnsafeAddr())) |
| 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) |
| } |