2015-10-31 20:26:58 -05:00
|
|
|
package arg
|
2015-10-31 18:15:24 -05:00
|
|
|
|
|
|
|
import (
|
2019-10-20 01:23:32 -05:00
|
|
|
"encoding"
|
2018-05-01 04:02:44 -05:00
|
|
|
"encoding/csv"
|
2015-11-01 01:57:26 -05:00
|
|
|
"errors"
|
2015-10-31 18:15:24 -05:00
|
|
|
"fmt"
|
|
|
|
"os"
|
2016-01-18 12:31:01 -06:00
|
|
|
"path/filepath"
|
2015-10-31 18:15:24 -05:00
|
|
|
"reflect"
|
|
|
|
"strings"
|
2017-02-15 20:19:41 -06:00
|
|
|
|
|
|
|
scalar "github.com/alexflint/go-scalar"
|
2015-10-31 18:15:24 -05:00
|
|
|
)
|
|
|
|
|
2019-05-03 15:07:12 -05:00
|
|
|
// to enable monkey-patching during tests
|
|
|
|
var osExit = os.Exit
|
|
|
|
|
2019-04-30 15:30:23 -05:00
|
|
|
// path represents a sequence of steps to find the output location for an
|
|
|
|
// argument or subcommand in the final destination struct
|
|
|
|
type path struct {
|
|
|
|
root int // index of the destination struct
|
|
|
|
fields []string // sequence of struct field names to traverse
|
|
|
|
}
|
|
|
|
|
|
|
|
// String gets a string representation of the given path
|
|
|
|
func (p path) String() string {
|
2019-05-02 11:50:44 -05:00
|
|
|
if len(p.fields) == 0 {
|
|
|
|
return "args"
|
|
|
|
}
|
2019-04-30 15:30:23 -05:00
|
|
|
return "args." + strings.Join(p.fields, ".")
|
|
|
|
}
|
|
|
|
|
|
|
|
// Child gets a new path representing a child of this path.
|
|
|
|
func (p path) Child(child string) path {
|
|
|
|
// copy the entire slice of fields to avoid possible slice overwrite
|
|
|
|
subfields := make([]string, len(p.fields)+1)
|
|
|
|
copy(subfields, append(p.fields, child))
|
|
|
|
return path{
|
|
|
|
root: p.root,
|
|
|
|
fields: subfields,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-10-31 20:26:58 -05:00
|
|
|
// spec represents a command line option
|
|
|
|
type spec struct {
|
2019-11-29 15:22:21 -06:00
|
|
|
dest path
|
|
|
|
typ reflect.Type
|
|
|
|
long string
|
|
|
|
short string
|
|
|
|
multiple bool
|
|
|
|
required bool
|
|
|
|
positional bool
|
|
|
|
separate bool
|
|
|
|
help string
|
|
|
|
env string
|
|
|
|
boolean bool
|
|
|
|
defaultVal string // default value for this option
|
|
|
|
placeholder string // name of the data in help
|
2015-10-31 20:26:58 -05:00
|
|
|
}
|
|
|
|
|
2019-04-14 21:50:17 -05:00
|
|
|
// command represents a named subcommand, or the top-level command
|
|
|
|
type command struct {
|
|
|
|
name string
|
2019-05-03 17:02:10 -05:00
|
|
|
help string
|
2019-04-30 15:30:23 -05:00
|
|
|
dest path
|
2019-04-14 21:50:17 -05:00
|
|
|
specs []*spec
|
|
|
|
subcommands []*command
|
2019-05-03 17:49:44 -05:00
|
|
|
parent *command
|
2019-04-14 21:50:17 -05:00
|
|
|
}
|
|
|
|
|
2015-11-01 01:13:23 -06:00
|
|
|
// ErrHelp indicates that -h or --help were provided
|
2015-11-01 01:57:26 -05:00
|
|
|
var ErrHelp = errors.New("help requested by user")
|
|
|
|
|
2016-09-08 23:18:19 -05:00
|
|
|
// ErrVersion indicates that --version was provided
|
|
|
|
var ErrVersion = errors.New("version requested by user")
|
|
|
|
|
2015-11-01 01:13:23 -06:00
|
|
|
// MustParse processes command line arguments and exits upon failure
|
2016-01-05 15:52:33 -06:00
|
|
|
func MustParse(dest ...interface{}) *Parser {
|
2016-01-18 12:31:01 -06:00
|
|
|
p, err := NewParser(Config{}, dest...)
|
2015-11-01 01:57:26 -05:00
|
|
|
if err != nil {
|
|
|
|
fmt.Println(err)
|
2019-05-03 15:07:12 -05:00
|
|
|
osExit(-1)
|
2019-05-03 17:49:44 -05:00
|
|
|
return nil // just in case osExit was monkey-patched
|
2015-11-01 01:57:26 -05:00
|
|
|
}
|
2019-05-03 15:07:12 -05:00
|
|
|
|
2017-02-09 17:12:33 -06:00
|
|
|
err = p.Parse(flags())
|
2019-05-03 15:07:12 -05:00
|
|
|
switch {
|
|
|
|
case err == ErrHelp:
|
2019-05-03 17:49:44 -05:00
|
|
|
p.writeHelpForCommand(os.Stdout, p.lastCmd)
|
2019-05-03 15:07:12 -05:00
|
|
|
osExit(0)
|
|
|
|
case err == ErrVersion:
|
2016-09-08 23:18:19 -05:00
|
|
|
fmt.Println(p.version)
|
2019-05-03 15:07:12 -05:00
|
|
|
osExit(0)
|
|
|
|
case err != nil:
|
2019-05-03 17:49:44 -05:00
|
|
|
p.failWithCommand(err.Error(), p.lastCmd)
|
2015-10-31 18:15:24 -05:00
|
|
|
}
|
2019-05-03 15:07:12 -05:00
|
|
|
|
2016-01-05 15:52:33 -06:00
|
|
|
return p
|
2015-10-31 18:15:24 -05:00
|
|
|
}
|
|
|
|
|
2015-11-01 01:13:23 -06:00
|
|
|
// Parse processes command line arguments and stores them in dest
|
2015-10-31 20:26:58 -05:00
|
|
|
func Parse(dest ...interface{}) error {
|
2016-01-18 12:31:01 -06:00
|
|
|
p, err := NewParser(Config{}, dest...)
|
2015-11-01 01:57:26 -05:00
|
|
|
if err != nil {
|
2015-11-01 01:13:23 -06:00
|
|
|
return err
|
2015-11-01 01:57:26 -05:00
|
|
|
}
|
2017-02-09 17:12:33 -06:00
|
|
|
return p.Parse(flags())
|
|
|
|
}
|
|
|
|
|
|
|
|
// flags gets all command line arguments other than the first (program name)
|
|
|
|
func flags() []string {
|
2017-02-15 20:24:32 -06:00
|
|
|
if len(os.Args) == 0 { // os.Args could be empty
|
2017-02-09 17:12:33 -06:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
return os.Args[1:]
|
2015-10-31 18:15:24 -05:00
|
|
|
}
|
|
|
|
|
2016-01-18 12:31:01 -06:00
|
|
|
// Config represents configuration options for an argument parser
|
|
|
|
type Config struct {
|
|
|
|
Program string // Program is the name of the program used in the help text
|
|
|
|
}
|
|
|
|
|
2015-11-01 01:57:26 -05:00
|
|
|
// Parser represents a set of command line options with destination values
|
|
|
|
type Parser struct {
|
2019-04-14 21:50:17 -05:00
|
|
|
cmd *command
|
|
|
|
roots []reflect.Value
|
2017-01-23 19:41:12 -06:00
|
|
|
config Config
|
|
|
|
version string
|
|
|
|
description string
|
2019-05-03 17:49:44 -05:00
|
|
|
|
|
|
|
// the following fields change curing processing of command line arguments
|
|
|
|
lastCmd *command
|
2016-09-08 23:18:19 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
// Versioned is the interface that the destination struct should implement to
|
|
|
|
// make a version string appear at the top of the help message.
|
|
|
|
type Versioned interface {
|
|
|
|
// Version returns the version string that will be printed on a line by itself
|
|
|
|
// at the top of the help message.
|
|
|
|
Version() string
|
2015-11-01 01:57:26 -05:00
|
|
|
}
|
2015-10-31 18:15:24 -05:00
|
|
|
|
2017-01-23 19:41:12 -06:00
|
|
|
// Described is the interface that the destination struct should implement to
|
|
|
|
// make a description string appear at the top of the help message.
|
|
|
|
type Described interface {
|
|
|
|
// Description returns the string that will be printed on a line by itself
|
|
|
|
// at the top of the help message.
|
|
|
|
Description() string
|
|
|
|
}
|
|
|
|
|
2016-10-09 19:18:28 -05:00
|
|
|
// walkFields calls a function for each field of a struct, recursively expanding struct fields.
|
2019-04-14 21:50:17 -05:00
|
|
|
func walkFields(t reflect.Type, visit func(field reflect.StructField, owner reflect.Type) bool) {
|
2016-10-09 19:18:28 -05:00
|
|
|
for i := 0; i < t.NumField(); i++ {
|
|
|
|
field := t.Field(i)
|
2019-04-14 21:50:17 -05:00
|
|
|
expand := visit(field, t)
|
2016-10-09 19:18:28 -05:00
|
|
|
if expand && field.Type.Kind() == reflect.Struct {
|
2019-04-14 21:50:17 -05:00
|
|
|
walkFields(field.Type, visit)
|
2016-10-09 19:18:28 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-11-01 01:57:26 -05:00
|
|
|
// NewParser constructs a parser from a list of destination structs
|
2016-01-18 12:31:01 -06:00
|
|
|
func NewParser(config Config, dests ...interface{}) (*Parser, error) {
|
2019-04-14 21:50:17 -05:00
|
|
|
// first pick a name for the command for use in the usage text
|
|
|
|
var name string
|
|
|
|
switch {
|
|
|
|
case config.Program != "":
|
|
|
|
name = config.Program
|
|
|
|
case len(os.Args) > 0:
|
|
|
|
name = filepath.Base(os.Args[0])
|
|
|
|
default:
|
|
|
|
name = "program"
|
|
|
|
}
|
|
|
|
|
|
|
|
// construct a parser
|
2016-09-08 23:18:19 -05:00
|
|
|
p := Parser{
|
2019-04-14 21:50:17 -05:00
|
|
|
cmd: &command{name: name},
|
2016-09-08 23:18:19 -05:00
|
|
|
config: config,
|
|
|
|
}
|
2019-04-14 21:50:17 -05:00
|
|
|
|
|
|
|
// make a list of roots
|
2015-10-31 20:26:58 -05:00
|
|
|
for _, dest := range dests {
|
2019-04-14 21:50:17 -05:00
|
|
|
p.roots = append(p.roots, reflect.ValueOf(dest))
|
|
|
|
}
|
|
|
|
|
|
|
|
// process each of the destination values
|
|
|
|
for i, dest := range dests {
|
|
|
|
t := reflect.TypeOf(dest)
|
|
|
|
if t.Kind() != reflect.Ptr {
|
|
|
|
panic(fmt.Sprintf("%s is not a pointer (did you forget an ampersand?)", t))
|
|
|
|
}
|
|
|
|
|
2019-04-30 15:30:23 -05:00
|
|
|
cmd, err := cmdFromStruct(name, path{root: i}, t)
|
2019-04-14 21:50:17 -05:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2019-10-20 01:23:32 -05:00
|
|
|
|
|
|
|
// add nonzero field values as defaults
|
|
|
|
for _, spec := range cmd.specs {
|
|
|
|
if v := p.val(spec.dest); v.IsValid() && !isZero(v) {
|
|
|
|
if defaultVal, ok := v.Interface().(encoding.TextMarshaler); ok {
|
|
|
|
str, err := defaultVal.MarshalText()
|
|
|
|
if err != nil {
|
2019-10-21 13:42:03 -05:00
|
|
|
return nil, fmt.Errorf("%v: error marshaling default value to string: %v", spec.dest, err)
|
2019-10-20 01:23:32 -05:00
|
|
|
}
|
|
|
|
spec.defaultVal = string(str)
|
|
|
|
} else {
|
|
|
|
spec.defaultVal = fmt.Sprintf("%v", v)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-04-14 21:50:17 -05:00
|
|
|
p.cmd.specs = append(p.cmd.specs, cmd.specs...)
|
2019-04-30 15:30:23 -05:00
|
|
|
p.cmd.subcommands = append(p.cmd.subcommands, cmd.subcommands...)
|
2019-04-14 21:50:17 -05:00
|
|
|
|
2016-09-08 23:18:19 -05:00
|
|
|
if dest, ok := dest.(Versioned); ok {
|
|
|
|
p.version = dest.Version()
|
|
|
|
}
|
2017-01-23 19:41:12 -06:00
|
|
|
if dest, ok := dest.(Described); ok {
|
|
|
|
p.description = dest.Description()
|
|
|
|
}
|
2019-04-14 21:50:17 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
return &p, nil
|
|
|
|
}
|
|
|
|
|
2019-04-30 15:30:23 -05:00
|
|
|
func cmdFromStruct(name string, dest path, t reflect.Type) (*command, error) {
|
2019-04-30 14:54:28 -05:00
|
|
|
// commands can only be created from pointers to structs
|
|
|
|
if t.Kind() != reflect.Ptr {
|
2019-04-30 15:30:23 -05:00
|
|
|
return nil, fmt.Errorf("subcommands must be pointers to structs but %s is a %s",
|
|
|
|
dest, t.Kind())
|
2019-04-30 14:54:28 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
t = t.Elem()
|
2019-04-14 21:50:17 -05:00
|
|
|
if t.Kind() != reflect.Struct {
|
2019-04-30 15:30:23 -05:00
|
|
|
return nil, fmt.Errorf("subcommands must be pointers to structs but %s is a pointer to %s",
|
|
|
|
dest, t.Kind())
|
|
|
|
}
|
|
|
|
|
|
|
|
cmd := command{
|
|
|
|
name: name,
|
|
|
|
dest: dest,
|
2019-04-14 21:50:17 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
var errs []string
|
|
|
|
walkFields(t, func(field reflect.StructField, t reflect.Type) bool {
|
|
|
|
// Check for the ignore switch in the tag
|
|
|
|
tag := field.Tag.Get("arg")
|
|
|
|
if tag == "-" {
|
|
|
|
return false
|
2015-10-31 18:15:24 -05:00
|
|
|
}
|
|
|
|
|
2019-04-14 21:50:17 -05:00
|
|
|
// If this is an embedded struct then recurse into its fields
|
|
|
|
if field.Anonymous && field.Type.Kind() == reflect.Struct {
|
|
|
|
return true
|
|
|
|
}
|
2016-10-09 19:18:28 -05:00
|
|
|
|
2019-04-30 14:54:28 -05:00
|
|
|
// duplicate the entire path to avoid slice overwrites
|
2019-04-30 15:30:23 -05:00
|
|
|
subdest := dest.Child(field.Name)
|
2019-04-14 21:50:17 -05:00
|
|
|
spec := spec{
|
2019-04-30 15:30:23 -05:00
|
|
|
dest: subdest,
|
2019-04-14 21:50:17 -05:00
|
|
|
long: strings.ToLower(field.Name),
|
|
|
|
typ: field.Type,
|
|
|
|
}
|
2015-10-31 18:15:24 -05:00
|
|
|
|
2019-04-14 21:50:17 -05:00
|
|
|
help, exists := field.Tag.Lookup("help")
|
|
|
|
if exists {
|
|
|
|
spec.help = help
|
|
|
|
}
|
2015-10-31 18:15:24 -05:00
|
|
|
|
2019-10-08 18:39:00 -05:00
|
|
|
defaultVal, hasDefault := field.Tag.Lookup("default")
|
|
|
|
if hasDefault {
|
|
|
|
spec.defaultVal = defaultVal
|
|
|
|
}
|
|
|
|
|
2019-04-14 21:50:17 -05:00
|
|
|
// Look at the tag
|
2019-04-30 15:30:23 -05:00
|
|
|
var isSubcommand bool // tracks whether this field is a subcommand
|
2019-04-14 21:50:17 -05:00
|
|
|
if tag != "" {
|
|
|
|
for _, key := range strings.Split(tag, ",") {
|
|
|
|
key = strings.TrimLeft(key, " ")
|
|
|
|
var value string
|
|
|
|
if pos := strings.Index(key, ":"); pos != -1 {
|
|
|
|
value = key[pos+1:]
|
|
|
|
key = key[:pos]
|
|
|
|
}
|
2015-11-11 03:15:57 -06:00
|
|
|
|
2019-04-14 21:50:17 -05:00
|
|
|
switch {
|
|
|
|
case strings.HasPrefix(key, "---"):
|
|
|
|
errs = append(errs, fmt.Sprintf("%s.%s: too many hyphens", t.Name(), field.Name))
|
|
|
|
case strings.HasPrefix(key, "--"):
|
|
|
|
spec.long = key[2:]
|
|
|
|
case strings.HasPrefix(key, "-"):
|
|
|
|
if len(key) != 2 {
|
|
|
|
errs = append(errs, fmt.Sprintf("%s.%s: short arguments must be one character only",
|
|
|
|
t.Name(), field.Name))
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
spec.short = key[1:]
|
|
|
|
case key == "required":
|
2019-10-08 18:39:00 -05:00
|
|
|
if hasDefault {
|
|
|
|
errs = append(errs, fmt.Sprintf("%s.%s: 'required' cannot be used when a default value is specified",
|
|
|
|
t.Name(), field.Name))
|
|
|
|
return false
|
|
|
|
}
|
2019-04-14 21:50:17 -05:00
|
|
|
spec.required = true
|
|
|
|
case key == "positional":
|
|
|
|
spec.positional = true
|
|
|
|
case key == "separate":
|
|
|
|
spec.separate = true
|
|
|
|
case key == "help": // deprecated
|
|
|
|
spec.help = value
|
|
|
|
case key == "env":
|
|
|
|
// Use override name if provided
|
|
|
|
if value != "" {
|
|
|
|
spec.env = value
|
|
|
|
} else {
|
|
|
|
spec.env = strings.ToUpper(field.Name)
|
|
|
|
}
|
|
|
|
case key == "subcommand":
|
|
|
|
// decide on a name for the subcommand
|
|
|
|
cmdname := value
|
|
|
|
if cmdname == "" {
|
|
|
|
cmdname = strings.ToLower(field.Name)
|
2015-10-31 20:26:58 -05:00
|
|
|
}
|
2015-10-31 18:15:24 -05:00
|
|
|
|
2019-04-30 15:40:45 -05:00
|
|
|
// parse the subcommand recursively
|
2019-04-30 15:30:23 -05:00
|
|
|
subcmd, err := cmdFromStruct(cmdname, subdest, field.Type)
|
2019-04-14 21:50:17 -05:00
|
|
|
if err != nil {
|
|
|
|
errs = append(errs, err.Error())
|
2016-10-09 19:18:28 -05:00
|
|
|
return false
|
2015-10-31 18:15:24 -05:00
|
|
|
}
|
2019-04-14 21:50:17 -05:00
|
|
|
|
2019-05-03 17:49:44 -05:00
|
|
|
subcmd.parent = &cmd
|
2019-05-03 17:02:10 -05:00
|
|
|
subcmd.help = field.Tag.Get("help")
|
|
|
|
|
2019-04-14 21:50:17 -05:00
|
|
|
cmd.subcommands = append(cmd.subcommands, subcmd)
|
2019-04-30 15:30:23 -05:00
|
|
|
isSubcommand = true
|
2019-04-14 21:50:17 -05:00
|
|
|
default:
|
|
|
|
errs = append(errs, fmt.Sprintf("unrecognized tag '%s' on field %s", key, tag))
|
|
|
|
return false
|
2015-10-31 18:15:24 -05:00
|
|
|
}
|
|
|
|
}
|
2019-04-14 21:50:17 -05:00
|
|
|
}
|
2019-04-30 15:30:23 -05:00
|
|
|
|
2019-11-29 15:22:21 -06:00
|
|
|
placeholder, hasPlaceholder := field.Tag.Lookup("placeholder")
|
|
|
|
if hasPlaceholder {
|
|
|
|
spec.placeholder = placeholder
|
|
|
|
} else {
|
|
|
|
spec.placeholder = strings.ToUpper(spec.long)
|
2019-11-29 13:33:16 -06:00
|
|
|
}
|
|
|
|
|
2019-04-30 15:30:23 -05:00
|
|
|
// Check whether this field is supported. It's good to do this here rather than
|
|
|
|
// wait until ParseValue because it means that a program with invalid argument
|
|
|
|
// fields will always fail regardless of whether the arguments it received
|
|
|
|
// exercised those fields.
|
|
|
|
if !isSubcommand {
|
|
|
|
cmd.specs = append(cmd.specs, &spec)
|
|
|
|
|
|
|
|
var parseable bool
|
|
|
|
parseable, spec.boolean, spec.multiple = canParse(field.Type)
|
|
|
|
if !parseable {
|
|
|
|
errs = append(errs, fmt.Sprintf("%s.%s: %s fields are not supported",
|
|
|
|
t.Name(), field.Name, field.Type.String()))
|
|
|
|
return false
|
|
|
|
}
|
2019-10-08 18:39:00 -05:00
|
|
|
if spec.multiple && hasDefault {
|
|
|
|
errs = append(errs, fmt.Sprintf("%s.%s: default values are not supported for slice fields",
|
|
|
|
t.Name(), field.Name))
|
|
|
|
return false
|
|
|
|
}
|
2019-04-30 15:30:23 -05:00
|
|
|
}
|
2016-10-09 19:18:28 -05:00
|
|
|
|
2019-04-14 21:50:17 -05:00
|
|
|
// if this was an embedded field then we already returned true up above
|
|
|
|
return false
|
|
|
|
})
|
2016-10-09 19:18:28 -05:00
|
|
|
|
2019-04-14 21:50:17 -05:00
|
|
|
if len(errs) > 0 {
|
|
|
|
return nil, errors.New(strings.Join(errs, "\n"))
|
2016-01-18 12:31:01 -06:00
|
|
|
}
|
2019-04-14 21:50:17 -05:00
|
|
|
|
2019-04-30 14:54:28 -05:00
|
|
|
// check that we don't have both positionals and subcommands
|
|
|
|
var hasPositional bool
|
|
|
|
for _, spec := range cmd.specs {
|
|
|
|
if spec.positional {
|
|
|
|
hasPositional = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if hasPositional && len(cmd.subcommands) > 0 {
|
2019-05-02 11:50:44 -05:00
|
|
|
return nil, fmt.Errorf("%s cannot have both subcommands and positional arguments", dest)
|
2019-04-30 14:54:28 -05:00
|
|
|
}
|
|
|
|
|
2019-04-14 21:50:17 -05:00
|
|
|
return &cmd, nil
|
2015-11-01 01:13:23 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
// Parse processes the given command line option, storing the results in the field
|
|
|
|
// of the structs from which NewParser was constructed
|
|
|
|
func (p *Parser) Parse(args []string) error {
|
2019-05-03 17:49:44 -05:00
|
|
|
err := p.process(args)
|
|
|
|
if err != nil {
|
|
|
|
// If -h or --help were specified then make sure help text supercedes other errors
|
|
|
|
for _, arg := range args {
|
|
|
|
if arg == "-h" || arg == "--help" {
|
|
|
|
return ErrHelp
|
|
|
|
}
|
|
|
|
if arg == "--" {
|
|
|
|
break
|
|
|
|
}
|
2015-11-01 01:13:23 -06:00
|
|
|
}
|
|
|
|
}
|
2019-05-03 17:49:44 -05:00
|
|
|
return err
|
2015-10-31 18:15:24 -05:00
|
|
|
}
|
|
|
|
|
2019-04-30 14:54:28 -05:00
|
|
|
// process environment vars for the given arguments
|
|
|
|
func (p *Parser) captureEnvVars(specs []*spec, wasPresent map[*spec]bool) error {
|
2019-04-14 20:24:59 -05:00
|
|
|
for _, spec := range specs {
|
2019-04-14 20:00:40 -05:00
|
|
|
if spec.env == "" {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
value, found := os.LookupEnv(spec.env)
|
|
|
|
if !found {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
if spec.multiple {
|
|
|
|
// expect a CSV string in an environment
|
|
|
|
// variable in the case of multiple values
|
|
|
|
values, err := csv.NewReader(strings.NewReader(value)).Read()
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf(
|
|
|
|
"error reading a CSV string from environment variable %s with multiple values: %v",
|
|
|
|
spec.env,
|
|
|
|
err,
|
|
|
|
)
|
|
|
|
}
|
2019-05-03 18:32:16 -05:00
|
|
|
if err = setSlice(p.val(spec.dest), values, !spec.separate); err != nil {
|
2019-04-14 20:00:40 -05:00
|
|
|
return fmt.Errorf(
|
|
|
|
"error processing environment variable %s with multiple values: %v",
|
|
|
|
spec.env,
|
|
|
|
err,
|
|
|
|
)
|
|
|
|
}
|
|
|
|
} else {
|
2019-05-03 18:32:16 -05:00
|
|
|
if err := scalar.ParseValue(p.val(spec.dest), value); err != nil {
|
2019-04-14 20:00:40 -05:00
|
|
|
return fmt.Errorf("error processing environment variable %s: %v", spec.env, err)
|
2016-01-18 12:42:04 -06:00
|
|
|
}
|
|
|
|
}
|
2019-04-14 20:00:40 -05:00
|
|
|
wasPresent[spec] = true
|
2015-10-31 18:15:24 -05:00
|
|
|
}
|
|
|
|
|
2019-04-30 14:54:28 -05:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// process goes through arguments one-by-one, parses them, and assigns the result to
|
|
|
|
// the underlying struct field
|
|
|
|
func (p *Parser) process(args []string) error {
|
|
|
|
// track the options we have seen
|
|
|
|
wasPresent := make(map[*spec]bool)
|
|
|
|
|
|
|
|
// union of specs for the chain of subcommands encountered so far
|
|
|
|
curCmd := p.cmd
|
2019-05-03 17:49:44 -05:00
|
|
|
p.lastCmd = curCmd
|
2019-04-30 14:54:28 -05:00
|
|
|
|
|
|
|
// make a copy of the specs because we will add to this list each time we expand a subcommand
|
|
|
|
specs := make([]*spec, len(curCmd.specs))
|
|
|
|
copy(specs, curCmd.specs)
|
|
|
|
|
|
|
|
// deal with environment vars
|
|
|
|
err := p.captureEnvVars(specs, wasPresent)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2015-10-31 18:15:24 -05:00
|
|
|
// process each string from the command line
|
|
|
|
var allpositional bool
|
|
|
|
var positionals []string
|
|
|
|
|
|
|
|
// must use explicit for loop, not range, because we manipulate i inside the loop
|
|
|
|
for i := 0; i < len(args); i++ {
|
|
|
|
arg := args[i]
|
|
|
|
if arg == "--" {
|
|
|
|
allpositional = true
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2017-02-21 11:08:08 -06:00
|
|
|
if !isFlag(arg) || allpositional {
|
2019-04-30 14:54:28 -05:00
|
|
|
// each subcommand can have either subcommands or positionals, but not both
|
|
|
|
if len(curCmd.subcommands) == 0 {
|
|
|
|
positionals = append(positionals, arg)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
// if we have a subcommand then make sure it is valid for the current context
|
|
|
|
subcmd := findSubcommand(curCmd.subcommands, arg)
|
|
|
|
if subcmd == nil {
|
|
|
|
return fmt.Errorf("invalid subcommand: %s", arg)
|
|
|
|
}
|
|
|
|
|
2019-04-30 15:40:45 -05:00
|
|
|
// instantiate the field to point to a new struct
|
2019-05-03 18:32:16 -05:00
|
|
|
v := p.val(subcmd.dest)
|
2019-04-30 15:40:45 -05:00
|
|
|
v.Set(reflect.New(v.Type().Elem())) // we already checked that all subcommands are struct pointers
|
|
|
|
|
2019-04-30 14:54:28 -05:00
|
|
|
// add the new options to the set of allowed options
|
|
|
|
specs = append(specs, subcmd.specs...)
|
|
|
|
|
|
|
|
// capture environment vars for these new options
|
|
|
|
err := p.captureEnvVars(subcmd.specs, wasPresent)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
curCmd = subcmd
|
2019-05-03 17:49:44 -05:00
|
|
|
p.lastCmd = curCmd
|
2015-10-31 18:15:24 -05:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2019-05-03 17:49:44 -05:00
|
|
|
// check for special --help and --version flags
|
|
|
|
switch arg {
|
|
|
|
case "-h", "--help":
|
|
|
|
return ErrHelp
|
|
|
|
case "--version":
|
|
|
|
return ErrVersion
|
|
|
|
}
|
|
|
|
|
2015-10-31 18:15:24 -05:00
|
|
|
// check for an equals sign, as in "--foo=bar"
|
|
|
|
var value string
|
|
|
|
opt := strings.TrimLeft(arg, "-")
|
|
|
|
if pos := strings.Index(opt, "="); pos != -1 {
|
|
|
|
value = opt[pos+1:]
|
|
|
|
opt = opt[:pos]
|
|
|
|
}
|
|
|
|
|
2019-04-30 14:54:28 -05:00
|
|
|
// lookup the spec for this option (note that the "specs" slice changes as
|
|
|
|
// we expand subcommands so it is better not to use a map)
|
|
|
|
spec := findOption(specs, opt)
|
|
|
|
if spec == nil {
|
2015-10-31 18:15:24 -05:00
|
|
|
return fmt.Errorf("unknown argument %s", arg)
|
|
|
|
}
|
2019-04-14 20:00:40 -05:00
|
|
|
wasPresent[spec] = true
|
2015-10-31 18:15:24 -05:00
|
|
|
|
|
|
|
// deal with the case of multiple values
|
|
|
|
if spec.multiple {
|
|
|
|
var values []string
|
|
|
|
if value == "" {
|
2019-10-04 15:18:17 -05:00
|
|
|
for i+1 < len(args) && !isFlag(args[i+1]) && args[i+1] != "--" {
|
2015-10-31 19:05:14 -05:00
|
|
|
values = append(values, args[i+1])
|
|
|
|
i++
|
2017-03-03 06:12:17 -06:00
|
|
|
if spec.separate {
|
|
|
|
break
|
|
|
|
}
|
2015-10-31 18:15:24 -05:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
values = append(values, value)
|
|
|
|
}
|
2019-05-03 18:32:16 -05:00
|
|
|
err := setSlice(p.val(spec.dest), values, !spec.separate)
|
2015-10-31 19:05:14 -05:00
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("error processing %s: %v", arg, err)
|
|
|
|
}
|
2015-10-31 18:15:24 -05:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
// if it's a flag and it has no value then set the value to true
|
2016-01-23 22:49:57 -06:00
|
|
|
// use boolean because this takes account of TextUnmarshaler
|
|
|
|
if spec.boolean && value == "" {
|
2015-10-31 18:15:24 -05:00
|
|
|
value = "true"
|
|
|
|
}
|
|
|
|
|
|
|
|
// if we have something like "--foo" then the value is the next argument
|
|
|
|
if value == "" {
|
2018-01-13 16:20:00 -06:00
|
|
|
if i+1 == len(args) {
|
|
|
|
return fmt.Errorf("missing value for %s", arg)
|
|
|
|
}
|
2019-04-14 21:50:17 -05:00
|
|
|
if !nextIsNumeric(spec.typ, args[i+1]) && isFlag(args[i+1]) {
|
2015-10-31 18:15:24 -05:00
|
|
|
return fmt.Errorf("missing value for %s", arg)
|
|
|
|
}
|
|
|
|
value = args[i+1]
|
|
|
|
i++
|
|
|
|
}
|
|
|
|
|
2019-05-03 18:32:16 -05:00
|
|
|
err := scalar.ParseValue(p.val(spec.dest), value)
|
2015-10-31 18:15:24 -05:00
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("error processing %s: %v", arg, err)
|
|
|
|
}
|
|
|
|
}
|
2015-10-31 19:05:14 -05:00
|
|
|
|
|
|
|
// process positionals
|
2019-04-14 20:24:59 -05:00
|
|
|
for _, spec := range specs {
|
2019-04-14 19:30:53 -05:00
|
|
|
if !spec.positional {
|
|
|
|
continue
|
|
|
|
}
|
2019-04-14 20:00:40 -05:00
|
|
|
if len(positionals) == 0 {
|
|
|
|
break
|
2019-04-14 19:30:53 -05:00
|
|
|
}
|
2019-04-14 20:00:40 -05:00
|
|
|
wasPresent[spec] = true
|
2019-04-14 19:30:53 -05:00
|
|
|
if spec.multiple {
|
2019-05-03 18:32:16 -05:00
|
|
|
err := setSlice(p.val(spec.dest), positionals, true)
|
2019-04-14 19:30:53 -05:00
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("error processing %s: %v", spec.long, err)
|
|
|
|
}
|
|
|
|
positionals = nil
|
2019-04-14 20:00:40 -05:00
|
|
|
} else {
|
2019-05-03 18:32:16 -05:00
|
|
|
err := scalar.ParseValue(p.val(spec.dest), positionals[0])
|
2019-04-14 19:30:53 -05:00
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("error processing %s: %v", spec.long, err)
|
2015-10-31 19:05:14 -05:00
|
|
|
}
|
2019-04-14 19:30:53 -05:00
|
|
|
positionals = positionals[1:]
|
2015-10-31 19:05:14 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
if len(positionals) > 0 {
|
|
|
|
return fmt.Errorf("too many positional arguments at '%s'", positionals[0])
|
|
|
|
}
|
2019-04-14 20:00:40 -05:00
|
|
|
|
2019-10-08 18:39:00 -05:00
|
|
|
// fill in defaults and check that all the required args were provided
|
2019-04-14 20:24:59 -05:00
|
|
|
for _, spec := range specs {
|
2019-10-08 18:39:00 -05:00
|
|
|
if wasPresent[spec] {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
name := spec.long
|
|
|
|
if !spec.positional {
|
|
|
|
name = "--" + spec.long
|
|
|
|
}
|
|
|
|
|
|
|
|
if spec.required {
|
2019-04-14 20:00:40 -05:00
|
|
|
return fmt.Errorf("%s is required", name)
|
|
|
|
}
|
2019-10-08 18:39:00 -05:00
|
|
|
if spec.defaultVal != "" {
|
|
|
|
err := scalar.ParseValue(p.val(spec.dest), spec.defaultVal)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("error processing default value for %s: %v", name, err)
|
|
|
|
}
|
|
|
|
}
|
2019-04-14 20:00:40 -05:00
|
|
|
}
|
|
|
|
|
2015-10-31 18:15:24 -05:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2018-01-13 16:20:00 -06:00
|
|
|
func nextIsNumeric(t reflect.Type, s string) bool {
|
|
|
|
switch t.Kind() {
|
|
|
|
case reflect.Ptr:
|
|
|
|
return nextIsNumeric(t.Elem(), s)
|
|
|
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Float32, reflect.Float64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
|
|
|
v := reflect.New(t)
|
|
|
|
err := scalar.ParseValue(v, s)
|
|
|
|
return err == nil
|
|
|
|
default:
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-02-21 11:08:08 -06:00
|
|
|
// isFlag returns true if a token is a flag such as "-v" or "--user" but not "-" or "--"
|
|
|
|
func isFlag(s string) bool {
|
|
|
|
return strings.HasPrefix(s, "-") && strings.TrimLeft(s, "-") != ""
|
|
|
|
}
|
|
|
|
|
2019-05-03 18:32:16 -05:00
|
|
|
// val returns a reflect.Value corresponding to the current value for the
|
|
|
|
// given path
|
|
|
|
func (p *Parser) val(dest path) reflect.Value {
|
2019-04-30 15:30:23 -05:00
|
|
|
v := p.roots[dest.root]
|
|
|
|
for _, field := range dest.fields {
|
2019-04-14 21:50:17 -05:00
|
|
|
if v.Kind() == reflect.Ptr {
|
|
|
|
if v.IsNil() {
|
|
|
|
return reflect.Value{}
|
|
|
|
}
|
|
|
|
v = v.Elem()
|
|
|
|
}
|
|
|
|
|
|
|
|
v = v.FieldByName(field)
|
|
|
|
if !v.IsValid() {
|
2019-04-30 13:40:11 -05:00
|
|
|
// it is appropriate to panic here because this can only happen due to
|
2019-04-30 15:30:23 -05:00
|
|
|
// an internal bug in this library (since we construct the path ourselves
|
2019-04-30 13:40:11 -05:00
|
|
|
// by reflecting on the same struct)
|
2019-04-14 21:50:17 -05:00
|
|
|
panic(fmt.Errorf("error resolving path %v: %v has no field named %v",
|
2019-04-30 15:30:23 -05:00
|
|
|
dest.fields, v.Type(), field))
|
2019-04-14 21:50:17 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return v
|
|
|
|
}
|
|
|
|
|
2016-07-31 11:14:44 -05:00
|
|
|
// parse a value as the appropriate type and store it in the struct
|
2017-03-03 06:12:17 -06:00
|
|
|
func setSlice(dest reflect.Value, values []string, trunc bool) error {
|
2015-10-31 19:05:14 -05:00
|
|
|
if !dest.CanSet() {
|
2019-08-06 18:38:11 -05:00
|
|
|
return fmt.Errorf("field is not writable")
|
2015-10-31 19:05:14 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
var ptr bool
|
|
|
|
elem := dest.Type().Elem()
|
2018-04-18 23:51:16 -05:00
|
|
|
if elem.Kind() == reflect.Ptr && !elem.Implements(textUnmarshalerType) {
|
2015-10-31 19:05:14 -05:00
|
|
|
ptr = true
|
|
|
|
elem = elem.Elem()
|
|
|
|
}
|
|
|
|
|
2016-02-29 15:05:26 -06:00
|
|
|
// Truncate the dest slice in case default values exist
|
2017-03-03 06:12:17 -06:00
|
|
|
if trunc && !dest.IsNil() {
|
2016-02-29 15:05:26 -06:00
|
|
|
dest.SetLen(0)
|
|
|
|
}
|
|
|
|
|
2015-10-31 19:05:14 -05:00
|
|
|
for _, s := range values {
|
|
|
|
v := reflect.New(elem)
|
2018-04-18 23:23:08 -05:00
|
|
|
if err := scalar.ParseValue(v.Elem(), s); err != nil {
|
2015-10-31 19:05:14 -05:00
|
|
|
return err
|
|
|
|
}
|
2015-11-04 12:27:17 -06:00
|
|
|
if !ptr {
|
|
|
|
v = v.Elem()
|
2015-10-31 19:05:14 -05:00
|
|
|
}
|
2015-11-04 12:27:17 -06:00
|
|
|
dest.Set(reflect.Append(dest, v))
|
2015-10-31 19:05:14 -05:00
|
|
|
}
|
2015-10-31 18:15:24 -05:00
|
|
|
return nil
|
|
|
|
}
|
2016-01-23 22:49:57 -06:00
|
|
|
|
2019-04-30 14:54:28 -05:00
|
|
|
// findOption finds an option from its name, or returns null if no spec is found
|
|
|
|
func findOption(specs []*spec, name string) *spec {
|
|
|
|
for _, spec := range specs {
|
|
|
|
if spec.positional {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if spec.long == name || spec.short == name {
|
|
|
|
return spec
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// findSubcommand finds a subcommand using its name, or returns null if no subcommand is found
|
|
|
|
func findSubcommand(cmds []*command, name string) *command {
|
|
|
|
for _, cmd := range cmds {
|
|
|
|
if cmd.name == name {
|
|
|
|
return cmd
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
2019-10-20 01:23:32 -05:00
|
|
|
|
|
|
|
// isZero returns true if v contains the zero value for its type
|
|
|
|
func isZero(v reflect.Value) bool {
|
|
|
|
t := v.Type()
|
|
|
|
if t.Kind() == reflect.Slice {
|
|
|
|
return v.IsNil()
|
|
|
|
}
|
|
|
|
if !t.Comparable() {
|
|
|
|
return false
|
|
|
|
}
|
2019-10-20 01:30:33 -05:00
|
|
|
return v.Interface() == reflect.Zero(t).Interface()
|
2019-10-20 01:23:32 -05:00
|
|
|
}
|