diff options
Diffstat (limited to 'vendor/github.com/alexflint/go-arg')
| -rw-r--r-- | vendor/github.com/alexflint/go-arg/.gitignore | 24 | ||||
| -rw-r--r-- | vendor/github.com/alexflint/go-arg/LICENSE | 24 | ||||
| -rw-r--r-- | vendor/github.com/alexflint/go-arg/README.md | 552 | ||||
| -rw-r--r-- | vendor/github.com/alexflint/go-arg/doc.go | 39 | ||||
| -rw-r--r-- | vendor/github.com/alexflint/go-arg/parse.go | 729 | ||||
| -rw-r--r-- | vendor/github.com/alexflint/go-arg/reflect.go | 107 | ||||
| -rw-r--r-- | vendor/github.com/alexflint/go-arg/sequence.go | 123 | ||||
| -rw-r--r-- | vendor/github.com/alexflint/go-arg/subcommand.go | 37 | ||||
| -rw-r--r-- | vendor/github.com/alexflint/go-arg/usage.go | 335 |
9 files changed, 1970 insertions, 0 deletions
diff --git a/vendor/github.com/alexflint/go-arg/.gitignore b/vendor/github.com/alexflint/go-arg/.gitignore new file mode 100644 index 0000000..daf913b --- /dev/null +++ b/vendor/github.com/alexflint/go-arg/.gitignore @@ -0,0 +1,24 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof diff --git a/vendor/github.com/alexflint/go-arg/LICENSE b/vendor/github.com/alexflint/go-arg/LICENSE new file mode 100644 index 0000000..a50c494 --- /dev/null +++ b/vendor/github.com/alexflint/go-arg/LICENSE @@ -0,0 +1,24 @@ +Copyright (c) 2015, Alex Flint +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + diff --git a/vendor/github.com/alexflint/go-arg/README.md b/vendor/github.com/alexflint/go-arg/README.md new file mode 100644 index 0000000..dab2996 --- /dev/null +++ b/vendor/github.com/alexflint/go-arg/README.md @@ -0,0 +1,552 @@ +<h1 align="center"> + <img src="./.github/banner.jpg" alt="go-arg" height="250px"> + <br> + go-arg + </br> +</h1> +<h4 align="center">Struct-based argument parsing for Go</h4> +<p align="center"> + <a href="https://sourcegraph.com/github.com/alexflint/go-arg?badge"><img src="https://sourcegraph.com/github.com/alexflint/go-arg/-/badge.svg" alt="Sourcegraph"></a> + <a href="https://pkg.go.dev/github.com/alexflint/go-arg"><img src="https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white&style=flat-square" alt="Documentation"></a> + <a href="https://github.com/alexflint/go-arg/actions"><img src="https://github.com/alexflint/go-arg/workflows/Go/badge.svg" alt="Build Status"></a> + <a href="https://codecov.io/gh/alexflint/go-arg"><img src="https://codecov.io/gh/alexflint/go-arg/branch/master/graph/badge.svg" alt="Coverage Status"></a> + <a href="https://goreportcard.com/report/github.com/alexflint/go-arg"><img src="https://goreportcard.com/badge/github.com/alexflint/go-arg" alt="Go Report Card"></a> +</p> +<br> + +Declare command line arguments for your program by defining a struct. + +```go +var args struct { + Foo string + Bar bool +} +arg.MustParse(&args) +fmt.Println(args.Foo, args.Bar) +``` + +```shell +$ ./example --foo=hello --bar +hello true +``` + +### Installation + +```shell +go get github.com/alexflint/go-arg +``` + +### Required arguments + +```go +var args struct { + ID int `arg:"required"` + Timeout time.Duration +} +arg.MustParse(&args) +``` + +```shell +$ ./example +Usage: example --id ID [--timeout TIMEOUT] +error: --id is required +``` + +### Positional arguments + +```go +var args struct { + Input string `arg:"positional"` + Output []string `arg:"positional"` +} +arg.MustParse(&args) +fmt.Println("Input:", args.Input) +fmt.Println("Output:", args.Output) +``` + +``` +$ ./example src.txt x.out y.out z.out +Input: src.txt +Output: [x.out y.out z.out] +``` + +### Environment variables + +```go +var args struct { + Workers int `arg:"env"` +} +arg.MustParse(&args) +fmt.Println("Workers:", args.Workers) +``` + +``` +$ WORKERS=4 ./example +Workers: 4 +``` + +``` +$ WORKERS=4 ./example --workers=6 +Workers: 6 +``` + +You can also override the name of the environment variable: + +```go +var args struct { + Workers int `arg:"env:NUM_WORKERS"` +} +arg.MustParse(&args) +fmt.Println("Workers:", args.Workers) +``` + +``` +$ NUM_WORKERS=4 ./example +Workers: 4 +``` + +You can provide multiple values using the CSV (RFC 4180) format: + +```go +var args struct { + Workers []int `arg:"env"` +} +arg.MustParse(&args) +fmt.Println("Workers:", args.Workers) +``` + +``` +$ WORKERS='1,99' ./example +Workers: [1 99] +``` + +### Usage strings +```go +var args struct { + Input string `arg:"positional"` + Output []string `arg:"positional"` + Verbose bool `arg:"-v,--verbose" help:"verbosity level"` + Dataset string `help:"dataset to use"` + Optimize int `arg:"-O" help:"optimization level"` +} +arg.MustParse(&args) +``` + +```shell +$ ./example -h +Usage: [--verbose] [--dataset DATASET] [--optimize OPTIMIZE] [--help] INPUT [OUTPUT [OUTPUT ...]] + +Positional arguments: + INPUT + OUTPUT + +Options: + --verbose, -v verbosity level + --dataset DATASET dataset to use + --optimize OPTIMIZE, -O OPTIMIZE + optimization level + --help, -h print this help message +``` + +### Default values + +```go +var args struct { + Foo string `default:"abc"` + Bar bool +} +arg.MustParse(&args) +``` + +### Default values (before v1.2) + +```go +var args struct { + Foo string + Bar bool +} +arg.Foo = "abc" +arg.MustParse(&args) +``` + +### Combining command line options, environment variables, and default values + +You can combine command line arguments, environment variables, and default values. Command line arguments take precedence over environment variables, which take precedence over default values. This means that we check whether a certain option was provided on the command line, then if not, we check for an environment variable (only if an `env` tag was provided), then if none is found, we check for a `default` tag containing a default value. + +```go +var args struct { + Test string `arg:"-t,env:TEST" default:"something"` +} +arg.MustParse(&args) +``` + +### Arguments with multiple values +```go +var args struct { + Database string + IDs []int64 +} +arg.MustParse(&args) +fmt.Printf("Fetching the following IDs from %s: %q", args.Database, args.IDs) +``` + +```shell +./example -database foo -ids 1 2 3 +Fetching the following IDs from foo: [1 2 3] +``` + +### Arguments that can be specified multiple times, mixed with positionals +```go +var args struct { + Commands []string `arg:"-c,separate"` + Files []string `arg:"-f,separate"` + Databases []string `arg:"positional"` +} +arg.MustParse(&args) +``` + +```shell +./example -c cmd1 db1 -f file1 db2 -c cmd2 -f file2 -f file3 db3 -c cmd3 +Commands: [cmd1 cmd2 cmd3] +Files [file1 file2 file3] +Databases [db1 db2 db3] +``` + +### Arguments with keys and values +```go +var args struct { + UserIDs map[string]int +} +arg.MustParse(&args) +fmt.Println(args.UserIDs) +``` + +```shell +./example --userids john=123 mary=456 +map[john:123 mary:456] +``` + +### Custom validation +```go +var args struct { + Foo string + Bar string +} +p := arg.MustParse(&args) +if args.Foo == "" && args.Bar == "" { + p.Fail("you must provide either --foo or --bar") +} +``` + +```shell +./example +Usage: samples [--foo FOO] [--bar BAR] +error: you must provide either --foo or --bar +``` + +### Version strings + +```go +type args struct { + ... +} + +func (args) Version() string { + return "someprogram 4.3.0" +} + +func main() { + var args args + arg.MustParse(&args) +} +``` + +```shell +$ ./example --version +someprogram 4.3.0 +``` + +### Overriding option names + +```go +var args struct { + Short string `arg:"-s"` + Long string `arg:"--custom-long-option"` + ShortAndLong string `arg:"-x,--my-option"` + OnlyShort string `arg:"-o,--"` +} +arg.MustParse(&args) +``` + +```shell +$ ./example --help +Usage: example [-o ONLYSHORT] [--short SHORT] [--custom-long-option CUSTOM-LONG-OPTION] [--my-option MY-OPTION] + +Options: + --short SHORT, -s SHORT + --custom-long-option CUSTOM-LONG-OPTION + --my-option MY-OPTION, -x MY-OPTION + -o ONLYSHORT + --help, -h display this help and exit +``` + + +### Embedded structs + +The fields of embedded structs are treated just like regular fields: + +```go + +type DatabaseOptions struct { + Host string + Username string + Password string +} + +type LogOptions struct { + LogFile string + Verbose bool +} + +func main() { + var args struct { + DatabaseOptions + LogOptions + } + arg.MustParse(&args) +} +``` + +As usual, any field tagged with `arg:"-"` is ignored. + +### Supported types + +The following types may be used as arguments: +- built-in integer types: `int, int8, int16, int32, int64, byte, rune` +- built-in floating point types: `float32, float64` +- strings +- booleans +- URLs represented as `url.URL` +- time durations represented as `time.Duration` +- email addresses represented as `mail.Address` +- MAC addresses represented as `net.HardwareAddr` +- pointers to any of the above +- slices of any of the above +- maps using any of the above as keys and values +- any type that implements `encoding.TextUnmarshaler` + +### Custom parsing + +Implement `encoding.TextUnmarshaler` to define your own parsing logic. + +```go +// Accepts command line arguments of the form "head.tail" +type NameDotName struct { + Head, Tail string +} + +func (n *NameDotName) UnmarshalText(b []byte) error { + s := string(b) + pos := strings.Index(s, ".") + if pos == -1 { + return fmt.Errorf("missing period in %s", s) + } + n.Head = s[:pos] + n.Tail = s[pos+1:] + return nil +} + +func main() { + var args struct { + Name NameDotName + } + arg.MustParse(&args) + fmt.Printf("%#v\n", args.Name) +} +``` +```shell +$ ./example --name=foo.bar +main.NameDotName{Head:"foo", Tail:"bar"} + +$ ./example --name=oops +Usage: example [--name NAME] +error: error processing --name: missing period in "oops" +``` + +### Custom parsing with default values + +Implement `encoding.TextMarshaler` to define your own default value strings: + +```go +// Accepts command line arguments of the form "head.tail" +type NameDotName struct { + Head, Tail string +} + +func (n *NameDotName) UnmarshalText(b []byte) error { + // same as previous example +} + +// this is only needed if you want to display a default value in the usage string +func (n *NameDotName) MarshalText() ([]byte, error) { + return []byte(fmt.Sprintf("%s.%s", n.Head, n.Tail)), nil +} + +func main() { + var args struct { + Name NameDotName `default:"file.txt"` + } + arg.MustParse(&args) + fmt.Printf("%#v\n", args.Name) +} +``` +```shell +$ ./example --help +Usage: test [--name NAME] + +Options: + --name NAME [default: file.txt] + --help, -h display this help and exit + +$ ./example +main.NameDotName{Head:"file", Tail:"txt"} +``` + +### Custom placeholders + +*Introduced in version 1.3.0* + +Use the `placeholder` tag to control which placeholder text is used in the usage text. + +```go +var args struct { + Input string `arg:"positional" placeholder:"SRC"` + Output []string `arg:"positional" placeholder:"DST"` + Optimize int `arg:"-O" help:"optimization level" placeholder:"LEVEL"` + MaxJobs int `arg:"-j" help:"maximum number of simultaneous jobs" placeholder:"N"` +} +arg.MustParse(&args) +``` +```shell +$ ./example -h +Usage: example [--optimize LEVEL] [--maxjobs N] SRC [DST [DST ...]] + +Positional arguments: + SRC + DST + +Options: + --optimize LEVEL, -O LEVEL + optimization level + --maxjobs N, -j N maximum number of simultaneous jobs + --help, -h display this help and exit +``` + +### Description strings + +```go +type args struct { + Foo string +} + +func (args) Description() string { + return "this program does this and that" +} + +func main() { + var args args + arg.MustParse(&args) +} +``` + +```shell +$ ./example -h +this program does this and that +Usage: example [--foo FOO] + +Options: + --foo FOO + --help, -h display this help and exit +``` + +### Subcommands + +*Introduced in version 1.1.0* + +Subcommands are commonly used in tools that wish to group multiple functions into a single program. An example is the `git` tool: +```shell +$ git checkout [arguments specific to checking out code] +$ git commit [arguments specific to committing] +$ git push [arguments specific to pushing] +``` + +The strings "checkout", "commit", and "push" are different from simple positional arguments because the options available to the user change depending on which subcommand they choose. + +This can be implemented with `go-arg` as follows: + +```go +type CheckoutCmd struct { + Branch string `arg:"positional"` + Track bool `arg:"-t"` +} +type CommitCmd struct { + All bool `arg:"-a"` + Message string `arg:"-m"` +} +type PushCmd struct { + Remote string `arg:"positional"` + Branch string `arg:"positional"` + SetUpstream bool `arg:"-u"` +} +var args struct { + Checkout *CheckoutCmd `arg:"subcommand:checkout"` + Commit *CommitCmd `arg:"subcommand:commit"` + Push *PushCmd `arg:"subcommand:push"` + Quiet bool `arg:"-q"` // this flag is global to all subcommands +} + +arg.MustParse(&args) + +switch { +case args.Checkout != nil: + fmt.Printf("checkout requested for branch %s\n", args.Checkout.Branch) +case args.Commit != nil: + fmt.Printf("commit requested with message \"%s\"\n", args.Commit.Message) +case args.Push != nil: + fmt.Printf("push requested from %s to %s\n", args.Push.Branch, args.Push.Remote) +} +``` + +Some additional rules apply when working with subcommands: +* The `subcommand` tag can only be used with fields that are pointers to structs +* Any struct that contains a subcommand must not contain any positionals + +This package allows to have a program that accepts subcommands, but also does something else +when no subcommands are specified. +If on the other hand you want the program to terminate when no subcommands are specified, +the recommended way is: + +```go +p := arg.MustParse(&args) +if p.Subcommand() == nil { + p.Fail("missing subcommand") +} +``` + +### API Documentation + +https://godoc.org/github.com/alexflint/go-arg + +### Rationale + +There are many command line argument parsing libraries for Go, including one in the standard library, so why build another? + +The `flag` library that ships in the standard library seems awkward to me. Positional arguments must preceed options, so `./prog x --foo=1` does what you expect but `./prog --foo=1 x` does not. It also does not allow arguments to have both long (`--foo`) and short (`-f`) forms. + +Many third-party argument parsing libraries are great for writing sophisticated command line interfaces, but feel to me like overkill for a simple script with a few flags. + +The idea behind `go-arg` is that Go already has an excellent way to describe data structures using structs, so there is no need to develop additional levels of abstraction. Instead of one API to specify which arguments your program accepts, and then another API to get the values of those arguments, `go-arg` replaces both with a single struct. + +### Backward compatibility notes + +Earlier versions of this library required the help text to be part of the `arg` tag. This is still supported but is now deprecated. Instead, you should use a separate `help` tag, described above, which removes most of the limits on the text you can write. In particular, you will need to use the new `help` tag if your help text includes any commas. diff --git a/vendor/github.com/alexflint/go-arg/doc.go b/vendor/github.com/alexflint/go-arg/doc.go new file mode 100644 index 0000000..3b0bafd --- /dev/null +++ b/vendor/github.com/alexflint/go-arg/doc.go @@ -0,0 +1,39 @@ +// Package arg parses command line arguments using the fields from a struct. +// +// For example, +// +// var args struct { +// Iter int +// Debug bool +// } +// arg.MustParse(&args) +// +// defines two command line arguments, which can be set using any of +// +// ./example --iter=1 --debug // debug is a boolean flag so its value is set to true +// ./example -iter 1 // debug defaults to its zero value (false) +// ./example --debug=true // iter defaults to its zero value (zero) +// +// The fastest way to see how to use go-arg is to read the examples below. +// +// Fields can be bool, string, any float type, or any signed or unsigned integer type. +// They can also be slices of any of the above, or slices of pointers to any of the above. +// +// Tags can be specified using the `arg` and `help` tag names: +// +// var args struct { +// Input string `arg:"positional"` +// Log string `arg:"positional,required"` +// Debug bool `arg:"-d" help:"turn on debug mode"` +// RealMode bool `arg:"--real" +// Wr io.Writer `arg:"-"` +// } +// +// Any tag string that starts with a single hyphen is the short form for an argument +// (e.g. `./example -d`), and any tag string that starts with two hyphens is the long +// form for the argument (instead of the field name). +// +// Other valid tag strings are `positional` and `required`. +// +// Fields can be excluded from processing with `arg:"-"`. +package arg diff --git a/vendor/github.com/alexflint/go-arg/parse.go b/vendor/github.com/alexflint/go-arg/parse.go new file mode 100644 index 0000000..7588dfb --- /dev/null +++ b/vendor/github.com/alexflint/go-arg/parse.go @@ -0,0 +1,729 @@ +package arg + +import ( + "encoding" + "encoding/csv" + "errors" + "fmt" + "os" + "path/filepath" + "reflect" + "strings" + + scalar "github.com/alexflint/go-scalar" +) + +// 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 +} + +// String gets a string representation of the given path +func (p path) String() string { + s := "args" + for _, f := range p.fields { + s += "." + f.Name + } + return s +} + +// Child gets a new path representing a child of this path. +func (p path) Child(f reflect.StructField) path { + // 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 + return path{ + root: p.root, + fields: subfields, + } +} + +// 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 + defaultVal string // default value for this option + placeholder string // name of the data in help +} + +// command represents a named subcommand, or the top-level command +type command struct { + name string + help string + dest path + specs []*spec + subcommands []*command + parent *command +} + +// ErrHelp indicates that -h or --help were provided +var ErrHelp = errors.New("help requested by user") + +// ErrVersion indicates that --version was provided +var ErrVersion = errors.New("version requested by user") + +// MustParse processes command line arguments and exits upon failure +func MustParse(dest ...interface{}) *Parser { + p, err := NewParser(Config{}, dest...) + if err != nil { + fmt.Fprintln(stdout, err) + osExit(-1) + return nil // just in case osExit was monkey-patched + } + + err = p.Parse(flags()) + switch { + case err == ErrHelp: + p.writeHelpForSubcommand(stdout, p.lastCmd) + osExit(0) + case err == ErrVersion: + fmt.Fprintln(stdout, p.version) + osExit(0) + case err != nil: + p.failWithSubcommand(err.Error(), p.lastCmd) + } + + return p +} + +// Parse processes command line arguments and stores them in dest +func Parse(dest ...interface{}) error { + p, err := NewParser(Config{}, dest...) + if err != nil { + return err + } + return p.Parse(flags()) +} + +// flags gets all command line arguments other than the first (program name) +func flags() []string { + if len(os.Args) == 0 { // os.Args could be empty + return nil + } + return os.Args[1:] +} + +// 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 +} + +// Parser represents a set of command line options with destination values +type Parser struct { + cmd *command + roots []reflect.Value + config Config + version string + description string + + // the following field changes during processing of command line arguments + lastCmd *command +} + +// 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 +} + +// 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 +} + +// walkFields calls a function for each field of a struct, recursively expanding struct fields. +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) { + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + field.Index = make([]int, len(path)+1) + copy(field.Index, append(path, i)) + expand := visit(field, t) + if expand && field.Type.Kind() == reflect.Struct { + var subpath []int + if field.Anonymous { + subpath = append(path, i) + } + walkFieldsImpl(field.Type, visit, subpath) + } + } +} + +// NewParser constructs a parser from a list of destination structs +func NewParser(config Config, dests ...interface{}) (*Parser, error) { + // 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 + p := Parser{ + cmd: &command{name: name}, + config: config, + } + + // make a list of roots + for _, dest := range dests { + 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)) + } + + cmd, err := cmdFromStruct(name, path{root: i}, t) + if err != nil { + return nil, err + } + + // 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 { + return nil, fmt.Errorf("%v: error marshaling default value to string: %v", spec.dest, err) + } + spec.defaultVal = string(str) + } else { + spec.defaultVal = fmt.Sprintf("%v", v) + } + } + } + + p.cmd.specs = append(p.cmd.specs, cmd.specs...) + p.cmd.subcommands = append(p.cmd.subcommands, cmd.subcommands...) + + if dest, ok := dest.(Versioned); ok { + p.version = dest.Version() + } + if dest, ok := dest.(Described); ok { + p.description = dest.Description() + } + } + + return &p, nil +} + +func cmdFromStruct(name string, dest path, t reflect.Type) (*command, error) { + // commands can only be created from pointers to structs + if t.Kind() != reflect.Ptr { + return nil, fmt.Errorf("subcommands must be pointers to structs but %s is a %s", + dest, t.Kind()) + } + + t = t.Elem() + if t.Kind() != reflect.Struct { + 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, + } + + 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 + } + + // 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 + if field.Anonymous && field.Type.Kind() == reflect.Struct { + return true + } + + // ignore any other unexported field + if !isExported(field.Name) { + return false + } + + // duplicate the entire path to avoid slice overwrites + subdest := dest.Child(field) + spec := spec{ + dest: subdest, + field: field, + long: strings.ToLower(field.Name), + } + + help, exists := field.Tag.Lookup("help") + if exists { + spec.help = help + } + + defaultVal, hasDefault := field.Tag.Lookup("default") + if hasDefault { + spec.defaultVal = defaultVal + } + + // Look at the tag + var isSubcommand bool // tracks whether this field is a subcommand + for _, key := range strings.Split(tag, ",") { + if key == "" { + continue + } + key = strings.TrimLeft(key, " ") + var value string + if pos := strings.Index(key, ":"); pos != -1 { + value = key[pos+1:] + key = key[:pos] + } + + 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": + 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 + } + 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) + } + + // parse the subcommand recursively + subcmd, err := cmdFromStruct(cmdname, subdest, field.Type) + if err != nil { + errs = append(errs, err.Error()) + return false + } + + 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 + } + } + + placeholder, hasPlaceholder := field.Tag.Lookup("placeholder") + if hasPlaceholder { + spec.placeholder = placeholder + } else if spec.long != "" { + spec.placeholder = strings.ToUpper(spec.long) + } else { + spec.placeholder = strings.ToUpper(spec.field.Name) + } + + // 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 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 + } + if spec.cardinality == multiple && hasDefault { + errs = append(errs, fmt.Sprintf("%s.%s: default values are not supported for slice or map fields", + t.Name(), field.Name)) + return false + } + } + + // if this was an embedded field then we already returned true up above + return false + }) + + if len(errs) > 0 { + return nil, errors.New(strings.Join(errs, "\n")) + } + + // 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 { + return nil, fmt.Errorf("%s cannot have both subcommands and positional arguments", dest) + } + + 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 +} + +// process environment vars for the given arguments +func (p *Parser) captureEnvVars(specs []*spec, wasPresent map[*spec]bool) error { + for _, spec := range specs { + if spec.env == "" { + continue + } + + value, found := os.LookupEnv(spec.env) + if !found { + continue + } + + if spec.cardinality == multiple { + // 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, + ) + } + } + if err = setSliceOrMap(p.val(spec.dest), values, !spec.separate); err != nil { + 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 { + return fmt.Errorf("error processing environment variable %s: %v", spec.env, err) + } + } + wasPresent[spec] = true + } + + 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 + + // 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 + } + } + + // 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 + } + + if !isFlag(arg) || allpositional { + // 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) + v.Set(reflect.New(v.Type().Elem())) // we already checked that all subcommands are struct pointers + + // add the new options to the set of allowed options + specs = append(specs, subcmd.specs...) + + // capture environment vars for these new options + if !p.config.IgnoreEnv { + err := p.captureEnvVars(subcmd.specs, wasPresent) + if err != nil { + return err + } + } + + curCmd = subcmd + p.lastCmd = curCmd + continue + } + + // check for special --help and --version flags + switch arg { + case "-h", "--help": + return ErrHelp + case "--version": + return ErrVersion + } + + // 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] + } + + // 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 { + return fmt.Errorf("unknown argument %s", arg) + } + wasPresent[spec] = true + + // deal with the case of multiple values + if spec.cardinality == multiple { + var values []string + if value == "" { + for i+1 < len(args) && !isFlag(args[i+1]) && args[i+1] != "--" { + values = append(values, args[i+1]) + i++ + if spec.separate { + break + } + } + } else { + values = append(values, value) + } + err := setSliceOrMap(p.val(spec.dest), values, !spec.separate) + if err != nil { + return fmt.Errorf("error processing %s: %v", arg, err) + } + 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 == "" { + value = "true" + } + + // if we have something like "--foo" then the value is the next argument + if value == "" { + 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]) { + return fmt.Errorf("missing value for %s", arg) + } + value = args[i+1] + i++ + } + + err := scalar.ParseValue(p.val(spec.dest), value) + if err != nil { + return fmt.Errorf("error processing %s: %v", arg, err) + } + } + + // process positionals + for _, spec := range specs { + if !spec.positional { + continue + } + if len(positionals) == 0 { + break + } + 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 + } else { + err := scalar.ParseValue(p.val(spec.dest), positionals[0]) + if err != nil { + return fmt.Errorf("error processing %s: %v", spec.field.Name, err) + } + positionals = positionals[1:] + } + } + if len(positionals) > 0 { + return fmt.Errorf("too many positional arguments at '%s'", positionals[0]) + } + + // fill in defaults and check that all the required args were provided + for _, spec := range specs { + if wasPresent[spec] { + continue + } + + name := strings.ToLower(spec.field.Name) + if spec.long != "" && !spec.positional { + name = "--" + spec.long + } + + if spec.required { + msg := fmt.Sprintf("%s is required", name) + if spec.env != "" { + msg += " (or environment variable " + spec.env + ")" + } + return errors.New(msg) + } + 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) + } + } + } + + return nil +} + +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 + } +} + +// 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 { + v := p.roots[dest.root] + for _, field := range dest.fields { + if v.Kind() == reflect.Ptr { + if v.IsNil() { + return reflect.Value{} + } + v = v.Elem() + } + + v = v.FieldByIndex(field.Index) + } + return v +} + +// 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 +} diff --git a/vendor/github.com/alexflint/go-arg/reflect.go b/vendor/github.com/alexflint/go-arg/reflect.go new file mode 100644 index 0000000..cd80be7 --- /dev/null +++ b/vendor/github.com/alexflint/go-arg/reflect.go @@ -0,0 +1,107 @@ +package arg + +import ( + "encoding" + "fmt" + "reflect" + "unicode" + "unicode/utf8" + + scalar "github.com/alexflint/go-scalar" +) + +var textUnmarshalerType = reflect.TypeOf([]encoding.TextUnmarshaler{}).Elem() + +// cardinality tracks how many tokens are expected for a given spec +// - zero is a boolean, which does to expect any value +// - one is an ordinary option that will be parsed from a single token +// - multiple is a slice or map that can accept zero or more tokens +type cardinality int + +const ( + zero cardinality = iota + one + multiple + unsupported +) + +func (k cardinality) String() string { + switch k { + case zero: + return "zero" + case one: + return "one" + case multiple: + return "multiple" + case unsupported: + return "unsupported" + default: + return fmt.Sprintf("unknown(%d)", int(k)) + } +} + +// cardinalityOf returns true if the type can be parsed from a string +func cardinalityOf(t reflect.Type) (cardinality, error) { + if scalar.CanParse(t) { + if isBoolean(t) { + return zero, nil + } + return one, nil + } + + // look inside pointer types + if t.Kind() == reflect.Ptr { + t = t.Elem() + } + + // look inside slice and map types + switch t.Kind() { + case reflect.Slice: + if !scalar.CanParse(t.Elem()) { + return unsupported, fmt.Errorf("cannot parse into %v because %v not supported", t, t.Elem()) + } + return multiple, nil + case reflect.Map: + if !scalar.CanParse(t.Key()) { + return unsupported, fmt.Errorf("cannot parse into %v because key type %v not supported", t, t.Elem()) + } + if !scalar.CanParse(t.Elem()) { + return unsupported, fmt.Errorf("cannot parse into %v because value type %v not supported", t, t.Elem()) + } + return multiple, nil + default: + return unsupported, fmt.Errorf("cannot parse into %v", t) + } +} + +// isBoolean returns true if the type can be parsed from a single string +func isBoolean(t reflect.Type) bool { + switch { + case t.Implements(textUnmarshalerType): + return false + case t.Kind() == reflect.Bool: + return true + case t.Kind() == reflect.Ptr && t.Elem().Kind() == reflect.Bool: + return true + default: + return false + } +} + +// isExported returns true if the struct field name is exported +func isExported(field string) bool { + r, _ := utf8.DecodeRuneInString(field) // returns RuneError for empty string or invalid UTF8 + return unicode.IsLetter(r) && unicode.IsUpper(r) +} + +// 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 || t.Kind() == reflect.Map { + return v.IsNil() + } + if !t.Comparable() { + return false + } + return v.Interface() == reflect.Zero(t).Interface() +} diff --git a/vendor/github.com/alexflint/go-arg/sequence.go b/vendor/github.com/alexflint/go-arg/sequence.go new file mode 100644 index 0000000..35a3614 --- /dev/null +++ b/vendor/github.com/alexflint/go-arg/sequence.go @@ -0,0 +1,123 @@ +package arg + +import ( + "fmt" + "reflect" + "strings" + + scalar "github.com/alexflint/go-scalar" +) + +// setSliceOrMap parses a sequence of strings into a slice or map. If clear is +// true then any values already in the slice or map are first removed. +func setSliceOrMap(dest reflect.Value, values []string, clear bool) error { + if !dest.CanSet() { + return fmt.Errorf("field is not writable") + } + + t := dest.Type() + if t.Kind() == reflect.Ptr { + dest = dest.Elem() + t = t.Elem() + } + + switch t.Kind() { + case reflect.Slice: + return setSlice(dest, values, clear) + case reflect.Map: + return setMap(dest, values, clear) + default: + return fmt.Errorf("setSliceOrMap cannot insert values into a %v", t) + } +} + +// setSlice parses a sequence of strings and inserts them into a slice. If clear +// is true then any values already in the slice are removed. +func setSlice(dest reflect.Value, values []string, clear bool) error { + var ptr bool + elem := dest.Type().Elem() + if elem.Kind() == reflect.Ptr && !elem.Implements(textUnmarshalerType) { + ptr = true + elem = elem.Elem() + } + + // clear the slice in case default values exist + if clear && !dest.IsNil() { + dest.SetLen(0) + } + + // parse the values one-by-one + for _, s := range values { + v := reflect.New(elem) + if err := scalar.ParseValue(v.Elem(), s); err != nil { + return err + } + if !ptr { + v = v.Elem() + } + dest.Set(reflect.Append(dest, v)) + } + return nil +} + +// setMap parses a sequence of name=value strings and inserts them into a map. +// If clear is true then any values already in the map are removed. +func setMap(dest reflect.Value, values []string, clear bool) error { + // determine the key and value type + var keyIsPtr bool + keyType := dest.Type().Key() + if keyType.Kind() == reflect.Ptr && !keyType.Implements(textUnmarshalerType) { + keyIsPtr = true + keyType = keyType.Elem() + } + + var valIsPtr bool + valType := dest.Type().Elem() + if valType.Kind() == reflect.Ptr && !valType.Implements(textUnmarshalerType) { + valIsPtr = true + valType = valType.Elem() + } + + // clear the slice in case default values exist + if clear && !dest.IsNil() { + for _, k := range dest.MapKeys() { + dest.SetMapIndex(k, reflect.Value{}) + } + } + + // allocate the map if it is not allocated + if dest.IsNil() { + dest.Set(reflect.MakeMap(dest.Type())) + } + + // parse the values one-by-one + for _, s := range values { + // split at the first equals sign + pos := strings.Index(s, "=") + if pos == -1 { + return fmt.Errorf("cannot parse %q into a map, expected format key=value", s) + } + + // parse the key + k := reflect.New(keyType) + if err := scalar.ParseValue(k.Elem(), s[:pos]); err != nil { + return err + } + if !keyIsPtr { + k = k.Elem() + } + + // parse the value + v := reflect.New(valType) + if err := scalar.ParseValue(v.Elem(), s[pos+1:]); err != nil { + return err + } + if !valIsPtr { + v = v.Elem() + } + + // add it to the map + dest.SetMapIndex(k, v) + } + return nil +} diff --git a/vendor/github.com/alexflint/go-arg/subcommand.go b/vendor/github.com/alexflint/go-arg/subcommand.go new file mode 100644 index 0000000..dff732c --- /dev/null +++ b/vendor/github.com/alexflint/go-arg/subcommand.go @@ -0,0 +1,37 @@ +package arg + +// Subcommand returns the user struct for the subcommand selected by +// the command line arguments most recently processed by the parser. +// The return value is always a pointer to a struct. If no subcommand +// was specified then it returns the top-level arguments struct. If +// no command line arguments have been processed by this parser then it +// returns nil. +func (p *Parser) Subcommand() interface{} { + if p.lastCmd == nil || p.lastCmd.parent == nil { + return nil + } + return p.val(p.lastCmd.dest).Interface() +} + +// SubcommandNames returns the sequence of subcommands specified by the +// user. If no subcommands were given then it returns an empty slice. +func (p *Parser) SubcommandNames() []string { + if p.lastCmd == nil { + return nil + } + + // make a list of ancestor commands + var ancestors []string + cur := p.lastCmd + for cur.parent != nil { // we want to exclude the root + ancestors = append(ancestors, cur.name) + cur = cur.parent + } + + // reverse the list + out := make([]string, len(ancestors)) + for i := 0; i < len(ancestors); i++ { + out[i] = ancestors[len(ancestors)-i-1] + } + return out +} diff --git a/vendor/github.com/alexflint/go-arg/usage.go b/vendor/github.com/alexflint/go-arg/usage.go new file mode 100644 index 0000000..e936811 --- /dev/null +++ b/vendor/github.com/alexflint/go-arg/usage.go @@ -0,0 +1,335 @@ +package arg + +import ( + "fmt" + "io" + "os" + "strings" +) + +// the width of the left column +const colWidth = 25 + +// to allow monkey patching in tests +var ( + stdout io.Writer = os.Stdout + stderr io.Writer = os.Stderr + osExit = os.Exit +) + +// Fail prints usage information to stderr and exits with non-zero status +func (p *Parser) Fail(msg string) { + p.failWithSubcommand(msg, p.cmd) +} + +// FailSubcommand prints usage information for a specified subcommand to stderr, +// then exits with non-zero status. To write usage information for a top-level +// subcommand, provide just the name of that subcommand. To write usage +// information for a subcommand that is nested under another subcommand, provide +// a sequence of subcommand names starting with the top-level subcommand and so +// on down the tree. +func (p *Parser) FailSubcommand(msg string, subcommand ...string) error { + cmd, err := p.lookupCommand(subcommand...) + if err != nil { + return err + } + p.failWithSubcommand(msg, cmd) + return nil +} + +// failWithSubcommand prints usage information for the given subcommand to stderr and exits with non-zero status +func (p *Parser) failWithSubcommand(msg string, cmd *command) { + p.writeUsageForSubcommand(stderr, cmd) + fmt.Fprintln(stderr, "error:", msg) + osExit(-1) +} + +// WriteUsage writes usage information to the given writer +func (p *Parser) WriteUsage(w io.Writer) { + cmd := p.cmd + if p.lastCmd != nil { + cmd = p.lastCmd + } + p.writeUsageForSubcommand(w, cmd) +} + +// WriteUsageForSubcommand writes the usage information for a specified +// subcommand. To write usage information for a top-level subcommand, provide +// just the name of that subcommand. To write usage information for a subcommand +// that is nested under another subcommand, provide a sequence of subcommand +// names starting with the top-level subcommand and so on down the tree. +func (p *Parser) WriteUsageForSubcommand(w io.Writer, subcommand ...string) error { + cmd, err := p.lookupCommand(subcommand...) + if err != nil { + return err + } + p.writeUsageForSubcommand(w, cmd) + return nil +} + +// writeUsageForSubcommand writes usage information for the given subcommand +func (p *Parser) writeUsageForSubcommand(w io.Writer, cmd *command) { + var positionals, longOptions, shortOptions []*spec + for _, spec := range cmd.specs { + switch { + case spec.positional: + positionals = append(positionals, spec) + case spec.long != "": + longOptions = append(longOptions, spec) + case spec.short != "": + shortOptions = append(shortOptions, spec) + } + } + + if p.version != "" { + fmt.Fprintln(w, p.version) + } + + // make a list of ancestor commands so that we print with full context + var ancestors []string + ancestor := cmd + for ancestor != nil { + ancestors = append(ancestors, ancestor.name) + ancestor = ancestor.parent + } + + // print the beginning of the usage string + fmt.Fprint(w, "Usage:") + for i := len(ancestors) - 1; i >= 0; i-- { + fmt.Fprint(w, " "+ancestors[i]) + } + + // write the option component of the usage message + for _, spec := range shortOptions { + // prefix with a space + fmt.Fprint(w, " ") + if !spec.required { + fmt.Fprint(w, "[") + } + fmt.Fprint(w, synopsis(spec, "-"+spec.short)) + if !spec.required { + fmt.Fprint(w, "]") + } + } + + for _, spec := range longOptions { + // prefix with a space + fmt.Fprint(w, " ") + if !spec.required { + fmt.Fprint(w, "[") + } + fmt.Fprint(w, synopsis(spec, "--"+spec.long)) + if !spec.required { + fmt.Fprint(w, "]") + } + } + + // When we parse positionals, we check that: + // 1. required positionals come before non-required positionals + // 2. there is at most one multiple-value positional + // 3. if there is a multiple-value positional then it comes after all other positionals + // Here we merely print the usage string, so we do not explicitly re-enforce those rules + + // write the positionals in following form: + // REQUIRED1 REQUIRED2 + // REQUIRED1 REQUIRED2 [OPTIONAL1 [OPTIONAL2]] + // REQUIRED1 REQUIRED2 REPEATED [REPEATED ...] + // REQUIRED1 REQUIRED2 [REPEATEDOPTIONAL [REPEATEDOPTIONAL ...]] + // REQUIRED1 REQUIRED2 [OPTIONAL1 [REPEATEDOPTIONAL [REPEATEDOPTIONAL ...]]] + var closeBrackets int + for _, spec := range positionals { + fmt.Fprint(w, " ") + if !spec.required { + fmt.Fprint(w, "[") + closeBrackets += 1 + } + if spec.cardinality == multiple { + fmt.Fprintf(w, "%s [%s ...]", spec.placeholder, spec.placeholder) + } else { + fmt.Fprint(w, spec.placeholder) + } + } + fmt.Fprint(w, strings.Repeat("]", closeBrackets)) + + // if the program supports subcommands, give a hint to the user about their existence + if len(cmd.subcommands) > 0 { + fmt.Fprint(w, " <command> [<args>]") + } + + fmt.Fprint(w, "\n") +} + +func printTwoCols(w io.Writer, left, help string, defaultVal string, envVal string) { + lhs := " " + left + fmt.Fprint(w, lhs) + if help != "" { + if len(lhs)+2 < colWidth { + fmt.Fprint(w, strings.Repeat(" ", colWidth-len(lhs))) + } else { + fmt.Fprint(w, "\n"+strings.Repeat(" ", colWidth)) + } + fmt.Fprint(w, help) + } + + bracketsContent := []string{} + + if defaultVal != "" { + bracketsContent = append(bracketsContent, + fmt.Sprintf("default: %s", defaultVal), + ) + } + + if envVal != "" { + bracketsContent = append(bracketsContent, + fmt.Sprintf("env: %s", envVal), + ) + } + + if len(bracketsContent) > 0 { + fmt.Fprintf(w, " [%s]", strings.Join(bracketsContent, ", ")) + } + fmt.Fprint(w, "\n") +} + +// WriteHelp writes the usage string followed by the full help string for each option +func (p *Parser) WriteHelp(w io.Writer) { + cmd := p.cmd + if p.lastCmd != nil { + cmd = p.lastCmd + } + p.writeHelpForSubcommand(w, cmd) +} + +// WriteHelpForSubcommand writes the usage string followed by the full help +// string for a specified subcommand. To write help for a top-level subcommand, +// provide just the name of that subcommand. To write help for a subcommand that +// is nested under another subcommand, provide a sequence of subcommand names +// starting with the top-level subcommand and so on down the tree. +func (p *Parser) WriteHelpForSubcommand(w io.Writer, subcommand ...string) error { + cmd, err := p.lookupCommand(subcommand...) + if err != nil { + return err + } + p.writeHelpForSubcommand(w, cmd) + return nil +} + +// writeHelp writes the usage string for the given subcommand +func (p *Parser) writeHelpForSubcommand(w io.Writer, cmd *command) { + var positionals, longOptions, shortOptions []*spec + for _, spec := range cmd.specs { + switch { + case spec.positional: + positionals = append(positionals, spec) + case spec.long != "": + longOptions = append(longOptions, spec) + case spec.short != "": + shortOptions = append(shortOptions, spec) + } + } + + if p.description != "" { + fmt.Fprintln(w, p.description) + } + p.writeUsageForSubcommand(w, cmd) + + // write the list of positionals + if len(positionals) > 0 { + fmt.Fprint(w, "\nPositional arguments:\n") + for _, spec := range positionals { + printTwoCols(w, spec.placeholder, spec.help, "", "") + } + } + + // write the list of options with the short-only ones first to match the usage string + if len(shortOptions)+len(longOptions) > 0 || cmd.parent == nil { + fmt.Fprint(w, "\nOptions:\n") + for _, spec := range shortOptions { + p.printOption(w, spec) + } + for _, spec := range longOptions { + p.printOption(w, spec) + } + } + + // obtain a flattened list of options from all ancestors + var globals []*spec + ancestor := cmd.parent + for ancestor != nil { + globals = append(globals, ancestor.specs...) + ancestor = ancestor.parent + } + + // write the list of global options + if len(globals) > 0 { + fmt.Fprint(w, "\nGlobal options:\n") + for _, spec := range globals { + p.printOption(w, spec) + } + } + + // write the list of built in options + p.printOption(w, &spec{ + cardinality: zero, + long: "help", + short: "h", + help: "display this help and exit", + }) + if p.version != "" { + p.printOption(w, &spec{ + cardinality: zero, + long: "version", + help: "display version and exit", + }) + } + + // write the list of subcommands + if len(cmd.subcommands) > 0 { + fmt.Fprint(w, "\nCommands:\n") + for _, subcmd := range cmd.subcommands { + printTwoCols(w, subcmd.name, subcmd.help, "", "") + } + } +} + +func (p *Parser) printOption(w io.Writer, spec *spec) { + ways := make([]string, 0, 2) + if spec.long != "" { + ways = append(ways, synopsis(spec, "--"+spec.long)) + } + if spec.short != "" { + ways = append(ways, synopsis(spec, "-"+spec.short)) + } + if len(ways) > 0 { + printTwoCols(w, strings.Join(ways, ", "), spec.help, spec.defaultVal, spec.env) + } +} + +// lookupCommand finds a subcommand based on a sequence of subcommand names. The +// first string should be a top-level subcommand, the next should be a child +// subcommand of that subcommand, and so on. If no strings are given then the +// root command is returned. If no such subcommand exists then an error is +// returned. +func (p *Parser) lookupCommand(path ...string) (*command, error) { + cmd := p.cmd + for _, name := range path { + var found *command + for _, child := range cmd.subcommands { + if child.name == name { + found = child + } + } + if found == nil { + return nil, fmt.Errorf("%q is not a subcommand of %s", name, cmd.name) + } + cmd = found + } + return cmd, nil +} + +func synopsis(spec *spec, form string) string { + if spec.cardinality == zero { + return form + } + return form + " " + spec.placeholder +} |
