From 27c832b934678d80580efd9940e5183ef3d687da Mon Sep 17 00:00:00 2001 From: Alex Flint Date: Sat, 29 Oct 2022 14:47:13 -0400 Subject: store both a default value and a string representation of that default value in the spec for each option --- parse.go | 126 ++++++++++++++++++++++++++++++++++----------------------------- 1 file changed, 69 insertions(+), 57 deletions(-) (limited to 'parse.go') diff --git a/parse.go b/parse.go index 28ed014..980ac22 100644 --- a/parse.go +++ b/parse.go @@ -43,18 +43,19 @@ func (p path) Child(f reflect.StructField) path { // spec represents a command line option type spec struct { - dest path - field reflect.StructField // the struct field from which this option was created - long string // the --long form for this option, or empty if none - short string // the -s short form for this option, or empty if none - cardinality cardinality // determines how many tokens will be present (possible values: zero, one, multiple) - required bool // if true, this option must be present on the command line - positional bool // if true, this option will be looked for in the positional flags - separate bool // if true, each slice and map entry will have its own --flag - help string // the help text for this option - env string // the name of the environment variable for this option, or empty for none - defaultVal string // default value for this option - placeholder string // name of the data in help + dest path + field reflect.StructField // the struct field from which this option was created + long string // the --long form for this option, or empty if none + short string // the -s short form for this option, or empty if none + cardinality cardinality // determines how many tokens will be present (possible values: zero, one, multiple) + required bool // if true, this option must be present on the command line + positional bool // if true, this option will be looked for in the positional flags + separate bool // if true, each slice and map entry will have its own --flag + help string // the help text for this option + env string // the name of the environment variable for this option, or empty for none + defaultValue reflect.Value // default value for this option + defaultString string // default value for this option, in string form to be displayed in help text + placeholder string // name of the data in help } // command represents a named subcommand, or the top-level command @@ -210,39 +211,30 @@ func NewParser(config Config, dests ...interface{}) (*Parser, error) { // for backwards compatibility, add nonzero field values as defaults for _, spec := range cmd.specs { - // do not read default when UnmarshalText is implemented but not MarshalText - if isTextUnmarshaler(spec.field.Type) && !isTextMarshaler(spec.field.Type) { + // get the value + v := p.val(spec.dest) + if !v.IsValid() { continue } - // do not process types that require multiple values - cardinality, _ := cardinalityOf(spec.field.Type) - if cardinality != one { + // if the value is the "zero value" (e.g. nil pointer, empty struct) then ignore + if isZero(v) { continue } - // get the value - v := p.val(spec.dest) - if !v.IsValid() { - continue - } + // store as a default + spec.defaultValue = v + // we need a string to display in help text // if MarshalText is implemented then use that if m, ok := v.Interface().(encoding.TextMarshaler); ok { - if v.IsNil() { - continue - } s, err := m.MarshalText() if err != nil { return nil, fmt.Errorf("%v: error marshaling default value to string: %v", spec.dest, err) } - spec.defaultVal = string(s) - continue - } - - // finally, use the value as a default if it is non-zero - if !isZero(v) { - spec.defaultVal = fmt.Sprintf("%v", v) + spec.defaultString = string(s) + } else { + spec.defaultString = fmt.Sprintf("%v", v) } } @@ -311,11 +303,6 @@ func cmdFromStruct(name string, dest path, t reflect.Type) (*command, error) { spec.help = help } - defaultVal, hasDefault := field.Tag.Lookup("default") - if hasDefault { - spec.defaultVal = defaultVal - } - // Look at the tag var isSubcommand bool // tracks whether this field is a subcommand for _, key := range strings.Split(tag, ",") { @@ -342,11 +329,6 @@ func cmdFromStruct(name string, dest path, t reflect.Type) (*command, error) { } spec.short = key[1:] case key == "required": - if hasDefault { - errs = append(errs, fmt.Sprintf("%s.%s: 'required' cannot be used when a default value is specified", - t.Name(), field.Name)) - return false - } spec.required = true case key == "positional": spec.positional = true @@ -395,27 +377,60 @@ func cmdFromStruct(name string, dest path, t reflect.Type) (*command, error) { spec.placeholder = strings.ToUpper(spec.field.Name) } - // Check whether this field is supported. It's good to do this here rather than + // if this is a subcommand then we've done everything we need to do + if isSubcommand { + return false + } + + // check whether this field is supported. It's good to do this here rather than // wait until ParseValue because it means that a program with invalid argument // fields will always fail regardless of whether the arguments it received // exercised those fields. - if !isSubcommand { - cmd.specs = append(cmd.specs, &spec) + var err error + spec.cardinality, err = cardinalityOf(field.Type) + if err != nil { + errs = append(errs, fmt.Sprintf("%s.%s: %s fields are not supported", + t.Name(), field.Name, field.Type.String())) + return false + } - var err error - spec.cardinality, err = cardinalityOf(field.Type) - if err != nil { - errs = append(errs, fmt.Sprintf("%s.%s: %s fields are not supported", - t.Name(), field.Name, field.Type.String())) + defaultString, hasDefault := field.Tag.Lookup("default") + if hasDefault { + // we do not support default values for maps and slices + if spec.cardinality == multiple { + errs = append(errs, fmt.Sprintf("%s.%s: default values are not supported for slice or map fields", + t.Name(), field.Name)) return false } - if spec.cardinality == multiple && hasDefault { - errs = append(errs, fmt.Sprintf("%s.%s: default values are not supported for slice or map fields", + + // a required field cannot also have a default value + if spec.required { + errs = append(errs, fmt.Sprintf("%s.%s: 'required' cannot be used when a default value is specified", t.Name(), field.Name)) return false } + + // parse the default value + spec.defaultString = defaultString + if field.Type.Kind() == reflect.Pointer { + // here we have a field of type *T and we create a new T, no need to dereference + // in order for the value to be settable + spec.defaultValue = reflect.New(field.Type.Elem()) + } else { + // here we have a field of type T and we create a new T and then dereference it + // so that the resulting value is settable + spec.defaultValue = reflect.New(field.Type).Elem() + } + err := scalar.ParseValue(spec.defaultValue, defaultString) + if err != nil { + errs = append(errs, fmt.Sprintf("%s.%s: error processing default value: %v", t.Name(), field.Name, err)) + return false + } } + // add the spec to the list of specs + cmd.specs = append(cmd.specs, &spec) + // if this was an embedded field then we already returned true up above return false }) @@ -682,11 +697,8 @@ func (p *Parser) process(args []string) error { } return errors.New(msg) } - if spec.defaultVal != "" { - err := scalar.ParseValue(p.val(spec.dest), spec.defaultVal) - if err != nil { - return fmt.Errorf("error processing default value for %s: %v", name, err) - } + if spec.defaultValue.IsValid() { + p.val(spec.dest).Set(spec.defaultValue) } } -- cgit v1.2.3