261 lines
6.4 KiB
Go
261 lines
6.4 KiB
Go
package config
|
|
|
|
import (
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Set wraps flag.FlagSet to enforce this project's option convention:
|
|
// a single hyphen always introduces a single letter (-s), a double hyphen
|
|
// always introduces a full word (--max-size). The stdlib flag package treats
|
|
// the two forms as interchangeable, so the convention is enforced here rather
|
|
// than by the package.
|
|
type Set struct {
|
|
fs *flag.FlagSet
|
|
envPrefix string
|
|
specs []*spec
|
|
longs map[string]bool
|
|
shorts map[string]bool
|
|
fromEnv map[string]bool
|
|
}
|
|
|
|
type spec struct {
|
|
long, short string
|
|
placeholder string
|
|
usage string
|
|
def string
|
|
}
|
|
|
|
func NewSet(name, envPrefix string) *Set {
|
|
s := &Set{
|
|
fs: flag.NewFlagSet(name, flag.ContinueOnError),
|
|
envPrefix: envPrefix,
|
|
longs: map[string]bool{},
|
|
shorts: map[string]bool{},
|
|
fromEnv: map[string]bool{},
|
|
}
|
|
s.fs.Usage = func() {}
|
|
return s
|
|
}
|
|
|
|
func (s *Set) Args() []string { return s.fs.Args() }
|
|
func (s *Set) SetOutput(w io.Writer) { s.fs.SetOutput(w) }
|
|
func (s *Set) register(sp *spec) { s.specs = append(s.specs, sp) }
|
|
func (s *Set) note(long, short string) {
|
|
s.longs[long] = true
|
|
if short != "" {
|
|
s.shorts[short] = true
|
|
}
|
|
}
|
|
|
|
// envName maps --max-size to SEND_MAX_SIZE.
|
|
func (s *Set) envName(long string) string {
|
|
return s.envPrefix + strings.ToUpper(strings.ReplaceAll(long, "-", "_"))
|
|
}
|
|
|
|
func (s *Set) String(p *string, long, short, def, placeholder, usage string) {
|
|
*p = def
|
|
s.fs.StringVar(p, long, def, usage)
|
|
if short != "" {
|
|
s.fs.StringVar(p, short, def, usage)
|
|
}
|
|
s.note(long, short)
|
|
s.register(&spec{long, short, placeholder, usage, def})
|
|
}
|
|
|
|
func (s *Set) Bool(p *bool, long, short string, def bool, usage string) {
|
|
*p = def
|
|
s.fs.BoolVar(p, long, def, usage)
|
|
if short != "" {
|
|
s.fs.BoolVar(p, short, def, usage)
|
|
}
|
|
s.note(long, short)
|
|
s.register(&spec{long, short, "", usage, fmt.Sprint(def)})
|
|
}
|
|
|
|
func (s *Set) Int(p *int, long, short string, def int, usage string) {
|
|
*p = def
|
|
s.fs.IntVar(p, long, def, usage)
|
|
if short != "" {
|
|
s.fs.IntVar(p, short, def, usage)
|
|
}
|
|
s.note(long, short)
|
|
s.register(&spec{long, short, "N", usage, fmt.Sprint(def)})
|
|
}
|
|
|
|
type sizeValue struct{ p *int64 }
|
|
|
|
func (v sizeValue) String() string {
|
|
if v.p == nil {
|
|
return ""
|
|
}
|
|
return FormatSize(*v.p)
|
|
}
|
|
|
|
func (v sizeValue) Set(s string) error {
|
|
n, err := ParseSize(s)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
*v.p = n
|
|
return nil
|
|
}
|
|
|
|
// Size registers a byte-count option accepting "2GiB", "500MB" or "unlimited".
|
|
func (s *Set) Size(p *int64, long, short, def, usage string) {
|
|
n, err := ParseSize(def)
|
|
if err != nil {
|
|
panic("config: bad default size for --" + long + ": " + err.Error())
|
|
}
|
|
*p = n
|
|
v := sizeValue{p}
|
|
s.fs.Var(v, long, usage)
|
|
if short != "" {
|
|
s.fs.Var(v, short, usage)
|
|
}
|
|
s.note(long, short)
|
|
s.register(&spec{long, short, "SIZE", usage, def})
|
|
}
|
|
|
|
type durationValue struct{ p *time.Duration }
|
|
|
|
func (v durationValue) String() string {
|
|
if v.p == nil {
|
|
return ""
|
|
}
|
|
return FormatDuration(*v.p)
|
|
}
|
|
|
|
func (v durationValue) Set(s string) error {
|
|
d, err := ParseDuration(s)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
*v.p = d
|
|
return nil
|
|
}
|
|
|
|
// Duration registers a time option accepting "3d", "90m" or "never".
|
|
func (s *Set) Duration(p *time.Duration, long, short, def, usage string) {
|
|
d, err := ParseDuration(def)
|
|
if err != nil {
|
|
panic("config: bad default duration for --" + long + ": " + err.Error())
|
|
}
|
|
*p = d
|
|
v := durationValue{p}
|
|
s.fs.Var(v, long, usage)
|
|
if short != "" {
|
|
s.fs.Var(v, short, usage)
|
|
}
|
|
s.note(long, short)
|
|
s.register(&spec{long, short, "DURATION", usage, def})
|
|
}
|
|
|
|
// checkConvention rejects a long name given with one hyphen, or a short name
|
|
// given with two. Only names actually registered are considered, so an option
|
|
// *value* that happens to start with a hyphen is never mistaken for a flag.
|
|
func (s *Set) checkConvention(args []string) error {
|
|
for _, a := range args {
|
|
if a == "--" {
|
|
return nil
|
|
}
|
|
if len(a) < 2 || a[0] != '-' {
|
|
continue
|
|
}
|
|
hyphens := 1
|
|
name := a[1:]
|
|
if name[0] == '-' {
|
|
hyphens = 2
|
|
name = name[1:]
|
|
}
|
|
name, _, _ = strings.Cut(name, "=")
|
|
switch {
|
|
case hyphens == 1 && len(name) > 1 && s.longs[name]:
|
|
return fmt.Errorf("use --%s, not -%s: long options take two hyphens", name, name)
|
|
case hyphens == 2 && len(name) == 1 && s.shorts[name]:
|
|
return fmt.Errorf("use -%s, not --%s: short options take one hyphen", name, name)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// applyEnv seeds every option from its SEND_-prefixed environment variable.
|
|
// Called before parsing so an explicit command-line flag always wins.
|
|
func (s *Set) applyEnv() error {
|
|
for _, sp := range s.specs {
|
|
v, ok := os.LookupEnv(s.envName(sp.long))
|
|
if !ok {
|
|
continue
|
|
}
|
|
if err := s.fs.Set(sp.long, v); err != nil {
|
|
return fmt.Errorf("%s: %w", s.envName(sp.long), err)
|
|
}
|
|
s.fromEnv[sp.long] = true
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Parse applies environment fallbacks, checks the hyphen convention and then
|
|
// parses args (which must exclude the program name).
|
|
func (s *Set) Parse(args []string) error {
|
|
if err := s.applyEnv(); err != nil {
|
|
return err
|
|
}
|
|
if err := s.checkConvention(args); err != nil {
|
|
return err
|
|
}
|
|
return s.fs.Parse(args)
|
|
}
|
|
|
|
// Changed reports whether an option was given on the command line.
|
|
//
|
|
// Values picked up from the environment do not count: a command that acts only
|
|
// on the options you actually typed must not quietly act on a SEND_ variable
|
|
// exported for the server.
|
|
func (s *Set) Changed(long string) bool {
|
|
if s.fromEnv[long] {
|
|
return false
|
|
}
|
|
short := ""
|
|
for _, sp := range s.specs {
|
|
if sp.long == long {
|
|
short = sp.short
|
|
break
|
|
}
|
|
}
|
|
given := false
|
|
s.fs.Visit(func(f *flag.Flag) {
|
|
if f.Name == long || (short != "" && f.Name == short) {
|
|
given = true
|
|
}
|
|
})
|
|
return given
|
|
}
|
|
|
|
// PrintUsage renders the options in the "-x, --xxx" form the convention implies.
|
|
func (s *Set) PrintUsage(w io.Writer, header string) {
|
|
fmt.Fprint(w, header)
|
|
for _, sp := range s.specs {
|
|
lead := " "
|
|
if sp.short != "" {
|
|
lead = " -" + sp.short + ","
|
|
}
|
|
name := lead + " --" + sp.long
|
|
if sp.placeholder != "" {
|
|
name += " " + sp.placeholder
|
|
}
|
|
fmt.Fprintf(w, "%s\n", name)
|
|
fmt.Fprintf(w, " %s", sp.usage)
|
|
// A zero default means "unset" for the options that have one, and
|
|
// printing it would read as a real value.
|
|
if sp.def != "" && sp.def != "false" && sp.def != "0" {
|
|
fmt.Fprintf(w, " (default %s)", sp.def)
|
|
}
|
|
fmt.Fprintf(w, "\n [%s]\n", s.envName(sp.long))
|
|
}
|
|
}
|