-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathparse.go
More file actions
213 lines (179 loc) · 5.12 KB
/
parse.go
File metadata and controls
213 lines (179 loc) · 5.12 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
package opts
import (
"fmt"
"strings"
)
// Parse scans args and sets option values defined in the [Group].
//
// Parse should be called after all options are defined and before any option
// values are used. Parse performs strict parsing. If any positional arguments
// remain after parsing stops, then Parse will return [*UnexpectedArgsError].
// If Parse returns without error, the Group is considered parsed and
// subsequent calls to Parse will return [ErrAlreadyParsed].
//
// Parsing stops at the first non-option argument. Any remaining arguments can
// be accessed via the returned slice of strings. Both '-' and '--' are
// considered non-option arguments, and both stop further parsing. However,
// the returned slice will not contain '--', but it will contain '-'. (By
// convention, many programs treat '-' as stdin, but that is up to the calling
// program to decide and handle.)
//
// If Parse encounters an unknown option, an option without a value, or a value
// that cannot be parsed as its type, it returns an error and the Group remains
// unparsed. The caller may retry with different arguments.
//
// The slice passed to Parse should not include the program name. If using
// `os.Args` directly, the caller should pass `os.Args[1:]`.
//
// Use Parse if your program should not accept leftover arguments.
func (g *Group) Parse(args []string) error {
if g.parsed {
return fmt.Errorf("opts: option group %q: %w", g.name, ErrAlreadyParsed)
}
err := g.parse(args)
if err != nil {
return err
}
if len(g.args) > 0 {
return &UnexpectedArgumentsError{Args: g.args}
}
g.parsed = true
return nil
}
// ParseKnown is like Parse in all ways but one: it allows arguments to remain
// after parsing stops and thus never returns UnexpectedArgsError.
//
// Use ParseKnown if your program expects leftover arguments.
func (g *Group) ParseKnown(args []string) ([]string, error) {
if g.parsed {
return []string{}, fmt.Errorf("opts: option group %q: %w", g.name, ErrAlreadyParsed)
}
err := g.parse(args)
if err != nil {
return []string{}, err
}
g.parsed = true
return g.args, nil
}
type argType int
const (
argEmpty argType = iota
argNoDash
argSingleDash
argDoubleDash
argSingleDashOpt
argDoubleDashOpt
)
func classifyArg(arg string) argType {
switch {
case arg == "":
return argEmpty
case arg == "-":
return argSingleDash
case arg == "--":
return argDoubleDash
case arg[0] != '-':
return argNoDash
case len(arg) > 2 && arg[0:2] == "--":
return argDoubleDashOpt
case len(arg) > 1:
return argSingleDashOpt
default:
return argNoDash
}
}
func (g *Group) shouldStopParsing(arg string, remainingArgs []string) bool {
switch classifyArg(arg) {
case argEmpty, argNoDash, argSingleDash:
// Stop parsing and keep arg in g.args.
return true
case argDoubleDash:
// Stop parsing but drop "--" from g.args.
g.args = remainingArgs
return true
default:
// Keep parsing and don't change g.args.
return false
}
}
func (g *Group) parseByArgType(arg string, args []string) ([]string, error) {
switch classifyArg(arg) {
case argSingleDashOpt:
return g.parseOpt(arg[1:], args)
case argDoubleDashOpt:
return g.parseOpt(arg[2:], args)
default:
panic(fmt.Sprintf("opts: internal error: impossible arg %q", arg))
}
}
func (g *Group) parse(args []string) error {
g.args = args
for len(args) > 0 {
arg := args[0]
args = args[1:]
if g.shouldStopParsing(arg, args) {
return nil
}
var err error
args, err = g.parseByArgType(arg, args)
if err != nil {
return err
}
g.args = args
}
return nil
}
func (g *Group) parseOpt(arg string, args []string) ([]string, error) {
name, value, eqFound := strings.Cut(arg, "=")
opt, ok := g.opts[name]
if !ok {
return nil, fmt.Errorf("opts: --%s: %w", name, ErrUnknownOption)
}
if eqFound {
return parseEquals(opt, name, value, arg, args)
}
return parseSpaced(opt, name, args)
}
func parseEquals(opt *opt, name, value, arg string, args []string) ([]string, error) {
if opt.isBool {
return nil, fmt.Errorf("opts: --%s=%s: %w", name, value, ErrBooleanWithValue)
}
if err := opt.value.set(value); err != nil {
// Distinguish no value from a bad value.
if value == "" {
return nil, fmt.Errorf("opts: --%s=: %w", name, ErrMissingValue)
}
return nil, &InvalidValueError{
Option: name,
Value: value,
Err: err,
}
}
// A string option `--foo=` will not produce an error when calling set.
// `--foo=` amounts to `--foo=""`, and the empty string is a valid
// string value. However, for consistency with other option types, we
// should return an error indicating that there is no value.
if value == "" && arg[len(arg)-1] == '=' {
return nil, fmt.Errorf("opts: --%s=: %w", name, ErrMissingValue)
}
return args, nil
}
func parseSpaced(opt *opt, name string, args []string) ([]string, error) {
var value string
switch {
case opt.isBool:
value = "true"
case len(args) > 0:
value, args = args[0], args[1:]
default:
return nil, fmt.Errorf("opts: --%s: %w", name, ErrMissingValue)
}
if err := opt.value.set(value); err != nil {
return nil, &InvalidValueError{
Option: name,
Value: value,
Err: err,
}
}
return args, nil
}