-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathapp_parse.go
More file actions
464 lines (394 loc) · 15.2 KB
/
app_parse.go
File metadata and controls
464 lines (394 loc) · 15.2 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
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
package warg
import (
"errors"
"fmt"
"os"
"sort"
"go.bbkane.com/warg/colerr"
"go.bbkane.com/warg/config"
"go.bbkane.com/warg/metadata"
"go.bbkane.com/warg/path"
"go.bbkane.com/warg/set"
"go.bbkane.com/warg/value"
)
// -- moved from app_parse_cli.go
// ParseOpts allows overriding the default inputs to the Parse function. Useful for tests. Create it using the [go.bbkane.com/warg/parseopt] package.
type ParseOpts struct {
// Args []string
// ParseMetadata for unstructured data. Useful for setting up mocks for tests (i.e., pass in in memory database and use it if it's here in the context)
ParseMetadata metadata.Metadata
LookupEnv LookupEnv
// Stderr will be passed to [CmdContext] for user commands to print to.
// This file is never closed by warg, so if setting to something other than stderr/stdout,
// remember to close the file after running the command.
// Useful for saving output for tests. Defaults to os.Stderr if not passed
Stderr *os.File
// Stdin will be passed to [CmdContext] for user commands to read from.
// This file is never closed by warg, so if setting to something other than stdin/stdout,
// remember to close the file after running the command.
// Useful for saving input for tests. Defaults to os.Stdin if not passed
Stdin *os.File
// Stdout will be passed to [CmdContext] for user commands to print to.
// This file is never closed by warg, so if setting to something other than stderr/stdout,
// remember to close the file after running the command.
// Useful for saving output for tests. Defaults to os.Stdout if not passed
Stdout *os.File
}
type ParseOpt func(*ParseOpts)
func NewParseOpts(opts ...ParseOpt) ParseOpts {
parseOptHolder := ParseOpts{
ParseMetadata: metadata.Empty(),
LookupEnv: os.LookupEnv,
Stderr: os.Stderr,
Stdin: os.Stdin,
Stdout: os.Stdout,
}
for _, opt := range opts {
opt(&parseOptHolder)
}
return parseOptHolder
}
// ParseResult holds the result of parsing the command line.
type ParseResult struct {
Context CmdContext
// Action holds the passed command's action to execute.
Action Action
}
// -- FlagValueMap
// ValueMap holds flag values. If produced as part of [ParseState], it will be fully resolved (i.e., config/env/defaults applied if possible).
type ValueMap map[string]value.Value
func (m ValueMap) ToPassedFlags() PassedFlags {
pf := make(PassedFlags)
for name, v := range m {
if v.UpdatedBy() != value.UpdatedByUnset {
pf[string(name)] = v.Get()
}
}
return pf
}
// IsSet returns true if the flag with the given name has been set to a non-empty value (i.e., not its empty constructor value). Assumes the flag exists in the map.
func (m ValueMap) IsSet(flagName string) bool {
return m[flagName].UpdatedBy() != value.UpdatedByUnset
}
// -- ParseState
// ParseArgState represents the current "thing" we want from the args. It transitions as we parse each incoming argument and match it to the expected application structure
type ParseArgState string
const (
ParseArgState_WantSectionOrCmd ParseArgState = "ParseArgState_WantSectionOrCmd"
ParseArgState_WantFlagNameOrEnd ParseArgState = "ParseArgState_WantFlagNameOrEnd"
ParseArgState_WantFlagValue ParseArgState = "ParseArgState_WantFlagValue"
)
// ParseState holds the current state of parsing the command line arguments, as well as fully resolving all flag values (including from config/env/defaults).
//
// See ParseArgState for which fields are valid:
//
// - [ParseArgState_WantSectionOrCmd]: only CurrentSection, SectionPath are valid
// - [ParseArgState_WantFlagNameOrEnd], [ParseArgState_WantFlagValue]: all fields valid!
type ParseState struct {
ParseArgState ParseArgState
SectionPath []string
CurrentSection *Section
CurrentCmdName string
CurrentCmd *Cmd
CurrentCmdForwardedArgs []string
CurrentFlagName string
CurrentFlag *Flag
// FlagValues holds all flag values, including global and command flags, keyed by flag name. It is always non-nil, and is filled with empty values for global flags at the start of parsing, and for command flags when a command is selected (state != [ParseArgState_WantSectionOrCmd]). These flags are updated with non-empty values as flags are resolved.
FlagValues ValueMap
UnsetFlagNames set.Set[string]
HelpPassed bool
}
// parseArgs parses the args into a ParseState. It does not resolve flag values from config/env/defaults, only from the command line, so call resolveFlags afterwards to get a resolved ParseState.
func (app *App) parseArgs(args []string) (ParseState, error) {
pr := ParseState{
ParseArgState: ParseArgState_WantSectionOrCmd,
SectionPath: nil,
CurrentSection: &app.RootSection,
CurrentCmdName: "",
CurrentCmd: nil,
CurrentCmdForwardedArgs: nil,
CurrentFlagName: "",
CurrentFlag: nil,
FlagValues: make(ValueMap),
UnsetFlagNames: set.New[string](),
HelpPassed: false,
}
aliasToFlagName := make(map[string]string)
for flagName, fl := range app.GlobalFlags {
if fl.Alias != "" {
aliasToFlagName[string(fl.Alias)] = flagName
}
}
// fill the FlagValues map with empty values from the app
for flagName := range app.GlobalFlags {
val := app.GlobalFlags[flagName].EmptyValueConstructor()
pr.FlagValues[flagName] = val
}
for i, arg := range args {
// --help <helptype> or --help must be the last thing passed and can appear at any state we aren't expecting a flag value
if i >= len(args)-2 &&
arg != "" && // just in case there's not help flag alias
(arg == app.HelpFlagName || arg == app.GlobalFlags[app.HelpFlagName].Alias) &&
pr.ParseArgState != ParseArgState_WantFlagValue {
pr.HelpPassed = true
// set the value of --help if an arg was passed, otherwise let it resolve with the rest of them...
if i == len(args)-2 {
err := pr.FlagValues[app.HelpFlagName].Update(args[i+1], value.UpdatedByFlag)
if err != nil {
return pr, colerr.NewWrapped(err, "error updating help flag")
}
}
return pr, nil
}
switch pr.ParseArgState {
case ParseArgState_WantSectionOrCmd:
if childSection, exists := pr.CurrentSection.Sections[string(arg)]; exists {
pr.CurrentSection = &childSection
pr.SectionPath = append(pr.SectionPath, arg)
} else if childCommand, exists := pr.CurrentSection.Cmds[string(arg)]; exists {
pr.CurrentCmd = &childCommand
pr.CurrentCmdName = string(arg)
// fill the FlagValues map with empty values from the command
// All names in (command flag names, command flag aliases, global flag names, global flag aliases)
// should be unique because app.Validate should have caught any conflicts
for flagName, f := range pr.CurrentCmd.Flags {
pr.FlagValues[flagName] = f.EmptyValueConstructor()
if f.Alias != "" {
aliasToFlagName[string(f.Alias)] = flagName
}
}
pr.ParseArgState = ParseArgState_WantFlagNameOrEnd
} else {
choices := make([]string, 0, len(pr.CurrentSection.Sections)+len(pr.CurrentSection.Cmds))
choices = append(choices, pr.CurrentSection.Sections.SortedNames()...)
choices = append(choices, pr.CurrentSection.Cmds.SortedNames()...)
return pr, colerr.ArgChoiceError{
Message: "expecting section or command",
Arg: arg,
Choices: choices,
}
}
case ParseArgState_WantFlagNameOrEnd:
flagName := arg
// check if we need to handle forwarded args
if flagName == "--" && pr.CurrentCmd.AllowForwardedArgs {
if i >= len(args)-1 {
return pr, errors.New("expecting forwarded args after --")
}
// all remaining args are forwarded args
pr.CurrentCmdForwardedArgs = append(pr.CurrentCmdForwardedArgs, args[i+1:]...)
return pr, nil
}
if actualFlagName, exists := aliasToFlagName[flagName]; exists {
flagName = actualFlagName
}
fl := findFlag(flagName, app.GlobalFlags, pr.CurrentCmd.Flags)
if fl == nil {
choices := make([]string, 0, len(app.GlobalFlags)+len(pr.CurrentCmd.Flags))
choices = append(choices, app.GlobalFlags.SortedNames()...)
choices = append(choices, pr.CurrentCmd.Flags.SortedNames()...)
// Also include aliases
aliases := make([]string, 0)
for alias := range aliasToFlagName {
aliases = append(aliases, alias)
}
sort.Strings(aliases)
choices = append(choices, aliases...)
return pr, colerr.ArgChoiceError{
Message: "expecting flag name",
Arg: arg,
Choices: choices,
}
}
pr.CurrentFlagName = flagName
pr.CurrentFlag = fl
pr.ParseArgState = ParseArgState_WantFlagValue
case ParseArgState_WantFlagValue:
// if the flag has an unset sentinel and the user passed it, unset the flag
// NOTE: UnsetSentinel must be a pointer to a string, because sometimes the user may pass an empty string
if pr.CurrentFlag.UnsetSentinel != nil && arg == *pr.CurrentFlag.UnsetSentinel {
pr.FlagValues[pr.CurrentFlagName] = pr.CurrentFlag.EmptyValueConstructor()
pr.UnsetFlagNames.Add(pr.CurrentFlagName)
} else {
err := pr.FlagValues[pr.CurrentFlagName].Update(arg, value.UpdatedByFlag)
if err != nil {
return pr, err
}
pr.UnsetFlagNames.Delete(pr.CurrentFlagName)
}
pr.ParseArgState = ParseArgState_WantFlagNameOrEnd
default:
panic("unexpected state: " + pr.ParseArgState)
}
}
return pr, nil
}
func findFlag(flagName string, globalFlags FlagMap, currentCommandFlags FlagMap) *Flag {
if fl, exists := globalFlags[flagName]; exists {
return &fl
}
if fl, exists := currentCommandFlags[flagName]; exists {
return &fl
}
return nil
}
func resolveFlag(
flagName string,
fl Flag,
flagValues ValueMap, // this gets updated - all other params are readonly
configReader config.Reader,
lookupEnv LookupEnv,
unsetFlagNames set.Set[string],
) error {
// don't update if its been explicitly unset or already set
if unsetFlagNames.Contains(flagName) || flagValues[flagName].UpdatedBy() != value.UpdatedByUnset {
return nil
}
// config
if fl.ConfigPath != "" && configReader != nil {
fpr, err := configReader.Search(fl.ConfigPath)
if err != nil {
return err
}
if fpr != nil {
err := flagValues[flagName].ReplaceFromInterface(fpr.IFace, value.UpdatedByConfig)
if err != nil {
return colerr.NewWrappedf(
err,
"could not replace container type value:\nval:\n%s\nreplacement:\n%s",
fmt.Sprintf("%#v", flagValues[flagName]),
fmt.Sprintf("%#v", fpr.IFace),
)
}
return nil
}
}
// envvar
for _, e := range fl.EnvVars {
val, exists := lookupEnv(e)
if exists {
err := flagValues[flagName].Update(val, value.UpdatedByEnvVar)
if err != nil {
return colerr.NewWrappedf(err, "error updating flag %s from envvar %s", fmt.Sprintf("%v", flagName), fmt.Sprintf("%v", val))
}
// Use first env var found
return nil
}
}
// default
if flagValues[flagName].HasDefault() {
err := flagValues[flagName].ReplaceFromDefault(value.UpdatedByDefault)
if err != nil {
return colerr.NewWrappedf(err, "error updating flag %s from default", fmt.Sprintf("%v", flagName))
}
return nil
}
return nil
}
// resolveFlags resolves the config flag first, and then uses its values to resolve the rest of the flags.
func (app *App) resolveFlags(currentCmd *Cmd, flagValues ValueMap, lookupEnv LookupEnv, unsetFlagNames set.Set[string]) error {
// resolve config flag first and try to get a reader
var configReader config.Reader
if app.ConfigFlagName != "" {
err := resolveFlag(
app.ConfigFlagName, app.GlobalFlags[app.ConfigFlagName], flagValues, nil, lookupEnv, unsetFlagNames)
if err != nil {
return colerr.NewWrappedf(err, "resolveFlag error for flag %s", app.ConfigFlagName)
}
if flagValues[app.ConfigFlagName].UpdatedBy() != value.UpdatedByUnset {
configPath := flagValues[app.ConfigFlagName].Get().(path.Path)
configPathStr, err := configPath.Expand()
if err != nil {
return colerr.NewWrappedf(err, "error expanding config path ( %s ) ", configPath.String())
}
configReader, err = app.NewConfigReader(configPathStr)
if err != nil {
return colerr.NewWrappedf(err, "error reading config path ( %s ) ", configPath.String())
}
}
}
// resolve app global flags
for flagName, fl := range app.GlobalFlags {
err := resolveFlag(flagName, fl, flagValues, configReader, lookupEnv, unsetFlagNames)
if err != nil {
return colerr.NewWrappedf(err, "resolveFlag error for global flag %s", flagName)
}
}
// resolve current command flags
if currentCmd != nil { // can be nil in the case of --help
for flagName, fl := range currentCmd.Flags {
err := resolveFlag(flagName, fl, flagValues, configReader, lookupEnv, unsetFlagNames)
if err != nil {
return colerr.NewWrappedf(err, "resolveFlag error for command flag %s", flagName)
}
}
}
return nil
}
// Parse parses command line arguments, environment variables, and configuration files to produce a [ParseResult]. expects ParseOpts.Args to be like os.Args (i.e., first arg is app name). It returns an error if parsing fails or required flags are missing.
func (app *App) Parse(args []string, opts ...ParseOpt) (*ParseResult, error) {
parseOpts := NewParseOpts(opts...)
parseState, err := app.parseArgs(args)
if err != nil {
return nil, colerr.NewWrapped(err, "Parse args error")
}
// --help means we don't need to do a lot of error checking
if parseState.HelpPassed || parseState.ParseArgState == ParseArgState_WantSectionOrCmd {
err = app.resolveFlags(parseState.CurrentCmd, parseState.FlagValues, parseOpts.LookupEnv, parseState.UnsetFlagNames)
if err != nil {
return nil, err
}
helpType := parseState.FlagValues[app.HelpFlagName].Get().(string)
command := app.HelpCmds[helpType]
pr := ParseResult{
Context: CmdContext{
App: app,
ParseMetadata: parseOpts.ParseMetadata,
Flags: parseState.FlagValues.ToPassedFlags(),
ForwardedArgs: parseState.CurrentCmdForwardedArgs,
ParseState: &parseState,
Stderr: parseOpts.Stderr,
Stdin: parseOpts.Stdin,
Stdout: parseOpts.Stdout,
},
Action: command.Action,
}
return &pr, nil
}
// ok, we're running a real command, let's do the error checking
if parseState.ParseArgState != ParseArgState_WantFlagNameOrEnd {
return nil, colerr.NewWrappedf(nil, "unexpected parse state: %s", string(parseState.ParseArgState))
}
err = app.resolveFlags(parseState.CurrentCmd, parseState.FlagValues, parseOpts.LookupEnv, parseState.UnsetFlagNames)
if err != nil {
return nil, err
}
missingRequiredFlags := []string{}
for flagName, flag := range app.GlobalFlags {
if flag.Required && !parseState.FlagValues.IsSet(flagName) {
missingRequiredFlags = append(missingRequiredFlags, string(flagName))
}
}
for flagName, flag := range parseState.CurrentCmd.Flags {
if flag.Required && !parseState.FlagValues.IsSet(flagName) {
missingRequiredFlags = append(missingRequiredFlags, string(flagName))
}
}
if len(missingRequiredFlags) > 0 {
return nil, colerr.NewWrappedf(nil, "missing but required flags: %s", fmt.Sprintf("%s", missingRequiredFlags))
}
pr := ParseResult{
Context: CmdContext{
App: app,
ParseMetadata: parseOpts.ParseMetadata,
Flags: parseState.FlagValues.ToPassedFlags(),
ForwardedArgs: parseState.CurrentCmdForwardedArgs,
ParseState: &parseState,
Stderr: parseOpts.Stderr,
Stdin: parseOpts.Stdin,
Stdout: parseOpts.Stdout,
},
Action: parseState.CurrentCmd.Action,
}
return &pr, nil
}