|
- // 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)
- }
|