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:
2026-06-22 08:29:25 +09:00
parent b98c36acb7
commit 2afcb861e4
3 changed files with 102 additions and 86 deletions

View File

@@ -9,15 +9,17 @@ import (
// Options is the resolved configuration: a flat name->value map plus a record
// of which names were explicitly set (so callers can tell a default from a
// chosen value). Values are kept as strings end-to-end and converted only at
// the point of use by the typed getters below.
// chosen value). The raw strings carry the layered override merge; finalize()
// then parses each one exactly once into typed, and the numeric/bool getters
// read that — no value is parsed twice.
type Options struct {
vals map[string]string
set map[string]bool
vals map[string]string
set map[string]bool
typed map[string]any // parsed value per option, filled once by finalize()
}
func newOptions() *Options {
return &Options{vals: map[string]string{}, set: map[string]bool{}}
return &Options{vals: map[string]string{}, set: map[string]bool{}, typed: map[string]any{}}
}
// IsSet reports whether name was given on the command line or in the config
@@ -44,30 +46,13 @@ func boolWord(s string) (val, ok bool) {
return val, ok
}
// Bool reports whether the value is a truthy boolean word.
func (o *Options) Bool(name string) bool {
val, _ := boolWord(o.vals[name])
return val
}
// Int returns the value as an int, or 0 if empty/invalid. The value has already
// passed validate(), so a parse failure here is unreachable in normal flow.
func (o *Options) Int(name string) int {
n, _ := strconv.ParseInt(strings.TrimSpace(o.vals[name]), 10, 64)
return int(n)
}
// Float returns the value as a float64, or 0 if empty/invalid.
func (o *Options) Float(name string) float64 {
f, _ := strconv.ParseFloat(strings.TrimSpace(o.vals[name]), 64)
return f
}
// Size returns a byte count parsed from a value like "20M" or "512K".
func (o *Options) Size(name string) int64 {
n, _ := parseSize(o.vals[name])
return n
}
// Bool, Int, Float and Size read the value finalize() already parsed and stored
// in typed; an unset option (no entry) reads as the zero value. The conversion
// happened once, at finalize, so these never re-parse a string.
func (o *Options) Bool(name string) bool { v, _ := o.typed[name].(bool); return v }
func (o *Options) Int(name string) int { v, _ := o.typed[name].(int64); return int(v) }
func (o *Options) Float(name string) float64 { v, _ := o.typed[name].(float64); return v }
func (o *Options) Size(name string) int64 { v, _ := o.typed[name].(int64); return v }
// List returns a repeatable option's accumulated values.
func (o *Options) List(name string) []string {