-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapi.go
More file actions
543 lines (471 loc) · 18.3 KB
/
api.go
File metadata and controls
543 lines (471 loc) · 18.3 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
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
// Package verbs implements a POSIX-compatible command line parser and help
// screen generator.
//
// Parsing command-line arguments with this package is a two-step process:
// build a CLI structure containing commands, options, and positional
// arguments, then call verbs.NewParser(cli).Parse(os.Args) and iterate over
// the returned parsed command, options, and arguments.
//
// The package supports two modes of operation:
//
// - Command-driven: at least one command is registered; a command name
// must appear on the command line (e.g., "git commit").
// - Argument-only: only options and positional arguments are used
// (e.g., "ls -l /tmp").
package verbs
import (
"fmt"
"os"
"strings"
)
// Tag is an opaque, caller-supplied value used to identify options, arguments,
// and commands in parsed results. Tags can be of any type: integers, strings,
// pointers, functions, etc. If Tag is nil, the Name field is used as the tag.
type Tag any
// Option describes a named command line option.
// Options can be boolean switches or can accept a parameter value.
// The order of options in the CLI structure determines the order of options in
// the help screen. However, they can appear in any order on the command line.
//
// Depending on the OptionResolution setting, option names must be unique
// either globally or within their parent namespaces, which are: the CLI
// structure, zero or more nested namespaces, and the command.
type Option struct {
// Long and short option name variants separated by vertical bars.
// Short options are single characters (e.g., "v"), while long options
// are words (e.g., "verbose").
//
// Name is the only required field in the Option structure.
//
// Examples:
// "v|verbose" - accepts -v or --verbose
// "o|output-file" - accepts -o or --output-file
// "f|force|no-prompt" - accepts -f, --force, or --no-prompt
Name string
// Parameter name, if the option requires one.
// Leave empty for boolean flags.
// This value is used in help text and error messages; it does not
// enforce validation by itself.
//
// Examples:
// "" - boolean flag (e.g., --verbose)
// "PATH" - requires a value (e.g., --output-file=PATH)
// "<file>" - requires a value (e.g., --template=<file>)
Param string
// A brief description of what this option does.
// This text appears in help screens. If the option is used with
// multiple commands, ensure that the description is clear in the
// context of each command.
Description string
// An identifier used to match this option in the results returned
// by Parser.Parse. Can be any value: an integer constant, a string,
// a function, etc. If Tag is nil, Name is used as the identifier.
//
// Examples:
// Tag: "file",
// Tag: FileOption,
// Tag: func(opt *ParsedArg) { fmt.Printf("file: %s\n", opt.Value) },
Tag Tag
}
// Occurrence specifies the quantity constraint for positional arguments.
// It determines how many arguments of a given type are expected on the
// command line.
type Occurrence int
const (
// Required describes a positional argument that is required.
Required Occurrence = iota
// Optional describes a positional argument that can be omitted.
Optional
// OneOrMore describes a non-empty sequence of positional arguments.
OneOrMore
// ZeroOrMore describes an optional sequence of positional arguments.
ZeroOrMore
)
// Arg describes a positional command argument. Positional arguments must
// appear on the command line in the order they are defined in the CLI
// structure.
type Arg struct {
// The user-visible name of this argument.
// This name appears in help screens and error messages. Unlike
// options and commands, positional arguments cannot have aliases.
// Name is the only required field.
//
// Examples: "FILE", "source"
Name string
// Specifies how many arguments of this type are expected.
// See Occurrence constants for available options.
Occurrence Occurrence
// A brief description of this argument.
// This text appears in help screens. The description can be omitted
// if the meaning is obvious from the name.
Description string
// An identifier used to match this argument in the results returned
// by Parser.Parse. Can be any value: an integer constant, a string,
// a function, etc. If Tag is nil, Name is used as the identifier.
//
// Examples:
// Tag: "file",
// Tag: FileArg,
// Tag: func(arg *ParsedArg) { fmt.Printf("file: %s\n", arg.Value) },
Tag Tag
}
// Command describes a command - a verb that triggers one of the program's
// functions.
type Command struct {
// The name of the command.
// Command names can have aliases separated by vertical bars.
// Name is the only required field.
//
// Examples:
// "commit" - single name
// "add|stage" - with alias
// "rm|remove|delete" - multiple aliases
Name string
// A one-line summary of what the command does.
// This appears in command lists and should be concise (typically one
// sentence). Summary is always displayed before Description.
Summary string
// A detailed description of the command.
// This appears in the full help screen for the command.
// Because Summary is always printed first, Description should not
// repeat the summary but provide additional details.
Description string
// Command-specific options.
// These options are available only when this command is used.
// The command also inherits options from its containing namespace
// (if any) and from the root CLI structure.
Options []*Option
// Positional arguments accepted by this command.
Args []*Arg
// An identifier returned by Parser.Parse when this command is
// selected by the user. Can be any value: an integer constant,
// a string, a function, etc. If Tag is nil, Name is used as the
// identifier.
//
// Examples:
// Tag: "commit",
// Tag: CommitCommand,
// Tag: func(cmd *ParsedCommand) { fmt.Println(cmd.CommandName) },
Tag Tag
}
// Namespace represents a group of semantically related commands and nested
// namespaces. Namespaces affect command line parsing: commands must be
// prefixed with the namespace name (e.g., "git remote add").
type Namespace struct {
// The name of this namespace.
// Namespace names can have aliases separated by vertical bars.
// Name is the only required field.
//
// Example: "remote" in "git remote add"
Name string
// A one-line summary of what this namespace contains.
// This appears in namespace lists and should be concise.
Summary string
// A detailed description of the namespace.
// This appears in help screens. Because Summary is always printed
// first, Description should provide additional context.
Description string
// Commands available within this namespace.
// These commands are accessed by prefixing them with the namespace
// name (e.g., "remote add", "remote remove").
Commands []*Command
// Nested namespaces within this namespace.
// Nesting namespaces allows for hierarchical command structures.
Namespaces []*Namespace
// Options available to all commands in this namespace and any nested
// namespaces. These options are inherited by commands defined in this
// namespace.
Options []*Option
}
// HelpTopic represents a help screen that describes a general concept or
// procedure that does not pertain to any particular command or namespace.
// Help topics are accessed via the 'help' command or the '--help' option.
//
// Examples: "tutorial", "configuration", "environment-variables"
type HelpTopic struct {
// The keyword used to access this help topic.
// Users can view this topic by running "help <keyword>".
// The keyword is recognized alongside command and namespace names.
//
// Example: "tutorial", "config", "examples"
Keyword string
// A optional one-line summary of what this help topic covers.
Summary string
// The full text content of this help topic.
// This text is displayed when the user requests help for this topic.
Text string
}
// ErrorHandler allows the caller to intercept parsing errors.
// If the caller does not provide an error handler to Parser.Parse, it will
// print the error message and terminate the program with exit code 2.
type ErrorHandler func(err error)
// HelpHandler allows the caller to intercept help requests.
// If the caller does not provide a help handler to Parser.Parse,
// it will print the help text and exit the program with exit code 0.
type HelpHandler func(helpText string)
type OptionResolution int
const (
// Global option name resolution requires all options to have
// unique names across all namespaces and commands.
// This allows users to place options anywhere in the command line.
// For example, "git commit --amend" and "git --amend commit"
// are both valid.
Global OptionResolution = iota
// Scoped option name resolution requires option names to be unique
// only within their parent namespaces and commands.
// This requires users to place options after the namespace or
// command to which they belong. For example, "git commit --amend"
// is valid, but "git --amend commit" is not.
Scoped
)
// CLI is a tree-like structure of command line interface definitions.
type CLI struct {
// The name of the program binary.
//
// Program name is used in error messages and help screens. If this
// field is not provided, the first element of the args parameter of
// Parser.Parse is used.
ProgramName string
// A one-line program description.
Summary string
// An optional longer program description.
Description string
// Program version information.
// This text is printed verbatim in response to the '--version' option.
// If Version is empty, the '--version' option is not recognized.
//
// Example: "1.2.3" or "my-cli v1.2.3 (build abc123)"
Version string
// Controls whether global or scoped option name resolution is used.
// Defaults to Global.
OptionResolution OptionResolution
// The maximum width of help text in columns. Text is wrapped to this
// width. Defaults to 72 columns. A zero value applies the default.
HelpTextWidth int
// The column number where command descriptions start in command lists.
// This controls alignment of command descriptions in help output.
// Defaults to column 24. A zero value applies the default.
CommandDescriptionIndent int
// The column number where option descriptions start in option lists.
// This controls alignment of option descriptions in help output.
// Defaults to column 32. A zero value applies the default.
OptionDescriptionIndent int
// Top-level namespaces.
// Namespaces affect command line parsing: commands must be prefixed
// with the namespace name.
Namespaces []*Namespace
// Top-level commands not included in any namespace.
// These commands can be invoked directly without a namespace prefix.
Commands []*Command
// Global options accepted by all commands.
// In argument-only mode (when no commands are defined), these options
// are available to the program itself. Options are inherited by
// commands from their containing namespace and the root CLI.
Options []*Option
// Positional arguments for argument-only mode.
// These arguments are only used when no commands are defined.
// In command-driven mode, use Args on individual Command structures.
Args []*Arg
// General help topics accessible via the 'help' command.
// These topics are offered alongside namespaces and standalone
// commands in the main help screen.
HelpTopics []*HelpTopic
// Error handler to intercept parsing errors.
OnError ErrorHandler
// Help handler to intercept help requests.
OnHelp HelpHandler
}
// ParsedCommand identifies the command that the user requested to execute.
// This is returned by Parser.Parse in command-driven mode.
type ParsedCommand struct {
// The tag of the command as provided when the command was defined.
// Use this to identify which command was selected.
// If the command's Tag was nil, this will be the command's Name.
Tag Tag
// The command name as specified by the user on the command line.
// For commands with aliases, this indicates which alias was used,
// which is useful for deprecation warnings or alias-specific behavior.
//
// Example: If a command has Name "add|stage" and the user types
// "stage", Token will be "stage".
Token string
}
// ParsedArg represents a parsed option or positional argument from the
// command line. Instances of this structure are returned by Parser.Parse
// in the order they appeared on the command line.
type ParsedArg struct {
// The tag of the option or argument as provided when it was defined.
// Use this to identify which option or argument this value
// corresponds to. If the Tag field was nil, this will be the Name
// field from the Option or Arg definition.
Tag Tag
// The value extracted from the command line.
// For options with parameters, this is the parameter value.
// For boolean options (switches), this is always the string "true".
// For positional arguments, this is the argument value.
//
// Examples:
// For option "--output file.txt", Value would be "file.txt".
// For positional argument "source.go", Value would be "source.go".
// For boolean option "--verbose", Value would be "true".
Value string
// Token is the exact option token as it appeared on the
// command line.
//
// It is set only for values originating from an option.
// For positional arguments, Token is "".
//
// This is useful for producing accurate error messages and for
// distinguishing option-derived values from positional arguments.
//
// Examples:
// For option "--output=file.txt", Token would be "--output".
// For option "-ofile.txt", Token would be "-o".
// For positional argument "file.txt", Token would be "".
Token string
}
// ParseResult is the result of parsing a command line.
type ParseResult struct {
// The command that the user requested to execute.
// In argument-only mode, this will be nil.
Command *ParsedCommand
// Option and argument values in the order they appeared
// on the command line.
OptsAndArgs []*ParsedArg
// The error handler that the parser would use if it were to
// encounter an error. This can be useful to report further
// command line validation errors.
HandleError ErrorHandler
// The name of the program that was parsed. This is taken from either
// the CLI configuration or the first argument to Parser.Parse.
ProgramName string
// The sequence of namespace names and the command name that the user
// requested to execute.
ScopePath []string
}
// NewParser creates a Parser from a CLI configuration.
// cli defines the supported commands, options, positional arguments,
// and help text. If there is any configuration error, NewParser will panic.
func NewParser(cli *CLI) *Parser {
p := &Parser{
cli: cli,
optionMap: make(map[string]*Option),
CommandSummaryPrefix: "- ",
OptionDescriptionSeparator: " :",
}
p.globalScope = p.newScope(nil, cli.Options, cli.Args, nil)
p.globalScope.buildNestedScopes(p, nil, cli.Namespaces, cli.Commands)
// Additional setup and validation for command-driven mode
if p.globalScope.nestedScopes != nil {
// Inject the help command unless a custom help command
// is defined
if p.globalScope.nestedScopes[helpCommand.Name] == nil {
helpCommandScope := p.newScope(nil,
helpCommand.Options, helpCommand.Args, nil)
helpCommandScope.command = helpCommand
p.globalScope.nestedScopes[helpCommand.Name] =
helpCommandScope
}
// Check that help topics do not conflict with command or
// namespace names
for _, helpTopic := range cli.HelpTopics {
if helpTopic.Keyword == "" {
panic("help topic keyword cannot be empty")
}
if p.globalScope.nestedScopes[helpTopic.Keyword] != nil {
panic(fmt.Sprintf("help topic '%s' conflicts "+
"with another name in the global scope",
helpTopic.Keyword))
}
}
// Global arguments are not allowed in command-driven mode
if len(cli.Args) > 0 {
panic("global arguments are not allowed " +
"when commands are defined")
}
} else {
// Help topics are not allowed in argument-only mode
if len(cli.HelpTopics) > 0 {
panic("help topics are not allowed " +
"when no commands are defined")
}
}
return p
}
// Parse parses a command line.
//
// args must contain all arguments as provided by os.Args, including the
// program name at args[0].
//
// Parse operates in two modes:
//
// - Command-driven: at least one command is registered in the CLI.
// A command must be present on the command line. A built-in 'help'
// command is always available.
//
// - Argument-only: no commands are registered. Only options and positional
// arguments are parsed. A built-in --help option is always available.
//
// On success:
//
// - command contains the requested command in command-driven mode, or
// nil in argument-only mode.
// - optsAndArgs contains all parsed options and positional arguments,
// in the exact order they appeared on the command line.
//
// Errors and help:
//
// If the number of positional arguments is incorrect, or no command is
// given in command-driven mode, Parse prints an error message and
// terminates the process with exit code 2, unless an error handler
// is configured.
//
// If the user requests help, Parse prints the requested help screen and
// terminates the process with exit code 0, unless a help handler is
// configured.
func (p *Parser) Parse(args []string) ParseResult {
ps := newParseState(p)
err := ps.parse(args)
handleError := p.cli.OnError
if handleError == nil {
handleError = func(err error) {
var helpCommand string
if ps.parser.globalScope.nestedScopes == nil {
helpCommand = "--help"
} else if len(ps.currentScopePath) == 0 {
helpCommand = "help"
} else {
helpCommand = "help " +
strings.Join(ps.currentScopePath, " ")
}
fmt.Fprintf(os.Stderr,
"%s: %s\n\nRun \"%s %s\" for usage\n",
ps.programName, err.Error(),
ps.programName, helpCommand)
os.Exit(2)
}
}
switch err {
case errHelpRequested:
ps.printHelpForCurrentScope()
case errVersionRequested:
ps.printHelpText(ps.parser.cli.Version + "\n")
case nil:
if ps.parsedCommand == nil ||
ps.parsedCommand.Tag != helpCommand.Tag {
return ParseResult{
Command: ps.parsedCommand,
OptsAndArgs: ps.optsAndArgs,
HandleError: handleError,
ProgramName: ps.programName,
ScopePath: ps.currentScopePath,
}
}
if err = ps.handleHelpCommand(); err != nil {
handleError(err)
}
default:
handleError(err)
}
return ParseResult{}
}