summaryrefslogtreecommitdiff
path: root/parse.go
diff options
context:
space:
mode:
Diffstat (limited to 'parse.go')
-rw-r--r--parse.go134
1 files changed, 87 insertions, 47 deletions
diff --git a/parse.go b/parse.go
index dc87947..dc48455 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,18 +211,31 @@ func NewParser(config Config, dests ...interface{}) (*Parser, error) {
return nil, err
}
- // add nonzero field values as defaults
+ // for backwards compatibility, add nonzero field values as defaults
+ // this applies only to the top-level command, not to subcommands (this inconsistency
+ // is the reason that this method for setting default values was deprecated)
for _, spec := range cmd.specs {
- if v := p.val(spec.dest); v.IsValid() && !isZero(v) {
- if defaultVal, ok := v.Interface().(encoding.TextMarshaler); ok {
- str, err := defaultVal.MarshalText()
- if err != nil {
- return nil, fmt.Errorf("%v: error marshaling default value to string: %v", spec.dest, err)
- }
- spec.defaultVal = string(str)
- } else {
- spec.defaultVal = fmt.Sprintf("%v", v)
+ // get the value
+ v := p.val(spec.dest)
+
+ // if the value is the "zero value" (e.g. nil pointer, empty struct) then ignore
+ if isZero(v) {
+ 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 {
+ s, err := m.MarshalText()
+ if err != nil {
+ return nil, fmt.Errorf("%v: error marshaling default value to string: %v", spec.dest, err)
}
+ spec.defaultString = string(s)
+ } else {
+ spec.defaultString = fmt.Sprintf("%v", v)
}
}
@@ -293,11 +307,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, ",") {
@@ -324,11 +333,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
@@ -377,27 +381,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
})
@@ -680,11 +717,14 @@ func (p *Parser) process(args []string) error {
}
return errors.New(msg)
}
- if !p.config.IgnoreDefault && 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.config.IgnoreDefault {
+ // One issue here is that if the user now modifies the value then
+ // the default value stored in the spec will be corrupted. There
+ // is no general way to "deep-copy" values in Go, and we still
+ // support the old-style method for specifying defaults as
+ // Go values assigned directly to the struct field, so we are stuck.
+ p.val(spec.dest).Set(spec.defaultValue)
}
}