go-arg/parse.go

837 lines
23 KiB
Go
Raw Normal View History

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"
"io"
2015-10-31 18:15:24 -05:00
"os"
"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-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 []reflect.StructField // sequence of struct fields to traverse
2019-04-30 15:30:23 -05:00
}
// String gets a string representation of the given path
func (p path) String() string {
s := "args"
for _, f := range p.fields {
s += "." + f.Name
2019-05-02 11:50:44 -05:00
}
return s
2019-04-30 15:30:23 -05:00
}
// Child gets a new path representing a child of this path.
func (p path) Child(f reflect.StructField) path {
2019-04-30 15:30:23 -05:00
// copy the entire slice of fields to avoid possible slice overwrite
subfields := make([]reflect.StructField, len(p.fields)+1)
copy(subfields, p.fields)
subfields[len(subfields)-1] = f
2019-04-30 15:30:23 -05:00
return path{
root: p.root,
fields: subfields,
}
}
2015-10-31 20:26:58 -05:00
// spec represents a command line option
type spec struct {
dest path
field reflect.StructField // the struct field from which this option was created
long string // the --long form for this option, or empty if none
short string // the -s short form for this option, or empty if none
cardinality cardinality // determines how many tokens will be present (possible values: zero, one, multiple)
required bool // if true, this option must be present on the command line
positional bool // if true, this option will be looked for in the positional flags
separate bool // if true, each slice and map entry will have its own --flag
help string // the help text for this option
env string // the name of the environment variable for this option, or empty for none
defaultValue reflect.Value // default value for this option
defaultString string // default value for this option, in string form to be displayed in help text
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
parent *command
2019-04-14 21:50:17 -05: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")
// for monkey patching in example code
var mustParseExit = os.Exit
// MustParse processes command line arguments and exits upon failure
2016-01-05 15:52:33 -06:00
func MustParse(dest ...interface{}) *Parser {
return mustParse(Config{Exit: mustParseExit}, dest...)
}
// mustParse is a helper that facilitates testing
func mustParse(config Config, dest ...interface{}) *Parser {
if config.Exit == nil {
config.Exit = os.Exit
}
if config.Out == nil {
config.Out = os.Stdout
}
p, err := NewParser(config, dest...)
2015-11-01 01:57:26 -05:00
if err != nil {
fmt.Fprintln(config.Out, err)
config.Exit(-1)
return nil
2015-11-01 01:57:26 -05:00
}
2019-05-03 15:07:12 -05:00
p.MustParse(flags())
2016-01-05 15:52:33 -06:00
return p
2015-10-31 18:15:24 -05:00
}
// Parse processes command line arguments and stores them in dest
2015-10-31 20:26:58 -05:00
func Parse(dest ...interface{}) error {
p, err := NewParser(Config{}, dest...)
2015-11-01 01:57:26 -05:00
if err != nil {
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
}
// Config represents configuration options for an argument parser
type Config struct {
// Program is the name of the program used in the help text
Program string
// IgnoreEnv instructs the library not to read environment variables
IgnoreEnv bool
2022-01-02 08:06:37 -06:00
// IgnoreDefault instructs the library not to reset the variables to the
// default values, including pointers to sub commands
IgnoreDefault bool
2023-01-18 02:50:50 -06:00
2023-01-18 02:52:13 -06:00
// StrictSubcommands intructs the library not to allow global commands after
2023-01-18 02:50:50 -06:00
// subcommand
StrictSubcommands bool
// Exit is called to terminate the process with an error code (defaults to os.Exit)
Exit func(int)
// Out is where help text, usage text, and failure messages are printed (defaults to os.Stdout)
Out io.Writer
}
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
epilogue string
2020-01-19 12:38:19 -06:00
// the following field changes during 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
}
// Epilogued is the interface that the destination struct should implement to
// add an epilogue string at the bottom of the help message.
type Epilogued interface {
// Epilogue returns the string that will be printed on a line by itself
// at the end of the help message.
Epilogue() 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) {
walkFieldsImpl(t, visit, nil)
}
func walkFieldsImpl(t reflect.Type, visit func(field reflect.StructField, owner reflect.Type) bool, path []int) {
2016-10-09 19:18:28 -05:00
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
field.Index = make([]int, len(path)+1)
copy(field.Index, append(path, 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 {
var subpath []int
if field.Anonymous {
subpath = append(path, i)
}
walkFieldsImpl(field.Type, visit, subpath)
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
func NewParser(config Config, dests ...interface{}) (*Parser, error) {
// fill in defaults
if config.Exit == nil {
config.Exit = os.Exit
}
if config.Out == nil {
config.Out = os.Stdout
}
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
2022-06-09 10:21:29 -05:00
// for backwards compatibility, add nonzero field values as defaults
// this applies only to the top-level command, not to subcommands (this inconsistency
// is the reason that this method for setting default values was deprecated)
2019-10-20 01:23:32 -05:00
for _, spec := range cmd.specs {
// get the value
v := p.val(spec.dest)
2022-06-09 10:21:29 -05:00
// if the value is the "zero value" (e.g. nil pointer, empty struct) then ignore
if isZero(v) {
2022-06-09 10:21:29 -05:00
continue
}
// store as a default
spec.defaultValue = v
2022-06-09 10:21:29 -05:00
// we need a string to display in help text
2022-06-09 10:21:29 -05:00
// if MarshalText is implemented then use that
if m, ok := v.Interface().(encoding.TextMarshaler); ok {
s, err := m.MarshalText()
if err != nil {
return nil, fmt.Errorf("%v: error marshaling default value to string: %v", spec.dest, err)
2019-10-20 01:23:32 -05:00
}
spec.defaultString = string(s)
} else {
spec.defaultString = fmt.Sprintf("%v", v)
2019-10-20 01:23:32 -05:00
}
}
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()
}
if dest, ok := dest.(Epilogued); ok {
p.epilogue = dest.Epilogue()
}
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
2019-04-14 21:50:17 -05:00
tag := field.Tag.Get("arg")
if tag == "-" {
2019-04-14 21:50:17 -05:00
return false
2015-10-31 18:15:24 -05:00
}
// if this is an embedded struct then recurse into its fields, even if
// it is unexported, because exported fields on unexported embedded
// structs are still writable
2019-04-14 21:50:17 -05:00
if field.Anonymous && field.Type.Kind() == reflect.Struct {
return true
}
2016-10-09 19:18:28 -05:00
// ignore any other unexported field
if !isExported(field.Name) {
return false
}
2019-04-30 14:54:28 -05:00
// duplicate the entire path to avoid slice overwrites
subdest := dest.Child(field)
2019-04-14 21:50:17 -05:00
spec := spec{
dest: subdest,
field: field,
long: strings.ToLower(field.Name),
2019-04-14 21:50:17 -05:00
}
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-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
2023-06-03 02:50:42 -05:00
for _, key := range strings.Split(tag, ",") {
2020-07-06 11:54:23 -05:00
if key == "" {
continue
}
key = strings.TrimLeft(key, " ")
var value string
if pos := strings.Index(key, ":"); pos != -1 {
value = key[pos+1:]
key = key[:pos]
}
2019-04-14 21:50:17 -05:00
2020-07-06 11:54:23 -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, "-"):
2023-06-03 02:50:42 -05:00
if len(key) > 2 {
2022-05-21 10:44:32 -05:00
errs = append(errs, fmt.Sprintf("%s.%s: short arguments must be one character only",
t.Name(), field.Name))
return false
}
2020-07-06 11:54:23 -05:00
spec.short = key[1:]
case key == "required":
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)
}
2019-05-03 17:02:10 -05:00
2020-07-06 11:54:23 -05:00
// parse the subcommand recursively
subcmd, err := cmdFromStruct(cmdname, subdest, field.Type)
if err != nil {
errs = append(errs, err.Error())
2019-04-14 21:50:17 -05:00
return false
2015-10-31 18:15:24 -05:00
}
2020-07-06 11:54:23 -05:00
subcmd.parent = &cmd
subcmd.help = field.Tag.Get("help")
cmd.subcommands = append(cmd.subcommands, subcmd)
isSubcommand = true
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 if spec.long != "" {
spec.placeholder = strings.ToUpper(spec.long)
2019-11-29 15:22:21 -06:00
} else {
spec.placeholder = strings.ToUpper(spec.field.Name)
2019-11-29 13:33:16 -06:00
}
// if this is a subcommand then we've done everything we need to do
if isSubcommand {
return false
}
// check whether this field is supported. It's good to do this here rather than
2019-04-30 15:30:23 -05:00
// 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.
var err error
spec.cardinality, err = cardinalityOf(field.Type)
if err != nil {
errs = append(errs, fmt.Sprintf("%s.%s: %s fields are not supported",
t.Name(), field.Name, field.Type.String()))
return false
}
2019-04-30 15:30:23 -05:00
defaultString, hasDefault := field.Tag.Lookup("default")
if hasDefault {
// we do not support default values for maps and slices
if spec.cardinality == multiple {
errs = append(errs, fmt.Sprintf("%s.%s: default values are not supported for slice or map fields",
t.Name(), field.Name))
2019-04-30 15:30:23 -05:00
return false
}
// a required field cannot also have a default value
if spec.required {
errs = append(errs, fmt.Sprintf("%s.%s: 'required' cannot be used when a default value is specified",
t.Name(), field.Name))
return false
}
// parse the default value
spec.defaultString = defaultString
if field.Type.Kind() == reflect.Ptr {
// here we have a field of type *T and we create a new T, no need to dereference
// in order for the value to be settable
spec.defaultValue = reflect.New(field.Type.Elem())
} else {
// here we have a field of type T and we create a new T and then dereference it
// so that the resulting value is settable
spec.defaultValue = reflect.New(field.Type).Elem()
}
err := scalar.ParseValue(spec.defaultValue, defaultString)
if err != nil {
errs = append(errs, fmt.Sprintf("%s.%s: error processing default value: %v", t.Name(), field.Name, err))
return false
}
2019-04-30 15:30:23 -05:00
}
2016-10-09 19:18:28 -05:00
// add the spec to the list of specs
cmd.specs = append(cmd.specs, &spec)
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"))
}
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
}
// 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 {
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
}
}
}
return err
2015-10-31 18:15:24 -05:00
}
func (p *Parser) MustParse(args []string) {
err := p.Parse(args)
switch {
case err == ErrHelp:
p.writeHelpForSubcommand(p.config.Out, p.lastCmd)
p.config.Exit(0)
case err == ErrVersion:
fmt.Fprintln(p.config.Out, p.version)
p.config.Exit(0)
case err != nil:
p.failWithSubcommand(err.Error(), p.lastCmd)
}
}
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.cardinality == multiple {
2019-04-14 20:00:40 -05:00
// expect a CSV string in an environment
// variable in the case of multiple values
var values []string
var err error
if len(strings.TrimSpace(value)) > 0 {
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-04-14 20:00:40 -05:00
}
if err = setSliceOrMap(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 {
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
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
if !p.config.IgnoreEnv {
err := p.captureEnvVars(specs, wasPresent)
if err != nil {
return err
}
2019-04-30 14:54:28 -05:00
}
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)
}
// instantiate the field to point to a new struct
v := p.val(subcmd.dest)
if v.IsNil() {
2022-01-02 08:06:37 -06: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
2023-01-18 02:50:50 -06:00
if p.config.StrictSubcommands {
specs = make([]*spec, len(subcmd.specs))
copy(specs, subcmd.specs)
} else {
specs = append(specs, subcmd.specs...)
}
2019-04-30 14:54:28 -05:00
// capture environment vars for these new options
if !p.config.IgnoreEnv {
err := p.captureEnvVars(subcmd.specs, wasPresent)
if err != nil {
return err
}
2019-04-30 14:54:28 -05:00
}
curCmd = subcmd
p.lastCmd = curCmd
2015-10-31 18:15:24 -05:00
continue
}
// 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)
2022-05-21 10:24:45 -05:00
if spec == nil || opt == "" {
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.cardinality == multiple {
2015-10-31 18:15:24 -05:00
var values []string
if value == "" {
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++
if spec.separate {
break
}
2015-10-31 18:15:24 -05:00
}
} else {
values = append(values, value)
}
err := setSliceOrMap(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
// use boolean because this takes account of TextUnmarshaler
if spec.cardinality == zero && 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)
}
if !nextIsNumeric(spec.field.Type, 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++
}
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 {
if !spec.positional {
continue
}
2019-04-14 20:00:40 -05:00
if len(positionals) == 0 {
break
}
2019-04-14 20:00:40 -05:00
wasPresent[spec] = true
if spec.cardinality == multiple {
err := setSliceOrMap(p.val(spec.dest), positionals, true)
if err != nil {
return fmt.Errorf("error processing %s: %v", spec.field.Name, err)
}
positionals = nil
2019-04-14 20:00:40 -05:00
} else {
err := scalar.ParseValue(p.val(spec.dest), positionals[0])
if err != nil {
return fmt.Errorf("error processing %s: %v", spec.field.Name, err)
2015-10-31 19:05:14 -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
// fill in defaults and check that all the required args were provided
2019-04-14 20:24:59 -05:00
for _, spec := range specs {
if wasPresent[spec] {
continue
}
name := strings.ToLower(spec.field.Name)
2020-12-19 17:54:03 -06:00
if spec.long != "" && !spec.positional {
name = "--" + spec.long
}
if spec.required {
if spec.short == "" && spec.long == "" {
msg := fmt.Sprintf("environment variable %s is required", spec.env)
return errors.New(msg)
}
msg := fmt.Sprintf("%s is required", name)
if spec.env != "" {
msg += " (or environment variable " + spec.env + ")"
}
return errors.New(msg)
2019-04-14 20:00:40 -05:00
}
if spec.defaultValue.IsValid() && !p.config.IgnoreDefault {
// One issue here is that if the user now modifies the value then
// the default value stored in the spec will be corrupted. There
// is no general way to "deep-copy" values in Go, and we still
// support the old-style method for specifying defaults as
// Go values assigned directly to the struct field, so we are stuck.
p.val(spec.dest).Set(spec.defaultValue)
}
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, "-") != ""
}
// 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()
}
2021-04-19 23:03:43 -05:00
v = v.FieldByIndex(field.Index)
2019-04-14 21:50:17 -05:00
}
return v
}
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
}