-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathcli.go
204 lines (179 loc) · 6.96 KB
/
cli.go
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
package mybase
import (
"errors"
"fmt"
"strings"
)
// CommandLine stores state relating to executing an application.
type CommandLine struct {
InvokedAs string // How the bin was invoked; e.g. os.Args[0]
Command *Command // Which command (or subcommand) is being executed
OptionValues map[string]string // Option values parsed from the command-line
ArgValues []string // Positional arg values (does not include InvokedAs or Command.Name)
}
// OptionValue returns the value for the requested option if it was specified
// on the command-line. This is satisfies the OptionValuer interface, allowing
// Config to use the command-line as the highest-priority option provider.
func (cli *CommandLine) OptionValue(optionName string) (string, bool) {
value, ok := cli.OptionValues[optionName]
return value, ok
}
func (cli *CommandLine) parseLongArg(arg string, args *[]string, longOptionIndex map[string]*Option) error {
key, value, hasValue, loose := NormalizeOptionToken(arg)
opt, found := longOptionIndex[key]
if !found {
if loose {
return nil
}
return OptionNotDefinedError{key, "CLI"}
}
// Use returned hasValue boolean instead of comparing value to "", since "" may
// be set explicitly (--some-opt='') or implicitly (--skip-some-bool-opt) and
// both of those cases treat hasValue=true
if !hasValue {
if opt.RequireValue {
// Value required: slurp next arg to allow format "--foo bar" in addition to "--foo=bar"
if len(*args) == 0 || strings.HasPrefix((*args)[0], "-") {
return OptionMissingValueError{opt.Name, "CLI"}
}
value = (*args)[0]
*args = (*args)[1:]
} else if opt.Type == OptionTypeBool {
// Boolean without value is treated as true
value = "1"
}
} else if value == "" && opt.Type == OptionTypeString {
// Convert empty strings into quote-wrapped empty strings, so that callers
// may differentiate between bare "--foo" vs "--foo=" if desired, by using
// Config.GetRaw(). Meanwhile Config.Get and most other getters strip
// surrounding quotes, so this does not break anything.
value = "''"
}
cli.OptionValues[opt.Name] = value
return nil
}
func (cli *CommandLine) parseShortArgs(arg string, args *[]string, shortOptionIndex map[rune]*Option) error {
runeList := []rune(arg)
var done bool
for len(runeList) > 0 && !done {
short := runeList[0]
runeList = runeList[1:]
var value string
opt, found := shortOptionIndex[short]
if !found {
return OptionNotDefinedError{string(short), "CLI"}
}
// Consume value. Depending on the option, value may be supplied as chars immediately following
// this one, or after a space as next arg on CLI.
if len(runeList) > 0 && opt.Type != OptionTypeBool { // "-xvalue", only supported for non-bools
value = string(runeList)
done = true
} else if opt.RequireValue { // "-x value", only supported if opt requires a value
if len(*args) > 0 && !strings.HasPrefix((*args)[0], "-") {
value = (*args)[0]
*args = (*args)[1:]
} else {
return OptionMissingValueError{opt.Name, "CLI"}
}
} else { // "-xyz", parse x as a valueless option and loop again to parse y (and possibly z) as separate shorthand options
if opt.Type == OptionTypeBool {
value = "1" // booleans handle lack of value as being true, whereas other types keep it as empty string
}
}
cli.OptionValues[opt.Name] = value
}
return nil
}
func (cli *CommandLine) String() string {
// Don't reveal the actual command-line value, since it may contain something
// sensitive (even though it shouldn't!)
return "command line"
}
// ParseCLI parses the command-line to generate a CommandLine, which
// stores which (sub)command was used, named option values, and positional arg
// values. The CommandLine will then be wrapped in a Config for returning.
//
// The supplied cmd should typically be a root Command (one with nil
// ParentCommand), but this is not a requirement.
//
// The supplied args should match format of os.Args; i.e. args[0]
// should contain the program name.
func ParseCLI(cmd *Command, args []string) (*Config, error) {
if len(args) == 0 {
return nil, errors.New("ParseCLI: No command-line supplied")
}
cli := &CommandLine{
Command: cmd,
InvokedAs: args[0],
OptionValues: make(map[string]string),
ArgValues: make([]string, 0),
}
args = args[1:]
// Index options by shorthand
longOptionIndex := cmd.Options()
shortOptionIndex := make(map[rune]*Option, len(longOptionIndex))
for name, opt := range longOptionIndex {
if opt.Shorthand != 0 {
if _, already := shortOptionIndex[opt.Shorthand]; already {
panic(fmt.Errorf("Command %s defines multiple conflicting options with short-form -%c", cmd.Name, opt.Shorthand))
}
shortOptionIndex[opt.Shorthand] = longOptionIndex[name]
}
}
var noMoreOptions bool
// Iterate over the cli args and process each in turn
for len(args) > 0 {
arg := args[0]
args = args[1:]
switch {
// option terminator
case arg == "--":
noMoreOptions = true
// long option
case len(arg) > 2 && arg[0:2] == "--" && !noMoreOptions:
if err := cli.parseLongArg(arg[2:], &args, longOptionIndex); err != nil {
return nil, err
}
// short option(s) -- multiple bools may be combined into one
case len(arg) > 1 && arg[0] == '-' && !noMoreOptions:
if err := cli.parseShortArgs(arg[1:], &args, shortOptionIndex); err != nil {
return nil, err
}
// first positional arg is command name if the current command is a command suite
case len(cli.Command.SubCommands) > 0:
command, validCommand := cli.Command.SubCommands[arg]
if !validCommand {
return nil, fmt.Errorf("Unknown command \"%s\"", arg)
}
cli.Command = command
// Add the options of the new command into our maps. Any name conflicts
// intentionally override parent versions.
for name, opt := range command.options {
longOptionIndex[name] = command.options[name]
if opt.Shorthand != 0 {
shortOptionIndex[opt.Shorthand] = command.options[name]
}
}
// supplying help or version as first positional arg to a non-command-suite:
// treat as if supplied as option instead
case len(cli.ArgValues) == 0 && (arg == "help" || arg == "version"):
if err := cli.parseLongArg(arg, &args, longOptionIndex); err != nil {
return nil, err
}
// superfluous positional arg
case len(cli.ArgValues) >= len(cli.Command.args):
return nil, fmt.Errorf("Extra command-line arg \"%s\" supplied; command %s takes a max of %d args", arg, cli.Command.Name, len(cli.Command.args))
// positional arg
default:
cli.ArgValues = append(cli.ArgValues, arg)
}
}
if _, helpWanted := cli.OptionValues["help"]; !helpWanted && len(cli.ArgValues) < cli.Command.minArgs() {
return nil, fmt.Errorf("Too few positional args supplied on command line; command %s requires at least %d args", cli.Command.Name, cli.Command.minArgs())
}
// If no command supplied on a command suite, redirect to help subcommand
if len(cli.Command.SubCommands) > 0 {
cli.Command = cli.Command.SubCommands["help"]
}
return NewConfig(cli), nil
}