Initial commit
This commit is contained in:
@@ -0,0 +1,230 @@
|
||||
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
|
||||
}
|
||||
|
||||
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{},
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
if sp.def != "" && sp.def != "false" {
|
||||
fmt.Fprintf(w, " (default %s)", sp.def)
|
||||
}
|
||||
fmt.Fprintf(w, "\n [%s]\n", s.envName(sp.long))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user