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
}
|