Files
uncensored-send/internal/config/flags.go
T
2026-09-12 23:48:03 +02:00

233 lines
5.7 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
}
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)
// 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))
}
}