internal/control/flags.go

5e9732fb1d5142142fdae2985f07125cf852647c
gitbay/internal/control/flags.go history · blame · raw

101 lines · 2686 bytes

  1package control
  2
  3import (
  4	"fmt"
  5	"strings"
  6)
  7
  8// flagSpec is what a command accepts: flags that take one value, flags
  9// that take a value and may repeat, switches, and how many positional
 10// arguments are allowed (-1 for any number). Every command used to walk
 11// argv by hand and decided on its own whether an unknown --flag was an
 12// error, a positional or nothing at all (#96); parseFlags decides once.
 13type flagSpec struct {
 14	Values []string
 15	Multi  []string
 16	Bools  []string
 17	MaxPos int
 18	Usage  string
 19}
 20
 21// flags is a parsed argv: positionals in order, and each flag by name.
 22type flags struct {
 23	Pos   []string
 24	vals  map[string]string
 25	multi map[string][]string
 26	seen  map[string]bool
 27}
 28
 29// Value is the last value given for a value flag, or "".
 30func (f flags) Value(name string) string { return f.vals[name] }
 31
 32// Has reports whether a flag of any kind was given.
 33func (f flags) Has(name string) bool { return f.seen[name] }
 34
 35// List is every value given for a repeatable flag, in order.
 36func (f flags) List(name string) []string { return f.multi[name] }
 37
 38// parseFlags reads args against spec. A flag must be one the spec names;
 39// a value flag consumes the next argument verbatim, "-" included; "--"
 40// ends flag parsing. The error, when there is one, is the usage message.
 41func parseFlags(args []string, spec flagSpec) (flags, error) {
 42	f := flags{vals: map[string]string{}, multi: map[string][]string{}, seen: map[string]bool{}}
 43	kind := map[string]byte{}
 44	for _, n := range spec.Values {
 45		kind[n] = 'v'
 46	}
 47	for _, n := range spec.Multi {
 48		kind[n] = 'm'
 49	}
 50	for _, n := range spec.Bools {
 51		kind[n] = 'b'
 52	}
 53	usage := func(format string, a ...any) error {
 54		msg := fmt.Sprintf(format, a...)
 55		if spec.Usage != "" {
 56			msg += "\nusage: " + strings.TrimPrefix(spec.Usage, "usage: ")
 57		}
 58		return fmt.Errorf("%s", msg)
 59	}
 60	onlyPos := false
 61	for i := 0; i < len(args); i++ {
 62		a := args[i]
 63		if !onlyPos && a == "--" {
 64			onlyPos = true
 65			continue
 66		}
 67		if !onlyPos && strings.HasPrefix(a, "--") {
 68			switch kind[a] {
 69			case 'b':
 70				f.seen[a] = true
 71			case 'v', 'm':
 72				if i+1 >= len(args) {
 73					return f, usage("%s requires a value", a)
 74				}
 75				f.seen[a] = true
 76				if kind[a] == 'v' {
 77					f.vals[a] = args[i+1]
 78				} else {
 79					f.multi[a] = append(f.multi[a], args[i+1])
 80				}
 81				i++
 82			default:
 83				return f, usage("unknown flag %q", a)
 84			}
 85			continue
 86		}
 87		if spec.MaxPos >= 0 && len(f.Pos) >= spec.MaxPos {
 88			return f, usage("unexpected argument %q", a)
 89		}
 90		f.Pos = append(f.Pos, a)
 91	}
 92	return f, nil
 93}
 94
 95// pos is the nth positional argument, or "" when absent.
 96func (f flags) pos(n int) string {
 97	if n < len(f.Pos) {
 98		return f.Pos[n]
 99	}
100	return ""
101}