cli: parse each option value once
validate() parsed every value to bounds-check it, then the typed getters re-parsed the same string at read time (with a comment admitting the failure was unreachable). A single parseValue() now validates and converts, finalize() runs it once per option after layering, and the getters just read the stored typed value.
This commit is contained in:
76
cli/parse.go
76
cli/parse.go
@@ -55,9 +55,34 @@ func Parse(args []string) (*Result, error) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if err := opts.finalize(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Result{Action: Run, Options: opts, URIs: uris}, nil
|
||||
}
|
||||
|
||||
// finalize parses every resolved value once into its typed form, validating it
|
||||
// in the same pass; the numeric/bool getters then just read the result. It runs
|
||||
// after all layering, so each option is converted exactly once, from its final
|
||||
// (last-wins) string — not once per layer and again per read.
|
||||
func (o *Options) finalize() error {
|
||||
for i := range options {
|
||||
opt := &options[i]
|
||||
raw, ok := o.vals[opt.Long]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
v, err := parseValue(opt, raw)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if v != nil {
|
||||
o.typed[opt.Long] = v
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseArgs walks the argument list once. It recognises --long, --long=val,
|
||||
// -x, -xval, -x val and short bool bundles like -cq. Like getopt_long, it
|
||||
// permutes: a non-flag token becomes a URI but scanning continues so flags may
|
||||
@@ -214,16 +239,13 @@ func applyEnv(o *Options) {
|
||||
}
|
||||
}
|
||||
|
||||
// set validates a value against its option descriptor and stores it, marking
|
||||
// the option as explicitly set.
|
||||
// set stores a raw value, marking the option as explicitly set. Validation and
|
||||
// conversion happen later, once, in finalize(); set only records the string.
|
||||
func set(o *Options, name, val string) error {
|
||||
opt := byLong[name]
|
||||
if opt == nil {
|
||||
return fmt.Errorf("unknown option %q", name)
|
||||
}
|
||||
if err := validate(opt, val); err != nil {
|
||||
return err
|
||||
}
|
||||
if opt.Kind == List && o.set[name] {
|
||||
val = appendList(o.vals[name], val)
|
||||
}
|
||||
@@ -232,52 +254,58 @@ func set(o *Options, name, val string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func validate(o *Opt, val string) error {
|
||||
// parseValue converts a raw string to the typed value for o.Kind, validating it
|
||||
// in the process: it is the single place a value is parsed. Bool/Int/Size/Float
|
||||
// return their typed value (read back by the getters); Str/List/Enum need no
|
||||
// stored conversion and return nil, with Enum still checked for membership.
|
||||
func parseValue(o *Opt, val string) (any, error) {
|
||||
switch o.Kind {
|
||||
case Bool:
|
||||
// Booleans accept true/false plus the yes/no/1/0/on/off spellings, but
|
||||
// nothing else, so e.g. --enable-dht=flase is a parse error. The accepted
|
||||
// set is boolWords, the same vocabulary the readers honour.
|
||||
if _, ok := boolWord(val); !ok {
|
||||
return fmt.Errorf("--%s: %q is not a boolean (true/false)", o.Long, val)
|
||||
// Booleans accept exactly true/false (the boolWords vocabulary the Bool
|
||||
// getter honours); --enable-dht=flase is a parse error.
|
||||
v, ok := boolWord(val)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("--%s: %q is not a boolean (true/false)", o.Long, val)
|
||||
}
|
||||
return v, nil
|
||||
case Int:
|
||||
n, err := strconv.ParseInt(strings.TrimSpace(val), 10, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("--%s: %q is not an integer", o.Long, val)
|
||||
return nil, fmt.Errorf("--%s: %q is not an integer", o.Long, val)
|
||||
}
|
||||
if err := checkBounds(o, n); err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
return n, nil
|
||||
case Size:
|
||||
n, err := parseSize(val)
|
||||
if err != nil {
|
||||
return fmt.Errorf("--%s: %v", o.Long, err)
|
||||
return nil, fmt.Errorf("--%s: %v", o.Long, err)
|
||||
}
|
||||
if err := checkBounds(o, n); err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
return n, nil
|
||||
case Float:
|
||||
// seed-time and seed-ratio are both rates/durations that must be
|
||||
// non-negative; a negative SeedTime would fire the seed timer immediately
|
||||
// and a negative SeedRatio is silently ignored. Reject negatives here.
|
||||
// Opt.Min/Max are int64, so we only enforce the floor, not full bounds.
|
||||
// seed-time and seed-ratio must be non-negative: a negative SeedTime would
|
||||
// fire the seed timer immediately and a negative SeedRatio is meaningless.
|
||||
f, err := strconv.ParseFloat(strings.TrimSpace(val), 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("--%s: %q is not a number", o.Long, val)
|
||||
return nil, fmt.Errorf("--%s: %q is not a number", o.Long, val)
|
||||
}
|
||||
if f < 0 {
|
||||
return fmt.Errorf("--%s: %v below minimum 0", o.Long, f)
|
||||
return nil, fmt.Errorf("--%s: %v below minimum 0", o.Long, f)
|
||||
}
|
||||
return f, nil
|
||||
case Enum:
|
||||
for _, e := range o.Enum {
|
||||
if val == e {
|
||||
return nil
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("--%s: %q not one of %s", o.Long, val, strings.Join(o.Enum, "|"))
|
||||
return nil, fmt.Errorf("--%s: %q not one of %s", o.Long, val, strings.Join(o.Enum, "|"))
|
||||
}
|
||||
return nil
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// checkBounds enforces an Int/Size option's numeric range. Min defaults to a
|
||||
|
||||
Reference in New Issue
Block a user