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.
101 lines
3.5 KiB
Go
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
|
|
}
|