Files
got/cli/options.go
Hojun-Cho 2afcb861e4 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.
2026-06-22 08:29:25 +09:00

101 lines
3.5 KiB
Go

package cli
import (
"fmt"
"math"
"strconv"
"strings"
)
// 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). 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
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{}, typed: map[string]any{}}
}
// IsSet reports whether name was given on the command line or in the config
// (as opposed to coming from the built-in default).
func (o *Options) IsSet(name string) bool { return o.set[name] }
// Str returns the raw string value (empty if unset and no default).
func (o *Options) Str(name string) string { return o.vals[name] }
// boolWords is the accepted vocabulary for boolean options: exactly true and
// false, nothing else. boolWord is the single consult point — the Bool reader,
// validate(), and the no-conf bootstrap all go through it — so exactly the words
// that validate are honoured.
var boolWords = map[string]bool{
"true": true,
"false": false,
}
// boolWord reports a value's truth and whether it is a recognised boolean word.
// Whitespace is trimmed, but case is significant: "True"/"TRUE" are rejected, so
// the match is against exactly "true"/"false".
func boolWord(s string) (val, ok bool) {
val, ok = boolWords[strings.TrimSpace(s)]
return val, ok
}
// 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 {
v := o.vals[name]
if v == "" {
return nil
}
return strings.Split(v, "\n")
}
// parseSize converts a size string into a byte count: the first 'K'/'k' or
// 'M'/'m' in the string selects the multiplier (1024 or 1024*1024) and
// everything from that byte on is discarded; with no such unit the whole string
// is the byte count. There is no gigabyte unit, so "1G" is rejected; "1Mi" and
// "10MB" are 1M and 10M (the trailing bytes are dropped). An empty string is 0
// bytes; a negative value is rejected.
func parseSize(s string) (int64, error) {
s = strings.TrimSpace(s)
if s == "" {
return 0, nil
}
mult := int64(1)
if i := strings.IndexAny(s, "KkMm"); i >= 0 {
if c := s[i]; c == 'M' || c == 'm' {
mult = 1 << 20
} else {
mult = 1 << 10
}
s = s[:i]
}
n, err := strconv.ParseInt(strings.TrimSpace(s), 10, 64)
if err != nil {
return 0, fmt.Errorf("bad size %q", s)
}
// Size parsing rejects negative sizes outright.
if n < 0 {
return 0, fmt.Errorf("negative size %q", s)
}
// Reject a value whose unit multiply would overflow int64 and silently wrap to
// a bogus (positive or negative) byte count.
if mult > 1 && n > math.MaxInt64/mult {
return 0, fmt.Errorf("size %q too large", s)
}
return n * mult, nil
}