internal/control/flags.go
116 lines · 3326 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// parseArgs is parseFlags for the running command, with the usage line
96// printed the way a usage refusal prints it (cmdUsage): the program in
97// front and the CLI's own path where it differs (#267). spec.Usage stays
98// the text, since some commands spell their flags out more fully there
99// than in the registered Usage.
100func (c *Ctx) parseArgs(args []string, spec flagSpec) (flags, error) {
101 usage := strings.TrimPrefix(spec.Usage, "usage: ")
102 spec.Usage = ""
103 f, err := parseFlags(args, spec)
104 if err != nil && usage != "" {
105 err = fmt.Errorf("%v\nusage: %s %s", err, c.program(), c.usageShape(c.Cmd.Path, usage))
106 }
107 return f, err
108}
109
110// pos is the nth positional argument, or "" when absent.
111func (f flags) pos(n int) string {
112 if n < len(f.Pos) {
113 return f.Pos[n]
114 }
115 return ""
116}