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 }