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"
2023-02-08 06:01:48 -06:00
"io"
2015-10-31 18:15:24 -05:00
"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
2023-08-06 18:14:16 -05:00
"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 {
2020-01-24 16:30:29 -06:00
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 {
2020-01-24 16:30:29 -06:00
s := "args"
for _ , f := range p . fields {
s += "." + f . Name
2019-05-02 11:50:44 -05:00
}
2020-01-24 16:30:29 -06:00
return s
2019-04-30 15:30:23 -05:00
}
// Child gets a new path representing a child of this path.
2020-01-24 16:30:29 -06:00
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
2020-01-24 16:30:29 -06:00
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 {
2022-10-29 13:47:13 -05:00
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
2023-06-29 14:26:34 -05:00
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
2019-05-03 17:49:44 -05:00
parent * command
2019-04-14 21:50:17 -05:00
}
2023-07-14 14:12:47 -05:00
// ErrHelp indicates that the builtin -h or --help were provided
2015-11-01 01:57:26 -05:00
var ErrHelp = errors . New ( "help requested by user" )
2023-07-14 14:12:47 -05:00
// ErrVersion indicates that the builtin --version was provided
2016-09-08 23:18:19 -05:00
var ErrVersion = errors . New ( "version requested by user" )
2023-02-08 08:49:03 -06:00
// for monkey patching in example code
var mustParseExit = os . Exit
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 {
2023-02-08 08:49:03 -06:00
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 {
2023-08-06 18:14:16 -05:00
_ , _ = fmt . Fprintln ( config . Out , err )
2023-02-08 08:49:03 -06:00
config . Exit ( - 1 )
return nil
2015-11-01 01:57:26 -05:00
}
2019-05-03 15:07:12 -05:00
2022-10-05 10:59:23 -05:00
p . MustParse ( flags ( ) )
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 {
2020-03-01 16:32:59 -06:00
// 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
2023-02-08 06:01:48 -06:00
2023-02-08 08:49:03 -06:00
// 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
2023-08-06 18:28:23 -05:00
// Environment is a map of environment variables to override those in the process environment, or provide values to those not in the process environment.
Environment map [ string ] string
2016-01-18 12:31:01 -06:00
}
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
2022-09-17 05:39:31 -05:00
epilogue string
2019-05-03 17:49:44 -05:00
2020-01-19 12:38:19 -06:00
// the following field changes during processing of command line arguments
2019-05-03 17:49:44 -05:00
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
}
2022-09-17 05:39:31 -05:00
// 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 ) {
2020-01-24 16:30:29 -06:00
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 )
2020-01-24 16:34:56 -06:00
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 {
2020-01-24 16:30:29 -06:00
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
2016-01-18 12:31:01 -06:00
func NewParser ( config Config , dests ... interface { } ) ( * Parser , error ) {
2023-02-08 08:49:03 -06:00
// 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
2022-10-29 14:13:57 -05:00
// 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 {
2022-10-29 13:47:13 -05:00
// get the value
v := p . val ( spec . dest )
2022-06-09 10:21:29 -05:00
2022-10-29 13:47:13 -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
}
2022-10-29 13:47:13 -05:00
// store as a default
spec . defaultValue = v
2022-06-09 10:21:29 -05:00
2022-10-29 13:47:13 -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
}
2022-10-29 13:47:13 -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 ( )
}
2022-09-17 05:39:31 -05:00
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 {
2021-05-24 23:45:11 -05:00
// check for the ignore switch in the tag
2019-04-14 21:50:17 -05:00
tag := field . Tag . Get ( "arg" )
2021-05-24 23:45:11 -05:00
if tag == "-" {
2019-04-14 21:50:17 -05:00
return false
2015-10-31 18:15:24 -05:00
}
2021-05-24 23:45:11 -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
2021-05-24 23:45:11 -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
2020-01-24 16:30:29 -06:00
subdest := dest . Child ( field )
2019-04-14 21:50:17 -05:00
spec := spec {
2021-01-31 21:40:38 -06:00
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
2023-06-29 14:26:34 -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
2021-01-31 21:40:38 -06:00
} else if spec . long != "" {
spec . placeholder = strings . ToUpper ( spec . long )
2019-11-29 15:22:21 -06:00
} else {
2021-01-31 21:40:38 -06:00
spec . placeholder = strings . ToUpper ( spec . field . Name )
2019-11-29 13:33:16 -06:00
}
2022-10-29 13:47:13 -05: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.
2022-10-29 13:47:13 -05:00
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
2022-10-29 13:47:13 -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
}
2022-10-29 13:47:13 -05:00
// 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" ,
2019-10-08 18:39:00 -05:00
t . Name ( ) , field . Name ) )
return false
}
2022-10-29 13:47:13 -05:00
// parse the default value
spec . defaultString = defaultString
2022-10-29 14:21:21 -05:00
if field . Type . Kind ( ) == reflect . Ptr {
2022-10-29 13:47:13 -05:00
// 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
2022-10-29 13:47:13 -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" ) )
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
}
2022-10-05 10:59:23 -05:00
func ( p * Parser ) MustParse ( args [ ] string ) {
err := p . Parse ( args )
switch {
2023-08-06 18:13:48 -05:00
case errors . Is ( err , ErrHelp ) :
2023-02-08 08:49:03 -06:00
p . writeHelpForSubcommand ( p . config . Out , p . lastCmd )
p . config . Exit ( 0 )
2023-08-06 18:13:48 -05:00
case errors . Is ( err , ErrVersion ) :
2023-08-06 18:14:16 -05:00
_ , _ = fmt . Fprintln ( p . config . Out , p . version )
2023-02-08 08:49:03 -06:00
p . config . Exit ( 0 )
2022-10-05 10:59:23 -05:00
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
}
2023-08-06 18:28:23 -05:00
var value string
var found bool
if ! p . config . IgnoreEnv {
value , found = os . LookupEnv ( spec . env )
}
if p . config . Environment != nil {
value , found = p . config . Environment [ spec . env ]
}
2019-04-14 20:00:40 -05:00
if ! found {
continue
}
2021-04-19 15:21:04 -05:00
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
2021-04-20 21:09:47 -05:00
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
}
2021-04-19 15:21:04 -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 {
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
2023-08-06 18:28:23 -05:00
if ! p . config . IgnoreEnv || p . config . Environment != nil {
2020-03-01 16:32:59 -06:00
err := p . captureEnvVars ( specs , wasPresent )
if err != nil {
return err
}
2019-04-30 14:54:28 -05:00
}
2023-07-14 14:12:47 -05:00
// determine if the current command has a version option spec
var hasVersionOption bool
for _ , spec := range curCmd . specs {
if spec . long == "version" {
hasVersionOption = true
break
}
}
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 )
2022-06-05 10:54:46 -05:00
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 15:40:45 -05:00
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
2023-08-06 18:28:23 -05:00
if ! p . config . IgnoreEnv || p . config . Environment != nil {
2020-03-01 16:32:59 -06:00
err := p . captureEnvVars ( subcmd . specs , wasPresent )
if err != nil {
return err
}
2019-04-30 14:54:28 -05:00
}
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" :
2023-07-14 14:12:47 -05:00
if ! hasVersionOption && p . version != "" {
return ErrVersion
}
2019-05-03 17:49:44 -05:00
}
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
2021-04-19 15:21:04 -05:00
if spec . cardinality == multiple {
2015-10-31 18:15:24 -05:00
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 )
}
2021-04-19 15:21:04 -05:00
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
2016-01-23 22:49:57 -06:00
// use boolean because this takes account of TextUnmarshaler
2021-04-19 15:21:04 -05:00
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 )
}
2021-01-31 21:40:38 -06:00
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 ++
}
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
2021-04-19 15:21:04 -05:00
if spec . cardinality == multiple {
err := setSliceOrMap ( p . val ( spec . dest ) , positionals , true )
2019-04-14 19:30:53 -05:00
if err != nil {
2021-01-31 21:40:38 -06:00
return fmt . Errorf ( "error processing %s: %v" , spec . field . Name , err )
2019-04-14 19:30:53 -05:00
}
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 {
2021-01-31 21:40:38 -06:00
return fmt . Errorf ( "error processing %s: %v" , spec . field . Name , 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
}
2021-01-31 21:40:38 -06:00
name := strings . ToLower ( spec . field . Name )
2020-12-19 17:54:03 -06:00
if spec . long != "" && ! spec . positional {
2019-10-08 18:39:00 -05:00
name = "--" + spec . long
}
if spec . required {
2023-06-29 14:26:34 -05:00
if spec . short == "" && spec . long == "" {
2023-06-03 05:47:47 -05:00
msg := fmt . Sprintf ( "environment variable %s is required" , spec . env )
return errors . New ( msg )
}
2021-10-01 06:35:15 -05:00
msg := fmt . Sprintf ( "%s is required" , name )
if spec . env != "" {
msg += " (or environment variable " + spec . env + ")"
}
2023-06-03 05:47:47 -05:00
2021-10-01 06:35:15 -05:00
return errors . New ( msg )
2019-04-14 20:00:40 -05:00
}
2022-10-29 14:19:23 -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.
2022-10-29 13:47:13 -05:00
p . val ( spec . dest ) . Set ( spec . defaultValue )
2019-10-08 18:39:00 -05:00
}
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 ( )
}
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
}