diff --git a/go.mod b/go.mod index 16e19fb1..b2b49581 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/Azure/go-autorest/autorest v0.11.18 github.com/DopplerHQ/cli v0.0.0-20210309042056-414bede8a50e github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 - github.com/alecthomas/kong v0.2.15 + github.com/alecthomas/kong v0.9.0 github.com/aws/aws-sdk-go-v2 v1.24.1 github.com/aws/aws-sdk-go-v2/config v1.26.5 github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.26.2 @@ -68,7 +68,7 @@ require ( github.com/aead/argon2 v0.0.0-20180111183520-a87724528b07 // indirect github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect github.com/alecthomas/colour v0.1.0 // indirect - github.com/alecthomas/repr v0.0.0-20201120212035-bb82daffcca2 // indirect + github.com/alecthomas/repr v0.4.0 // indirect github.com/alessio/shellescape v1.4.1 // indirect github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da // indirect github.com/atotto/clipboard v0.1.4 // indirect diff --git a/go.sum b/go.sum index d4bc7073..ae1e1a91 100644 --- a/go.sum +++ b/go.sum @@ -116,12 +116,13 @@ github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmH github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA= github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 h1:smF2tmSOzy2Mm+0dGI2AIUHY+w0BUc+4tn40djz7+6U= github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI= +github.com/alecthomas/assert/v2 v2.6.0 h1:o3WJwILtexrEUk3cUVal3oiQY2tfgr/FHWiz/v2n4FU= github.com/alecthomas/colour v0.1.0 h1:nOE9rJm6dsZ66RGWYSFrXw461ZIt9A6+nHgL7FRrDUk= github.com/alecthomas/colour v0.1.0/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0= -github.com/alecthomas/kong v0.2.15 h1:HP3K1XuFn0wGSWFGVW67V+65tXw/Ht8FDYiLNAuX2Ug= -github.com/alecthomas/kong v0.2.15/go.mod h1:kQOmtJgV+Lb4aj+I2LEn40cbtawdWJ9Y8QLq+lElKxE= -github.com/alecthomas/repr v0.0.0-20201120212035-bb82daffcca2 h1:G5TeG64Ox4OWq2YwlsxS7nOedU8vbGgNRTRDAjGvDCk= -github.com/alecthomas/repr v0.0.0-20201120212035-bb82daffcca2/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8= +github.com/alecthomas/kong v0.9.0 h1:G5diXxc85KvoV2f0ZRVuMsi45IrBgx9zDNGNj165aPA= +github.com/alecthomas/kong v0.9.0/go.mod h1:Y47y5gKfHp1hDc7CH7OeXgLIpp+Q2m1Ni0L5s3bI8Os= +github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= +github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= @@ -552,6 +553,7 @@ github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKe github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= github.com/heroku/heroku-go/v5 v5.2.1 h1:5g379GyHuOI3qhb1ujFwQ13Kjt96M+KMkV8s7omg+do= github.com/heroku/heroku-go/v5 v5.2.1/go.mod h1:d+1QrZyjbnQJG1f8xIoVvMQRFLt3XRVZOdlm26Sr73U= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174 h1:WlZsjVhE8Af9IcZDGgJGQpNflI3+MJSBhsgT5PCtzBQ= github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= diff --git a/vendor/github.com/alecthomas/kong/.gitignore b/vendor/github.com/alecthomas/kong/.gitignore index ba077a40..e69de29b 100644 --- a/vendor/github.com/alecthomas/kong/.gitignore +++ b/vendor/github.com/alecthomas/kong/.gitignore @@ -1 +0,0 @@ -bin diff --git a/vendor/github.com/alecthomas/kong/.golangci.yml b/vendor/github.com/alecthomas/kong/.golangci.yml index af552572..e8980bfc 100644 --- a/vendor/github.com/alecthomas/kong/.golangci.yml +++ b/vendor/github.com/alecthomas/kong/.golangci.yml @@ -15,17 +15,53 @@ linters: - gocognit - gomnd - goprintffuncname + - paralleltest + - nlreturn + - goerr113 + - ifshort + - testpackage + - wrapcheck + - exhaustivestruct + - forbidigo + - gci + - godot + - gofumpt + - cyclop + - errorlint + - nestif + - golint + - scopelint + - interfacer + - tagliatelle + - thelper + - godox + - goconst + - varnamelen + - ireturn + - exhaustruct + - nonamedreturns + - nilnil + - nosnakecase # deprecated since v1.48.1 + - structcheck # deprecated since v1.49.0 + - deadcode # deprecated since v1.49.0 + - varcheck # deprecated since v1.49.0 + - depguard # nothing to guard against yet + - tagalign # hurts readability of kong tags linters-settings: govet: check-shadowing: true + # These govet checks are disabled by default, but they're useful. + enable: + - niliness + - sortslice + - unusedwrite dupl: threshold: 100 - goconst: - min-len: 5 - min-occurrences: 3 gocyclo: min-complexity: 20 + exhaustive: + default-signifies-exhaustive: true issues: max-per-linter: 0 @@ -40,3 +76,14 @@ issues: - 'bad syntax for struct tag key' - 'bad syntax for struct tag pair' - 'result .* \(error\) is always nil' + + exclude-rules: + # Don't warn on unused parameters. + # Parameter names are useful for documentation. + # Replacing them with '_' hides useful information. + - linters: [revive] + text: 'unused-parameter: parameter \S+ seems to be unused, consider removing or renaming it as _' + + # Duplicate words are okay in tests. + - linters: [dupword] + path: _test\.go diff --git a/vendor/github.com/alecthomas/kong/README.md b/vendor/github.com/alecthomas/kong/README.md index f0b7a30b..22e00f06 100644 --- a/vendor/github.com/alecthomas/kong/README.md +++ b/vendor/github.com/alecthomas/kong/README.md @@ -2,39 +2,43 @@

# Kong is a command-line parser for Go + [![](https://godoc.org/github.com/alecthomas/kong?status.svg)](http://godoc.org/github.com/alecthomas/kong) [![CircleCI](https://img.shields.io/circleci/project/github/alecthomas/kong.svg)](https://circleci.com/gh/alecthomas/kong) [![Go Report Card](https://goreportcard.com/badge/github.com/alecthomas/kong)](https://goreportcard.com/report/github.com/alecthomas/kong) [![Slack chat](https://img.shields.io/static/v1?logo=slack&style=flat&label=slack&color=green&message=gophers)](https://gophers.slack.com/messages/CN9DS8YF3) - + - [Introduction](#introduction) - [Help](#help) - - [Help as a user of a Kong application](#help-as-a-user-of-a-kong-application) - - [Defining help in Kong](#defining-help-in-kong) + - [Help as a user of a Kong application](#help-as-a-user-of-a-kong-application) + - [Defining help in Kong](#defining-help-in-kong) - [Command handling](#command-handling) - - [Switch on the command string](#switch-on-the-command-string) - - [Attach a Run... error method to each command](#attach-a-run-error-method-to-each-command) -- [Hooks: BeforeResolve, BeforeApply, AfterApply and the Bind option](#hooks-beforeresolve-beforeapply-afterapply-and-the-bind-option) + - [Switch on the command string](#switch-on-the-command-string) + - [Attach a Run... error method to each command](#attach-a-run-error-method-to-each-command) +- [Hooks: BeforeReset, BeforeResolve, BeforeApply, AfterApply and the Bind option](#hooks-beforereset-beforeresolve-beforeapply-afterapply-and-the-bind-option) - [Flags](#flags) - [Commands and sub-commands](#commands-and-sub-commands) - [Branching positional arguments](#branching-positional-arguments) -- [Terminating positional arguments](#terminating-positional-arguments) +- [Positional arguments](#positional-arguments) - [Slices](#slices) - [Maps](#maps) +- [Pointers](#pointers) +- [Nested data structure](#nested-data-structure) - [Custom named decoders](#custom-named-decoders) - [Supported field types](#supported-field-types) - [Custom decoders mappers](#custom-decoders-mappers) - [Supported tags](#supported-tags) - [Plugins](#plugins) +- [Dynamic Commands](#dynamic-commands) - [Variable interpolation](#variable-interpolation) - [Validation](#validation) - [Modifying Kong's behaviour](#modifying-kongs-behaviour) - - [Namehelp and Descriptionhelp - set the application name description](#namehelp-and-descriptionhelp---set-the-application-name-description) - - [Configurationloader, paths... - load defaults from configuration files](#configurationloader-paths---load-defaults-from-configuration-files) - - [Resolver... - support for default values from external sources](#resolver---support-for-default-values-from-external-sources) - - [*Mapper... - customising how the command-line is mapped to Go values](#mapper---customising-how-the-command-line-is-mapped-to-go-values) - - [ConfigureHelpHelpOptions and HelpHelpFunc - customising help](#configurehelphelpoptions-and-helphelpfunc---customising-help) - - [Bind... - bind values for callback hooks and Run methods](#bind---bind-values-for-callback-hooks-and-run-methods) - - [Other options](#other-options) + - [Namehelp and Descriptionhelp - set the application name description](#namehelp-and-descriptionhelp---set-the-application-name-description) + - [Configurationloader, paths... - load defaults from configuration files](#configurationloader-paths---load-defaults-from-configuration-files) + - [Resolver... - support for default values from external sources](#resolver---support-for-default-values-from-external-sources) + - [\*Mapper... - customising how the command-line is mapped to Go values](#mapper---customising-how-the-command-line-is-mapped-to-go-values) + - [ConfigureHelpHelpOptions and HelpHelpFunc - customising help](#configurehelphelpoptions-and-helphelpfunc---customising-help) + - [Bind... - bind values for callback hooks and Run methods](#bind---bind-values-for-callback-hooks-and-run-methods) + - [Other options](#other-options) @@ -61,12 +65,12 @@ var CLI struct { Force bool `help:"Force removal."` Recursive bool `help:"Recursively remove files."` - Paths []string `arg name:"path" help:"Paths to remove." type:"path"` - } `cmd help:"Remove files."` + Paths []string `arg:"" name:"path" help:"Paths to remove." type:"path"` + } `cmd:"" help:"Remove files."` Ls struct { - Paths []string `arg optional name:"path" help:"Paths to list." type:"path"` - } `cmd help:"List paths."` + Paths []string `arg:"" optional:"" name:"path" help:"Paths to list." type:"path"` + } `cmd:"" help:"List paths."` } func main() { @@ -128,9 +132,67 @@ Help is automatically generated from the command-line structure itself, including `help:""` and other tags. [Variables](#variable-interpolation) will also be interpolated into the help string. -Finally, any command, argument, or flag type implementing the interface -`Help() string` will have this function called to retrieve the help string. -This allows for much more descriptive text than can fit in Go tags. +Finally, any command, or argument type implementing the interface +`Help() string` will have this function called to retrieve more detail to +augment the help tag. This allows for much more descriptive text than can +fit in Go tags. [See \_examples/shell/help](./_examples/shell/help) + +#### Showing the _command_'s detailed help + +A command's additional help text is _not_ shown from top-level help, but can be displayed within contextual help: + +**Top level help** + +```bash + $ go run ./_examples/shell/help --help +Usage: help + +An app demonstrating HelpProviders + +Flags: + -h, --help Show context-sensitive help. + --flag Regular flag help + +Commands: + echo Regular command help +``` + +**Contextual** + +```bash + $ go run ./_examples/shell/help echo --help +Usage: help echo + +Regular command help + +🚀 additional command help + +Arguments: + Regular argument help + +Flags: + -h, --help Show context-sensitive help. + --flag Regular flag help +``` + +#### Showing an _argument_'s detailed help + +Custom help will only be shown for _positional arguments with named fields_ ([see the README section on positional arguments for more details on what that means](../../../README.md#branching-positional-arguments)) + +**Contextual argument help** + +```bash + $ go run ./_examples/shell/help msg --help +Usage: help echo + +Regular argument help + +📣 additional argument help + +Flags: + -h, --help Show context-sensitive help. + --flag Regular flag help +``` ## Command handling @@ -140,7 +202,7 @@ There are two ways to handle commands in Kong. When you call `kong.Parse()` it will return a unique string representation of the command. Each command branch in the hierarchy will be a bare word and each branching argument or required positional argument will be the name surrounded by angle brackets. Here's an example: -There's an example of this pattern [here](https://github.com/alecthomas/kong/blob/master/_examples/shell/main.go). +There's an example of this pattern [here](https://github.com/alecthomas/kong/blob/master/_examples/shell/commandstring/main.go). eg. @@ -154,12 +216,12 @@ var CLI struct { Force bool `help:"Force removal."` Recursive bool `help:"Recursively remove files."` - Paths []string `arg name:"path" help:"Paths to remove." type:"path"` - } `cmd help:"Remove files."` + Paths []string `arg:"" name:"path" help:"Paths to remove." type:"path"` + } `cmd:"" help:"Remove files."` Ls struct { - Paths []string `arg optional name:"path" help:"Paths to list." type:"path"` - } `cmd help:"List paths."` + Paths []string `arg:"" optional:"" name:"path" help:"Paths to list." type:"path"` + } `cmd:"" help:"List paths."` } func main() { @@ -207,7 +269,7 @@ type RmCmd struct { Force bool `help:"Force removal."` Recursive bool `help:"Recursively remove files."` - Paths []string `arg name:"path" help:"Paths to remove." type:"path"` + Paths []string `arg:"" name:"path" help:"Paths to remove." type:"path"` } func (r *RmCmd) Run(ctx *Context) error { @@ -216,7 +278,7 @@ func (r *RmCmd) Run(ctx *Context) error { } type LsCmd struct { - Paths []string `arg optional name:"path" help:"Paths to list." type:"path"` + Paths []string `arg:"" optional:"" name:"path" help:"Paths to list." type:"path"` } func (l *LsCmd) Run(ctx *Context) error { @@ -227,8 +289,8 @@ func (l *LsCmd) Run(ctx *Context) error { var cli struct { Debug bool `help:"Enable debug mode."` - Rm RmCmd `cmd help:"Remove files."` - Ls LsCmd `cmd help:"List paths."` + Rm RmCmd `cmd:"" help:"Remove files."` + Ls LsCmd `cmd:"" help:"List paths."` } func main() { @@ -240,11 +302,14 @@ func main() { ``` -## Hooks: BeforeResolve(), BeforeApply(), AfterApply() and the Bind() option +## Hooks: BeforeReset(), BeforeResolve(), BeforeApply(), AfterApply() and the Bind() option -If a node in the grammar has a `BeforeResolve(...)`, `BeforeApply(...) error` and/or `AfterApply(...) error` method, those methods will be called before validation/assignment and after validation/assignment, respectively. +If a node in the grammar has a `BeforeReset(...)`, `BeforeResolve +(...)`, `BeforeApply(...) error` and/or `AfterApply(...) error` method, those +methods will be called before values are reset, before validation/assignment, +and after validation/assignment, respectively. -The `--help` flag is implemented with a `BeforeApply` hook. +The `--help` flag is implemented with a `BeforeReset` hook. Arguments to hooks are provided via the `Run(...)` method or `Bind(...)` option. `*Kong`, `*Context` and `*Path` are also bound and finally, hooks can also contribute bindings via `kong.Context.Bind()` and `kong.Context.BindTo()`. @@ -265,7 +330,7 @@ var cli struct { func main() { // Debug logger going to discard. - logger := log.New(ioutil.Discard, "", log.LstdFlags) + logger := log.New(io.Discard, "", log.LstdFlags) ctx := kong.Parse(&cli, kong.Bind(logger)) @@ -273,9 +338,43 @@ func main() { } ``` +Another example of using hooks is load the env-file: + +```go +package main + +import ( + "fmt" + "github.com/alecthomas/kong" + "github.com/joho/godotenv" +) + +type EnvFlag string + +// BeforeResolve loads env file. +func (c EnvFlag) BeforeReset(ctx *kong.Context, trace *kong.Path) error { + path := string(ctx.FlagValue(trace.Flag).(EnvFlag)) // nolint + path = kong.ExpandPath(path) + if err := godotenv.Load(path); err != nil { + return err + } + return nil +} + +var CLI struct { + EnvFile EnvFlag + Flag `env:"FLAG"` +} + +func main() { + _ = kong.Parse(&CLI) + fmt.Println(CLI.Flag) +} +``` + ## Flags -Any [mapped](#mapper---customising-how-the-command-line-is-mapped-to-go-values) field in the command structure *not* tagged with `cmd` or `arg` will be a flag. Flags are optional by default. +Any [mapped](#mapper---customising-how-the-command-line-is-mapped-to-go-values) field in the command structure _not_ tagged with `cmd` or `arg` will be a flag. Flags are optional by default. eg. The command-line `app [--flag="foo"]` can be represented by the following. @@ -302,7 +401,7 @@ type CLI struct { } ``` -If a sub-command is tagged with `default:"1"` it will be selected if there are no further arguments. +If a sub-command is tagged with `default:"1"` it will be selected if there are no further arguments. If a sub-command is tagged with `default:"withargs"` it will be selected even if there are further arguments or flags and those arguments or flags are valid for the sub-command. This allows the user to omit the sub-command name on the CLI if its arguments/flags are not ambiguous with the sibling commands or flags. ## Branching positional arguments @@ -331,11 +430,14 @@ var CLI struct { This looks a little verbose in this contrived example, but typically this will not be the case. -## Terminating positional arguments +## Positional arguments -If a [mapped type](#mapper---customising-how-the-command-line-is-mapped-to-go-values) is tagged with `arg` it will be treated as the final positional values to be parsed on the command line. +If a field is tagged with `arg:""` it will be treated as the final positional +value to be parsed on the command line. By default positional arguments are +required, but specifying `optional:""` will alter this. -If a positional argument is a slice, all remaining arguments will be appended to that slice. +If a positional argument is a slice, all remaining arguments will be appended +to that slice. ## Slices @@ -350,7 +452,7 @@ You would use the following: ```go var CLI struct { Ls struct { - Files []string `arg type:"existingfile"` + Files []string `arg:"" type:"existingfile"` } `cmd` } ``` @@ -369,7 +471,7 @@ You would use the following: var CLI struct { Config struct { Set struct { - Config map[string]float64 `arg type:"file:"` + Config map[string]float64 `arg:"" type:"file:"` } `cmd` } `cmd` } @@ -377,19 +479,48 @@ var CLI struct { For flags, multiple key+value pairs should be separated by `mapsep:"rune"` tag (defaults to `;`) eg. `--set="key1=value1;key2=value2"`. +## Pointers + +Pointers work like the underlying type, except that you can differentiate between the presence of the zero value and no value being supplied. + +For example: + +```go +var CLI struct { + Foo *int +} +``` + +Would produce a nil value for `Foo` if no `--foo` argument is supplied, but would have a pointer to the value 0 if the argument `--foo=0` was supplied. + +## Nested data structure + +Kong support a nested data structure as well with `embed:""`. You can combine `embed:""` with `prefix:""`: + +```go +var CLI struct { + Logging struct { + Level string `enum:"debug,info,warn,error" default:"info"` + Type string `enum:"json,console" default:"console"` + } `embed:"" prefix:"logging."` +} +``` + +This configures Kong to accept flags `--logging.level` and `--logging.type`. + ## Custom named decoders Kong includes a number of builtin custom type mappers. These can be used by specifying the tag `type:""`. They are registered with the option function `NamedMapper(name, mapper)`. -| Name | Description -|-------------------|--------------------------------------------------- -| `path` | A path. ~ expansion is applied. -| `existingfile` | An existing file. ~ expansion is applied. `-` is accepted for stdin. -| `existingdir` | An existing directory. ~ expansion is applied. -| `counter` | Increment a numeric field. Useful for `-vvv`. Can accept `-s`, `--long` or `--long=N`. - +| Name | Description | +| -------------- | ---------------------------------------------------------------------------------------------------------------------- | +| `path` | A path. ~ expansion is applied. `-` is accepted for stdout, and will be passed unaltered. | +| `existingfile` | An existing file. ~ expansion is applied. `-` is accepted for stdin, and will be passed unaltered. | +| `existingdir` | An existing directory. ~ expansion is applied. | +| `counter` | Increment a numeric field. Useful for `-vvv`. Can accept `-s`, `--long` or `--long=N`. | +| `filecontent` | Read the file at path into the field. ~ expansion is applied. `-` is accepted for stdin, and will be passed unaltered. | Slices and maps treat type tags specially. For slices, the `type:""` tag specifies the element type. For maps, the tag has the format @@ -397,19 +528,17 @@ specifies the element type. For maps, the tag has the format ## Supported field types - ## Custom decoders (mappers) - Any field implementing `encoding.TextUnmarshaler` or `json.Unmarshaler` will use those interfaces for decoding values. Kong also includes builtin support for many common Go types: -| Type | Description -|---------------------|-------------------------------------------- -| `time.Duration` | Populated using `time.ParseDuration()`. -| `time.Time` | Populated using `time.Parse()`. Format defaults to RFC3339 but can be overridden with the `format:"X"` tag. -| `*os.File` | Path to a file that will be opened, or `-` for `os.Stdin`. File must be closed by the user. -| `*url.URL` | Populated with `url.Parse()`. +| Type | Description | +| --------------- | ----------------------------------------------------------------------------------------------------------- | +| `time.Duration` | Populated using `time.ParseDuration()`. | +| `time.Time` | Populated using `time.Parse()`. Format defaults to RFC3339 but can be overridden with the `format:"X"` tag. | +| `*os.File` | Path to a file that will be opened, or `-` for `os.Stdin`. File must be closed by the user. | +| `*url.URL` | Populated with `url.Parse()`. | For more fine-grained control, if a field implements the [MapperValue](https://godoc.org/github.com/alecthomas/kong#MapperValue) @@ -420,36 +549,40 @@ interface it will be used to decode arguments into the field. Tags can be in two forms: 1. Standard Go syntax, eg. `kong:"required,name='foo'"`. -2. Bare tags, eg. `required name:"foo"` +2. Bare tags, eg. `required:"" name:"foo"` Both can coexist with standard Tag parsing. -Tag | Description ------------------------| ------------------------------------------- -`cmd` | If present, struct is a command. -`arg` | If present, field is an argument. -`env:"X"` | Specify envar to use for default value. -`name:"X"` | Long name, for overriding field name. -`help:"X"` | Help text. -`type:"X"` | Specify [named types](#custom-named-decoders) to use. -`placeholder:"X"` | Placeholder text. -`default:"X"` | Default value. -`default:"1"` | On a command, make it the default. -`short:"X"` | Short name, if flag. -`aliases:"X,Y"` | One or more aliases (for cmd). -`required` | If present, flag/arg is required. -`optional` | If present, flag/arg is optional. -`hidden` | If present, command or flag is hidden. -`format:"X"` | Format for parsing input, if supported. -`sep:"X"` | Separator for sequences (defaults to ","). May be `none` to disable splitting. -`mapsep:"X"` | Separator for maps (defaults to ";"). May be `none` to disable splitting. -`enum:"X,Y,..."` | Set of valid values allowed for this flag. -`group:"X"` | Logical group for a flag or command. -`xor:"X"` | Exclusive OR group for flags. Only one flag in the group can be used which is restricted within the same command. -`prefix:"X"` | Prefix for all sub-flags. -`set:"K=V"` | Set a variable for expansion by child elements. Multiples can occur. -`embed` | If present, this field's children will be embedded in the parent. Useful for composition. -`-` | Ignore the field. Useful for adding non-CLI fields to a configuration struct. +| Tag | Description | +| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `cmd:""` | If present, struct is a command. | +| `arg:""` | If present, field is an argument. Required by default. | +| `env:"X,Y,..."` | Specify envars to use for default value. The envs are resolved in the declared order. The first value found is used. | +| `name:"X"` | Long name, for overriding field name. | +| `help:"X"` | Help text. | +| `type:"X"` | Specify [named types](#custom-named-decoders) to use. | +| `placeholder:"X"` | Placeholder text. | +| `default:"X"` | Default value. | +| `default:"1"` | On a command, make it the default. | +| `default:"withargs"` | On a command, make it the default and allow args/flags from that command | +| `short:"X"` | Short name, if flag. | +| `aliases:"X,Y"` | One or more aliases (for cmd or flag). | +| `required:""` | If present, flag/arg is required. | +| `optional:""` | If present, flag/arg is optional. | +| `hidden:""` | If present, command or flag is hidden. | +| `negatable:""` | If present on a `bool` field, supports prefixing a flag with `--no-` to invert the default value | +| `format:"X"` | Format for parsing input, if supported. | +| `sep:"X"` | Separator for sequences (defaults to ","). May be `none` to disable splitting. | +| `mapsep:"X"` | Separator for maps (defaults to ";"). May be `none` to disable splitting. | +| `enum:"X,Y,..."` | Set of valid values allowed for this flag. An enum field must be `required` or have a valid `default`. | +| `group:"X"` | Logical group for a flag or command. | +| `xor:"X,Y,..."` | Exclusive OR groups for flags. Only one flag in the group can be used which is restricted within the same command. When combined with `required`, at least one of the `xor` group will be required. | +| `prefix:"X"` | Prefix for all sub-flags. | +| `envprefix:"X"` | Envar prefix for all sub-flags. | +| `set:"K=V"` | Set a variable for expansion by child elements. Multiples can occur. | +| `embed:""` | If present, this field's children will be embedded in the parent. Useful for composition. | +| `passthrough:""` | If present on a positional argument, it stops flag parsing when encountered, as if `--` was processed before. Useful for external command wrappers, like `exec`. On a command it requires that the command contains only one argument of type `[]string` which is then filled with everything following the command, unparsed. | +| `-` | Ignore the field. Useful for adding non-CLI fields to a configuration struct. e.g `` `kong:"-"` `` | ## Plugins @@ -471,6 +604,11 @@ cli.Plugins = kong.Plugins{&pluginOne, &pluginTwo} Additionally if an interface type is embedded, it can also be populated with a Kong annotated struct. +## Dynamic Commands + +While plugins give complete control over extending command-line interfaces, Kong +also supports dynamically adding commands via `kong.DynamicCommand()`. + ## Variable interpolation Kong supports limited variable interpolation into help strings, enum lists and @@ -523,10 +661,10 @@ interface: type Validatable interface { Validate() error } - ``` +``` -+If one of these nodes is in the active command-line it will be called during -+normal validation. +If one of these nodes is in the active command-line it will be called during +normal validation. ## Modifying Kong's behaviour @@ -552,7 +690,14 @@ eg. kong.Parse(&cli, kong.Configuration(kong.JSON, "/etc/myapp.json", "~/.myapp.json")) ``` -[See the tests](https://github.com/alecthomas/kong/blob/master/resolver_test.go#L103) for an example of how the JSON file is structured. +[See the tests](https://github.com/alecthomas/kong/blob/master/resolver_test.go#L206) for an example of how the JSON file is structured. + +#### List of Configuration Loaders + +- [YAML](https://github.com/alecthomas/kong-yaml) +- [HCL](https://github.com/alecthomas/kong-hcl) +- [TOML](https://github.com/alecthomas/kong-toml) +- [JSON](https://github.com/alecthomas/kong) ### `Resolver(...)` - support for default values from external sources @@ -565,13 +710,14 @@ Example resolvers can be found in [resolver.go](https://github.com/alecthomas/ko Command-line arguments are mapped to Go values via the Mapper interface: ```go -// A Mapper knows how to map command-line input to Go. +// A Mapper represents how a field is mapped from command-line values to Go. +// +// Mappers can be associated with concrete fields via pointer, reflect.Type, reflect.Kind, or via a "type" tag. +// +// Additionally, if a type implements the MapperValue interface, it will be used. type Mapper interface { - // Decode scan into target. - // - // "ctx" contains context about the value being decoded that may be useful - // to some mappers. - Decode(ctx *MapperContext, scan *Scanner, target reflect.Value) error + // Decode ctx.Value with ctx.Scanner into target. + Decode(ctx *DecodeContext, target reflect.Value) error } ``` @@ -588,7 +734,7 @@ The default help output is usually sufficient, but if not there are two solution 1. Use `ConfigureHelp(HelpOptions)` to configure how help is formatted (see [HelpOptions](https://godoc.org/github.com/alecthomas/kong#HelpOptions) for details). 2. Custom help can be wired into Kong via the `Help(HelpFunc)` option. The `HelpFunc` is passed a `Context`, which contains the parsed context for the current command-line. See the implementation of `PrintHelp` for an example. -3. Use `HelpFormatter(HelpValueFormatter)` if you want to just customize the help text that is accompanied by flags and arguments. +3. Use `ValueFormatter(HelpValueFormatter)` if you want to just customize the help text that is accompanied by flags and arguments. 4. Use `Groups([]Group)` if you want to customize group titles or add a header. ### `Bind(...)` - bind values for callback hooks and Run() methods diff --git a/vendor/github.com/alecthomas/kong/build.go b/vendor/github.com/alecthomas/kong/build.go index 0e3bc5f7..a52d90fa 100644 --- a/vendor/github.com/alecthomas/kong/build.go +++ b/vendor/github.com/alecthomas/kong/build.go @@ -12,7 +12,6 @@ import ( type Plugins []interface{} func build(k *Kong, ast interface{}) (app *Application, err error) { - defer catch(&err) v := reflect.ValueOf(ast) iv := reflect.Indirect(v) if v.Kind() != reflect.Ptr || iv.Kind() != reflect.Struct { @@ -25,7 +24,11 @@ func build(k *Kong, ast interface{}) (app *Application, err error) { for _, flag := range extraFlags { seenFlags[flag.Name] = true } - node := buildNode(k, iv, ApplicationNode, seenFlags) + + node, err := buildNode(k, iv, ApplicationNode, newEmptyTag(), seenFlags) + if err != nil { + return nil, err + } if len(node.Positional) > 0 && len(node.Children) > 0 { return nil, fmt.Errorf("can't mix positional arguments and branching arguments on %T", ast) } @@ -46,18 +49,36 @@ type flattenedField struct { tag *Tag } -func flattenedFields(v reflect.Value) (out []flattenedField) { +func flattenedFields(v reflect.Value, ptag *Tag) (out []flattenedField, err error) { v = reflect.Indirect(v) for i := 0; i < v.NumField(); i++ { ft := v.Type().Field(i) fv := v.Field(i) - tag := parseTag(fv, ft) + tag, err := parseTag(v, ft) + if err != nil { + return nil, err + } if tag.Ignored { continue } + // Assign group if it's not already set. + if tag.Group == "" { + tag.Group = ptag.Group + } + // Accumulate prefixes. + tag.Prefix = ptag.Prefix + tag.Prefix + tag.EnvPrefix = ptag.EnvPrefix + tag.EnvPrefix + // Combine parent vars. + tag.Vars = ptag.Vars.CloneWith(tag.Vars) + // Command and embedded structs can be pointers, so we hydrate them now. + if (tag.Cmd || tag.Embed) && ft.Type.Kind() == reflect.Ptr { + fv = reflect.New(ft.Type.Elem()).Elem() + v.FieldByIndex(ft.Index).Set(fv.Addr()) + } if !ft.Anonymous && !tag.Embed { if fv.CanSet() { - out = append(out, flattenedField{field: ft, value: fv, tag: tag}) + field := flattenedField{field: ft, value: fv, tag: tag} + out = append(out, field) } continue } @@ -67,77 +88,125 @@ func flattenedFields(v reflect.Value) (out []flattenedField) { fv = fv.Elem() } else if fv.Type() == reflect.TypeOf(Plugins{}) { for i := 0; i < fv.Len(); i++ { - out = append(out, flattenedFields(fv.Index(i).Elem())...) + fields, ferr := flattenedFields(fv.Index(i).Elem(), tag) + if ferr != nil { + return nil, ferr + } + out = append(out, fields...) } continue } - sub := flattenedFields(fv) - for _, subf := range sub { - // Assign parent if it's not already set. - if subf.tag.Group == "" { - subf.tag.Group = tag.Group - } - // Accumulate prefixes. - subf.tag.Prefix = tag.Prefix + subf.tag.Prefix - // Combine parent vars. - subf.tag.Vars = tag.Vars.CloneWith(subf.tag.Vars) + sub, err := flattenedFields(fv, tag) + if err != nil { + return nil, err } out = append(out, sub...) } - return out + return out, nil } -func buildNode(k *Kong, v reflect.Value, typ NodeType, seenFlags map[string]bool) *Node { +// Build a Node in the Kong data model. +// +// "v" is the value to create the node from, "typ" is the output Node type. +func buildNode(k *Kong, v reflect.Value, typ NodeType, tag *Tag, seenFlags map[string]bool) (*Node, error) { node := &Node{ Type: typ, Target: v, - Tag: newEmptyTag(), + Tag: tag, } - for _, field := range flattenedFields(v) { + fields, err := flattenedFields(v, tag) + if err != nil { + return nil, err + } + +MAIN: + for _, field := range fields { + for _, r := range k.ignoreFields { + if r.MatchString(v.Type().Name() + "." + field.field.Name) { + continue MAIN + } + } + ft := field.field fv := field.value tag := field.tag name := tag.Name if name == "" { - name = tag.Prefix + strings.ToLower(dashedString(ft.Name)) + name = tag.Prefix + k.flagNamer(ft.Name) } else { name = tag.Prefix + name } + if len(tag.Envs) != 0 { + for i := range tag.Envs { + tag.Envs[i] = tag.EnvPrefix + tag.Envs[i] + } + } + // Nested structs are either commands or args, unless they implement the Mapper interface. - if ft.Type.Kind() == reflect.Struct && (tag.Cmd || tag.Arg) && k.registry.ForValue(fv) == nil { + if field.value.Kind() == reflect.Struct && (tag.Cmd || tag.Arg) && k.registry.ForValue(fv) == nil { typ := CommandNode if tag.Arg { typ = ArgumentNode } - buildChild(k, node, typ, v, ft, fv, tag, name, seenFlags) + err = buildChild(k, node, typ, v, ft, fv, tag, name, seenFlags) } else { - buildField(k, node, v, ft, fv, tag, name, seenFlags) + err = buildField(k, node, v, ft, fv, tag, name, seenFlags) + } + if err != nil { + return nil, err } } + // Validate if there are no duplicate names + if err := checkDuplicateNames(node, v); err != nil { + return nil, err + } + // "Unsee" flags. for _, flag := range node.Flags { - delete(seenFlags, flag.Name) + delete(seenFlags, "--"+flag.Name) + if flag.Short != 0 { + delete(seenFlags, "-"+string(flag.Short)) + } } - // Scan through argument positionals to ensure optional is never before a required. - last := true - for i, p := range node.Positional { - if !last && p.Required { - fail("argument %q can not be required after an optional", p.Name) + if err := validatePositionalArguments(node); err != nil { + return nil, err + } + + return node, nil +} + +func validatePositionalArguments(node *Node) error { + var last *Value + for i, curr := range node.Positional { + if last != nil { + // Scan through argument positionals to ensure optional is never before a required. + if !last.Required && curr.Required { + return fmt.Errorf("%s: required %q cannot come after optional %q", node.FullPath(), curr.Name, last.Name) + } + + // Cumulative argument needs to be last. + if last.IsCumulative() { + return fmt.Errorf("%s: argument %q cannot come after cumulative %q", node.FullPath(), curr.Name, last.Name) + } } - last = p.Required - p.Position = i + last = curr + curr.Position = i } - return node + return nil } -func buildChild(k *Kong, node *Node, typ NodeType, v reflect.Value, ft reflect.StructField, fv reflect.Value, tag *Tag, name string, seenFlags map[string]bool) { - child := buildNode(k, fv, typ, seenFlags) +func buildChild(k *Kong, node *Node, typ NodeType, v reflect.Value, ft reflect.StructField, fv reflect.Value, tag *Tag, name string, seenFlags map[string]bool) error { + child, err := buildNode(k, fv, typ, newEmptyTag(), seenFlags) + if err != nil { + return err + } + child.Name = name child.Tag = tag child.Parent = node child.Help = tag.Help @@ -153,48 +222,67 @@ func buildChild(k *Kong, node *Node, typ NodeType, v reflect.Value, ft reflect.S // a positional argument is provided to the child, and move it to the branching argument field. if tag.Arg { if len(child.Positional) == 0 { - fail("positional branch %s.%s must have at least one child positional argument named %q", - v.Type().Name(), ft.Name, name) + return failField(v, ft, "positional branch must have at least one child positional argument named %q", name) + } + if child.Positional[0].Name != name { + return failField(v, ft, "first field in positional branch must have the same name as the parent field (%s).", child.Name) } - value := child.Positional[0] + child.Argument = child.Positional[0] child.Positional = child.Positional[1:] if child.Help == "" { - child.Help = value.Help + child.Help = child.Argument.Help } - - child.Name = value.Name - if child.Name != name { - fail("first field in positional branch %s.%s must have the same name as the parent field (%s).", - v.Type().Name(), ft.Name, child.Name) - } - - child.Argument = value } else { - child.Name = name + if tag.HasDefault { + if node.DefaultCmd != nil { + return failField(v, ft, "can't have more than one default command under %s", node.Summary()) + } + if tag.Default != "withargs" && (len(child.Children) > 0 || len(child.Positional) > 0) { + return failField(v, ft, "default command %s must not have subcommands or arguments", child.Summary()) + } + node.DefaultCmd = child + } + if tag.Passthrough { + if len(child.Children) > 0 || len(child.Flags) > 0 { + return failField(v, ft, "passthrough command %s must not have subcommands or flags", child.Summary()) + } + if len(child.Positional) != 1 { + return failField(v, ft, "passthrough command %s must contain exactly one positional argument", child.Summary()) + } + if !checkPassthroughArg(child.Positional[0].Target) { + return failField(v, ft, "passthrough command %s must contain exactly one positional argument of []string type", child.Summary()) + } + child.Passthrough = true + } } node.Children = append(node.Children, child) if len(child.Positional) > 0 && len(child.Children) > 0 { - fail("can't mix positional arguments and branching arguments on %s.%s", v.Type().Name(), ft.Name) + return failField(v, ft, "can't mix positional arguments and branching arguments") } + + return nil } -func buildField(k *Kong, node *Node, v reflect.Value, ft reflect.StructField, fv reflect.Value, tag *Tag, name string, seenFlags map[string]bool) { +func buildField(k *Kong, node *Node, v reflect.Value, ft reflect.StructField, fv reflect.Value, tag *Tag, name string, seenFlags map[string]bool) error { mapper := k.registry.ForNamedValue(tag.Type, fv) if mapper == nil { - fail("unsupported field type %s.%s (of type %s), perhaps missing a cmd:\"\" tag?", v.Type(), ft.Name, ft.Type) + return failField(v, ft, "unsupported field type %s, perhaps missing a cmd:\"\" tag?", ft.Type) } value := &Value{ Name: name, Help: tag.Help, + OrigHelp: tag.Help, + HasDefault: tag.HasDefault, Default: tag.Default, DefaultValue: reflect.New(fv.Type()).Elem(), Mapper: mapper, Tag: tag, Target: fv, Enum: tag.Enum, + Passthrough: tag.Passthrough, // Flags are optional by default, and args are required by default. Required: (!tag.Arg && tag.Required) || (tag.Arg && !tag.Optional), @@ -204,15 +292,29 @@ func buildField(k *Kong, node *Node, v reflect.Value, ft reflect.StructField, fv if tag.Arg { node.Positional = append(node.Positional, value) } else { - if seenFlags[value.Name] { - fail("duplicate flag --%s", value.Name) + if seenFlags["--"+value.Name] { + return failField(v, ft, "duplicate flag --%s", value.Name) + } + seenFlags["--"+value.Name] = true + for _, alias := range tag.Aliases { + aliasFlag := "--" + alias + if seenFlags[aliasFlag] { + return failField(v, ft, "duplicate flag %s", aliasFlag) + } + seenFlags[aliasFlag] = true + } + if tag.Short != 0 { + if seenFlags["-"+string(tag.Short)] { + return failField(v, ft, "duplicate short flag -%c", tag.Short) + } + seenFlags["-"+string(tag.Short)] = true } - seenFlags[value.Name] = true flag := &Flag{ Value: value, + Aliases: tag.Aliases, Short: tag.Short, PlaceHolder: tag.PlaceHolder, - Env: tag.Env, + Envs: tag.Envs, Group: buildGroupForKey(k, tag.Group), Xor: tag.Xor, Hidden: tag.Hidden, @@ -220,6 +322,7 @@ func buildField(k *Kong, node *Node, v reflect.Value, ft reflect.StructField, fv value.Flag = flag node.Flags = append(node.Flags, flag) } + return nil } func buildGroupForKey(k *Kong, key string) *Group { @@ -238,3 +341,20 @@ func buildGroupForKey(k *Kong, key string) *Group { Title: key, } } + +func checkDuplicateNames(node *Node, v reflect.Value) error { + seenNames := make(map[string]struct{}) + for _, node := range node.Children { + if _, ok := seenNames[node.Name]; ok { + name := v.Type().Name() + if name == "" { + name = "" + } + return fmt.Errorf("duplicate command name %q in command %q", node.Name, name) + } + + seenNames[node.Name] = struct{}{} + } + + return nil +} diff --git a/vendor/github.com/alecthomas/kong/callbacks.go b/vendor/github.com/alecthomas/kong/callbacks.go index 139a83bf..45ef0d25 100644 --- a/vendor/github.com/alecthomas/kong/callbacks.go +++ b/vendor/github.com/alecthomas/kong/callbacks.go @@ -24,6 +24,30 @@ func (b bindings) add(values ...interface{}) bindings { return b } +func (b bindings) addTo(impl, iface interface{}) { + valueOf := reflect.ValueOf(impl) + b[reflect.TypeOf(iface).Elem()] = func() (reflect.Value, error) { return valueOf, nil } +} + +func (b bindings) addProvider(provider interface{}) error { + pv := reflect.ValueOf(provider) + t := pv.Type() + if t.Kind() != reflect.Func || t.NumIn() != 0 || t.NumOut() != 2 || t.Out(1) != reflect.TypeOf((*error)(nil)).Elem() { + return fmt.Errorf("%T must be a function with the signature func()(T, error)", provider) + } + rt := pv.Type().Out(0) + b[rt] = func() (reflect.Value, error) { + out := pv.Call(nil) + errv := out[1] + var err error + if !errv.IsNil() { + err = errv.Interface().(error) //nolint + } + return out[0], err + } + return nil +} + // Clone and add values. func (b bindings) clone() bindings { out := make(bindings, len(b)) @@ -50,11 +74,14 @@ func getMethod(value reflect.Value, name string) reflect.Value { return method } -func callMethod(name string, v, f reflect.Value, bindings bindings) error { +func callFunction(f reflect.Value, bindings bindings) error { + if f.Kind() != reflect.Func { + return fmt.Errorf("expected function, got %s", f.Type()) + } in := []reflect.Value{} t := f.Type() - if t.NumOut() != 1 || t.Out(0) != callbackReturnSignature { - return fmt.Errorf("return value of %T.%s() must be exactly \"error\"", v.Type(), name) + if t.NumOut() != 1 || !t.Out(0).Implements(callbackReturnSignature) { + return fmt.Errorf("return value of %s must implement \"error\"", t) } for i := 0; i < t.NumIn(); i++ { pt := t.In(i) @@ -65,12 +92,38 @@ func callMethod(name string, v, f reflect.Value, bindings bindings) error { } in = append(in, argv) } else { - return fmt.Errorf("couldn't find binding of type %s for parameter %d of %s.%s(), use kong.Bind(%s)", pt, i, v.Type(), name, pt) + return fmt.Errorf("couldn't find binding of type %s for parameter %d of %s(), use kong.Bind(%s)", pt, i, t, pt) } } out := f.Call(in) if out[0].IsNil() { return nil } - return out[0].Interface().(error) + return out[0].Interface().(error) //nolint +} + +func callAnyFunction(f reflect.Value, bindings bindings) (out []any, err error) { + if f.Kind() != reflect.Func { + return nil, fmt.Errorf("expected function, got %s", f.Type()) + } + in := []reflect.Value{} + t := f.Type() + for i := 0; i < t.NumIn(); i++ { + pt := t.In(i) + if argf, ok := bindings[pt]; ok { + argv, err := argf() + if err != nil { + return nil, err + } + in = append(in, argv) + } else { + return nil, fmt.Errorf("couldn't find binding of type %s for parameter %d of %s(), use kong.Bind(%s)", pt, i, t, pt) + } + } + outv := f.Call(in) + out = make([]any, len(outv)) + for i, v := range outv { + out[i] = v.Interface() + } + return out, nil } diff --git a/vendor/github.com/alecthomas/kong/camelcase.go b/vendor/github.com/alecthomas/kong/camelcase.go index acf29f75..a955b16b 100644 --- a/vendor/github.com/alecthomas/kong/camelcase.go +++ b/vendor/github.com/alecthomas/kong/camelcase.go @@ -13,37 +13,37 @@ import ( // // Examples // -// "" => [""] -// "lowercase" => ["lowercase"] -// "Class" => ["Class"] -// "MyClass" => ["My", "Class"] -// "MyC" => ["My", "C"] -// "HTML" => ["HTML"] -// "PDFLoader" => ["PDF", "Loader"] -// "AString" => ["A", "String"] -// "SimpleXMLParser" => ["Simple", "XML", "Parser"] -// "vimRPCPlugin" => ["vim", "RPC", "Plugin"] -// "GL11Version" => ["GL", "11", "Version"] -// "99Bottles" => ["99", "Bottles"] -// "May5" => ["May", "5"] -// "BFG9000" => ["BFG", "9000"] -// "BöseÜberraschung" => ["Böse", "Überraschung"] -// "Two spaces" => ["Two", " ", "spaces"] -// "BadUTF8\xe2\xe2\xa1" => ["BadUTF8\xe2\xe2\xa1"] +// "" => [""] +// "lowercase" => ["lowercase"] +// "Class" => ["Class"] +// "MyClass" => ["My", "Class"] +// "MyC" => ["My", "C"] +// "HTML" => ["HTML"] +// "PDFLoader" => ["PDF", "Loader"] +// "AString" => ["A", "String"] +// "SimpleXMLParser" => ["Simple", "XML", "Parser"] +// "vimRPCPlugin" => ["vim", "RPC", "Plugin"] +// "GL11Version" => ["GL", "11", "Version"] +// "99Bottles" => ["99", "Bottles"] +// "May5" => ["May", "5"] +// "BFG9000" => ["BFG", "9000"] +// "BöseÜberraschung" => ["Böse", "Überraschung"] +// "Two spaces" => ["Two", " ", "spaces"] +// "BadUTF8\xe2\xe2\xa1" => ["BadUTF8\xe2\xe2\xa1"] // // Splitting rules // -// 1) If string is not valid UTF-8, return it without splitting as +// 1. If string is not valid UTF-8, return it without splitting as // single item array. -// 2) Assign all unicode characters into one of 4 sets: lower case +// 2. Assign all unicode characters into one of 4 sets: lower case // letters, upper case letters, numbers, and all other characters. -// 3) Iterate through characters of string, introducing splits +// 3. Iterate through characters of string, introducing splits // between adjacent characters that belong to different sets. -// 4) Iterate through array of split strings, and if a given string +// 4. Iterate through array of split strings, and if a given string // is upper case: -// if subsequent string is lower case: -// move last character of upper case string to beginning of -// lower case string +// if subsequent string is lower case: +// move last character of upper case string to beginning of +// lower case string func camelCase(src string) (entries []string) { // don't split invalid utf8 if !utf8.ValidString(src) { diff --git a/vendor/github.com/alecthomas/kong/context.go b/vendor/github.com/alecthomas/kong/context.go index 4d241613..b2bfea69 100644 --- a/vendor/github.com/alecthomas/kong/context.go +++ b/vendor/github.com/alecthomas/kong/context.go @@ -1,14 +1,13 @@ package kong import ( + "errors" "fmt" "os" "reflect" "sort" "strconv" "strings" - - "github.com/pkg/errors" ) // Path records the nodes and parsed values from the current command-line. @@ -111,10 +110,17 @@ func (c *Context) Bind(args ...interface{}) { // // This will typically have to be called like so: // -// BindTo(impl, (*MyInterface)(nil)) +// BindTo(impl, (*MyInterface)(nil)) func (c *Context) BindTo(impl, iface interface{}) { - valueOf := reflect.ValueOf(impl) - c.bindings[reflect.TypeOf(iface).Elem()] = func() (reflect.Value, error) { return valueOf, nil } + c.bindings.addTo(impl, iface) +} + +// BindToProvider allows binding of provider functions. +// +// This is useful when the Run() function of different commands require different values that may +// not all be initialisable from the main() function. +func (c *Context) BindToProvider(provider interface{}) error { + return c.bindings.addProvider(provider) } // Value returns the value for a particular path element. @@ -155,20 +161,20 @@ func (c *Context) Empty() bool { } // Validate the current context. -func (c *Context) Validate() error { // nolint: gocyclo +func (c *Context) Validate() error { //nolint: gocyclo err := Visit(c.Model, func(node Visitable, next Next) error { switch node := node.(type) { case *Value: - _, ok := os.LookupEnv(node.Tag.Env) - if node.Enum != "" && (!node.Required || node.Default != "" || (node.Tag.Env != "" && ok)) { + ok := atLeastOneEnvSet(node.Tag.Envs) + if node.Enum != "" && (!node.Required || node.HasDefault || (len(node.Tag.Envs) != 0 && ok)) { if err := checkEnum(node, node.Target); err != nil { return err } } case *Flag: - _, ok := os.LookupEnv(node.Tag.Env) - if node.Enum != "" && (!node.Required || node.Default != "" || (node.Tag.Env != "" && ok)) { + ok := atLeastOneEnvSet(node.Tag.Envs) + if node.Enum != "" && (!node.Required || node.HasDefault || (len(node.Tag.Envs) != 0 && ok)) { if err := checkEnum(node.Value, node.Target); err != nil { return err } @@ -195,16 +201,18 @@ func (c *Context) Validate() error { // nolint: gocyclo case *Application: value = node.Target - desc = node.Name + desc = "" case *Node: value = node.Target desc = node.Path() } if validate := isValidatable(value); validate != nil { - err := validate.Validate() - if err != nil { - return errors.Wrap(err, desc) + if err := validate.Validate(); err != nil { + if desc != "" { + return fmt.Errorf("%s: %w", desc, err) + } + return err } } } @@ -324,14 +332,41 @@ func (c *Context) Reset() error { }) } -func (c *Context) trace(node *Node) (err error) { // nolint: gocyclo +func (c *Context) endParsing() { + args := []string{} + for { + token := c.scan.Pop() + if token.Type == EOLToken { + break + } + args = append(args, token.String()) + } + // Note: tokens must be pushed in reverse order. + for i := range args { + c.scan.PushTyped(args[len(args)-1-i], PositionalArgumentToken) + } +} + +func (c *Context) trace(node *Node) (err error) { //nolint: gocyclo positional := 0 + node.Active = true flags := []*Flag{} - for _, group := range node.AllFlags(false) { + flagNode := node + if node.DefaultCmd != nil && node.DefaultCmd.Tag.Default == "withargs" { + // Add flags of the default command if the current node has one + // and that default command allows args / flags without explicitly + // naming the command on the CLI. + flagNode = node.DefaultCmd + } + for _, group := range flagNode.AllFlags(false) { flags = append(flags, group...) } + if node.Passthrough { + c.endParsing() + } + for !c.scan.Peek().IsEOL() { token := c.scan.Peek() switch token.Type { @@ -342,25 +377,14 @@ func (c *Context) trace(node *Node) (err error) { // nolint: gocyclo switch { case v == "-": fallthrough - default: // nolint + default: //nolint c.scan.Pop() c.scan.PushTyped(token.Value, PositionalArgumentToken) // Indicates end of parsing. All remaining arguments are treated as positional arguments only. case v == "--": c.scan.Pop() - args := []string{} - for { - token = c.scan.Pop() - if token.Type == EOLToken { - break - } - args = append(args, token.String()) - } - // Note: tokens must be pushed in reverse order. - for i := range args { - c.scan.PushTyped(args[len(args)-1-i], PositionalArgumentToken) - } + c.endParsing() // Long flag. case strings.HasPrefix(v, "--"): @@ -413,6 +437,12 @@ func (c *Context) trace(node *Node) (err error) { // nolint: gocyclo // Ensure we've consumed all positional arguments. if positional < len(node.Positional) { arg := node.Positional[positional] + + if arg.Passthrough { + c.endParsing() + } + + arg.Active = true err := arg.Parse(c.scan, c.getValue(arg)) if err != nil { return err @@ -474,6 +504,17 @@ func (c *Context) trace(node *Node) (err error) { // nolint: gocyclo } } + // If there is a default command that allows args and nothing else + // matches, take the branch of the default command + if node.DefaultCmd != nil && node.DefaultCmd.Tag.Default == "withargs" { + c.Path = append(c.Path, &Path{ + Parent: node, + Command: node.DefaultCmd, + Flags: node.DefaultCmd.Flags, + }) + return c.trace(node.DefaultCmd) + } + return findPotentialCandidates(token.String(), candidates, "unexpected argument %s", token) default: return fmt.Errorf("unexpected token %s", token) @@ -490,21 +531,12 @@ func (c *Context) maybeSelectDefault(flags []*Flag, node *Node) error { return nil } } - var defaultNode *Path - for _, child := range node.Children { - if child.Type == CommandNode && child.Tag.Default != "" { - if defaultNode != nil { - return fmt.Errorf("can't have more than one default command under %s", node.Summary()) - } - defaultNode = &Path{ - Parent: child, - Command: child, - Flags: child.Flags, - } - } - } - if defaultNode != nil { - c.Path = append(c.Path, defaultNode) + if node.DefaultCmd != nil { + c.Path = append(c.Path, &Path{ + Parent: node.DefaultCmd, + Command: node.DefaultCmd, + Flags: node.DefaultCmd.Flags, + }) } return nil } @@ -529,7 +561,7 @@ func (c *Context) Resolve() error { for _, resolver := range resolvers { s, err := resolver.Resolve(c, path, flag) if err != nil { - return errors.Wrap(err, flag.ShortSummary()) + return fmt.Errorf("%s: %w", flag.ShortSummary(), err) } if s == nil { continue @@ -553,7 +585,7 @@ func (c *Context) Resolve() error { }) } } - c.Path = append(inserted, c.Path...) + c.Path = append(c.Path, inserted...) return nil } @@ -576,6 +608,7 @@ func (c *Context) getValue(value *Value) reflect.Value { v.Set(reflect.MakeSlice(v.Type(), 0, 0)) case reflect.Map: v.Set(reflect.MakeMap(v.Type())) + default: } c.values[value] = v } @@ -633,34 +666,78 @@ func (c *Context) Apply() (string, error) { return strings.Join(path, " "), nil } +func flipBoolValue(value reflect.Value) error { + if value.Kind() == reflect.Bool { + value.SetBool(!value.Bool()) + return nil + } + + if value.Kind() == reflect.Ptr { + if !value.IsNil() { + return flipBoolValue(value.Elem()) + } + return nil + } + + return fmt.Errorf("cannot negate a value of %s", value.Type().String()) +} + func (c *Context) parseFlag(flags []*Flag, match string) (err error) { - defer catch(&err) candidates := []string{} + for _, flag := range flags { long := "--" + flag.Name - short := "-" + string(flag.Short) + matched := long == match candidates = append(candidates, long) if flag.Short != 0 { + short := "-" + string(flag.Short) + matched = matched || (short == match) candidates = append(candidates, short) } - if short != match && long != match { + for _, alias := range flag.Aliases { + alias = "--" + alias + matched = matched || (alias == match) + candidates = append(candidates, alias) + } + + neg := "--no-" + flag.Name + if !matched && !(match == neg && flag.Tag.Negatable) { continue } // Found a matching flag. c.scan.Pop() + if match == neg && flag.Tag.Negatable { + flag.Negated = true + } err := flag.Parse(c.scan, c.getValue(flag.Value)) if err != nil { - if e, ok := errors.Cause(err).(*expectedError); ok && e.token.InferredType().IsAny(FlagToken, ShortFlagToken) { - return errors.Errorf("%s; perhaps try %s=%q?", err, flag.ShortSummary(), e.token) + var expected *expectedError + if errors.As(err, &expected) && expected.token.InferredType().IsAny(FlagToken, ShortFlagToken) { + return fmt.Errorf("%s; perhaps try %s=%q?", err.Error(), flag.ShortSummary(), expected.token) } return err } + if flag.Negated { + value := c.getValue(flag.Value) + err := flipBoolValue(value) + if err != nil { + return err + } + flag.Value.Apply(value) + } c.Path = append(c.Path, &Path{Flag: flag}) return nil } return findPotentialCandidates(match, candidates, "unknown flag %s", match) } +// Call an arbitrary function filling arguments with bound values. +func (c *Context) Call(fn any, binds ...interface{}) (out []interface{}, err error) { + fv := reflect.ValueOf(fn) + bindings := c.Kong.bindings.clone().add(binds...).add(c).merge(c.bindings) //nolint:govet + return callAnyFunction(fv, bindings) +} + // RunNode calls the Run() method on an arbitrary node. // // This is useful in conjunction with Visit(), for dynamically running commands. @@ -694,7 +771,7 @@ func (c *Context) RunNode(node *Node, binds ...interface{}) (err error) { } for _, method := range methods { - if err = callMethod("Run", method.node.Target, method.method, method.binds); err != nil { + if err = callFunction(method.method, method.binds); err != nil { return err } } @@ -706,9 +783,17 @@ func (c *Context) RunNode(node *Node, binds ...interface{}) (err error) { // Any passed values will be bindable to arguments of the target Run() method. Additionally, // all parent nodes in the command structure will be bound. func (c *Context) Run(binds ...interface{}) (err error) { - defer catch(&err) node := c.Selected() if node == nil { + if len(c.Path) > 0 { + selected := c.Path[0].Node() + if selected.Type == ApplicationNode { + method := getMethod(selected.Target, "Run") + if method.IsValid() { + return c.RunNode(selected, binds...) + } + } + } return fmt.Errorf("no command selected") } return c.RunNode(node, binds...) @@ -724,17 +809,41 @@ func (c *Context) PrintUsage(summary bool) error { } func checkMissingFlags(flags []*Flag) error { + xorGroupSet := map[string]bool{} + xorGroup := map[string][]string{} missing := []string{} for _, flag := range flags { + if flag.Set { + for _, xor := range flag.Xor { + xorGroupSet[xor] = true + } + } if !flag.Required || flag.Set { continue } - missing = append(missing, flag.Summary()) + if len(flag.Xor) > 0 { + for _, xor := range flag.Xor { + if xorGroupSet[xor] { + continue + } + xorGroup[xor] = append(xorGroup[xor], flag.Summary()) + } + } else { + missing = append(missing, flag.Summary()) + } } + for xor, flags := range xorGroup { + if !xorGroupSet[xor] && len(flags) > 1 { + missing = append(missing, strings.Join(flags, " or ")) + } + } + if len(missing) == 0 { return nil } + sort.Strings(missing) + return fmt.Errorf("missing flags: %s", strings.Join(missing, ", ")) } @@ -751,7 +860,6 @@ func checkMissingChildren(node *Node) error { missing = append(missing, strconv.Quote(strings.Join(missingArgs, " "))) } - haveDefault := 0 for _, child := range node.Children { if child.Hidden { continue @@ -762,19 +870,10 @@ func checkMissingChildren(node *Node) error { } missing = append(missing, strconv.Quote(child.Summary())) } else { - if child.Tag.Default != "" { - if len(child.Children) > 0 { - return fmt.Errorf("default command %s must not have subcommands or arguments", child.Summary()) - } - haveDefault++ - } missing = append(missing, strconv.Quote(child.Name)) } } - if haveDefault > 1 { - return fmt.Errorf("more than one default command found under %s", node.Summary()) - } - if len(missing) == 0 || haveDefault > 0 { + if len(missing) == 0 { return nil } @@ -801,7 +900,17 @@ func checkMissingPositionals(positional int, values []*Value) error { missing := []string{} for ; positional < len(values); positional++ { - missing = append(missing, "<"+values[positional].Name+">") + arg := values[positional] + // TODO(aat): Fix hardcoding of these env checks all over the place :\ + if len(arg.Tag.Envs) != 0 { + if atLeastOneEnvSet(arg.Tag.Envs) { + continue + } + } + missing = append(missing, "<"+arg.Name+">") + } + if len(missing) == 0 { + return nil } return fmt.Errorf("missing positional arguments %s", strings.Join(missing, " ")) } @@ -817,23 +926,37 @@ func checkEnum(value *Value, target reflect.Value) error { return nil case reflect.Map, reflect.Struct: - return errors.Errorf("enum can only be applied to a slice or value") + return errors.New("enum can only be applied to a slice or value") - default: - enumMap := value.EnumMap() - v := fmt.Sprintf("%v", target) - if enumMap[v] { + case reflect.Ptr: + if target.IsNil() { return nil } + return checkEnum(value, target.Elem()) + default: + enumSlice := value.EnumSlice() + v := fmt.Sprintf("%v", target) enums := []string{} - for enum := range enumMap { + for _, enum := range enumSlice { + if enum == v { + return nil + } enums = append(enums, fmt.Sprintf("%q", enum)) } - sort.Strings(enums) return fmt.Errorf("%s must be one of %s but got %q", value.ShortSummary(), strings.Join(enums, ","), target.Interface()) } } +func checkPassthroughArg(target reflect.Value) bool { + typ := target.Type() + switch typ.Kind() { + case reflect.Slice: + return typ.Elem().Kind() == reflect.String + default: + return false + } +} + func checkXorDuplicates(paths []*Path) error { for _, path := range paths { seen := map[string]*Flag{} @@ -841,13 +964,12 @@ func checkXorDuplicates(paths []*Path) error { if !flag.Set { continue } - if flag.Xor == "" { - continue - } - if seen[flag.Xor] != nil { - return fmt.Errorf("--%s and --%s can't be used together", seen[flag.Xor].Name, flag.Name) + for _, xor := range flag.Xor { + if seen[xor] != nil { + return fmt.Errorf("--%s and --%s can't be used together", seen[xor].Name, flag.Name) + } + seen[xor] = flag } - seen[flag.Xor] = flag } } return nil @@ -886,3 +1008,12 @@ func isValidatable(v reflect.Value) validatable { } return nil } + +func atLeastOneEnvSet(envs []string) bool { + for _, env := range envs { + if _, ok := os.LookupEnv(env); ok { + return true + } + } + return false +} diff --git a/vendor/github.com/alecthomas/kong/doc.go b/vendor/github.com/alecthomas/kong/doc.go index 78c4d110..7e53da7f 100644 --- a/vendor/github.com/alecthomas/kong/doc.go +++ b/vendor/github.com/alecthomas/kong/doc.go @@ -2,31 +2,31 @@ // // Here's an example: // -// shell rm [-f] [-r] ... -// shell ls [ ...] +// shell rm [-f] [-r] ... +// shell ls [ ...] // // This can be represented by the following command-line structure: // -// package main +// package main // -// import "github.com/alecthomas/kong" +// import "github.com/alecthomas/kong" // -// var CLI struct { -// Rm struct { -// Force bool `short:"f" help:"Force removal."` -// Recursive bool `short:"r" help:"Recursively remove files."` +// var CLI struct { +// Rm struct { +// Force bool `short:"f" help:"Force removal."` +// Recursive bool `short:"r" help:"Recursively remove files."` // -// Paths []string `arg help:"Paths to remove." type:"path"` -// } `cmd help:"Remove files."` +// Paths []string `arg help:"Paths to remove." type:"path"` +// } `cmd help:"Remove files."` // -// Ls struct { -// Paths []string `arg optional help:"Paths to list." type:"path"` -// } `cmd help:"List paths."` -// } +// Ls struct { +// Paths []string `arg optional help:"Paths to list." type:"path"` +// } `cmd help:"List paths."` +// } // -// func main() { -// kong.Parse(&CLI) -// } +// func main() { +// kong.Parse(&CLI) +// } // // See https://github.com/alecthomas/kong for details. package kong diff --git a/vendor/github.com/alecthomas/kong/error.go b/vendor/github.com/alecthomas/kong/error.go index 30b88589..18225ef5 100644 --- a/vendor/github.com/alecthomas/kong/error.go +++ b/vendor/github.com/alecthomas/kong/error.go @@ -8,5 +8,5 @@ type ParseError struct { Context *Context } -// Cause returns the original cause of the error. -func (p *ParseError) Cause() error { return p.error } +// Unwrap returns the original cause of the error. +func (p *ParseError) Unwrap() error { return p.error } diff --git a/vendor/github.com/alecthomas/kong/guesswidth.go b/vendor/github.com/alecthomas/kong/guesswidth.go index 46768e68..dfdc3f51 100644 --- a/vendor/github.com/alecthomas/kong/guesswidth.go +++ b/vendor/github.com/alecthomas/kong/guesswidth.go @@ -1,3 +1,4 @@ +//go:build appengine || (!linux && !freebsd && !darwin && !dragonfly && !netbsd && !openbsd) // +build appengine !linux,!freebsd,!darwin,!dragonfly,!netbsd,!openbsd package kong diff --git a/vendor/github.com/alecthomas/kong/guesswidth_unix.go b/vendor/github.com/alecthomas/kong/guesswidth_unix.go index 41d54e64..0170055a 100644 --- a/vendor/github.com/alecthomas/kong/guesswidth_unix.go +++ b/vendor/github.com/alecthomas/kong/guesswidth_unix.go @@ -1,3 +1,4 @@ +//go:build (!appengine && linux) || freebsd || darwin || dragonfly || netbsd || openbsd // +build !appengine,linux freebsd darwin dragonfly netbsd openbsd package kong @@ -26,9 +27,9 @@ func guessWidth(w io.Writer) int { if _, _, err := syscall.Syscall6( syscall.SYS_IOCTL, - uintptr(fd), // nolint: unconvert + uintptr(fd), //nolint: unconvert uintptr(syscall.TIOCGWINSZ), - uintptr(unsafe.Pointer(&dimensions)), // nolint: gas + uintptr(unsafe.Pointer(&dimensions)), //nolint: gas 0, 0, 0, ); err == 0 { if dimensions[1] == 0 { diff --git a/vendor/github.com/alecthomas/kong/help.go b/vendor/github.com/alecthomas/kong/help.go index e1077240..cf5a912c 100644 --- a/vendor/github.com/alecthomas/kong/help.go +++ b/vendor/github.com/alecthomas/kong/help.go @@ -16,7 +16,7 @@ const ( // Help flag. type helpValue bool -func (h helpValue) BeforeApply(ctx *Context) error { +func (h helpValue) BeforeReset(ctx *Context) error { options := ctx.Kong.helpOptions options.Summary = false err := ctx.Kong.help(options, ctx) @@ -41,10 +41,21 @@ type HelpOptions struct { // Tree writes command chains in a tree structure instead of listing them separately. Tree bool + // Place the flags after the commands listing. + FlagsLast bool + // Indenter modulates the given prefix for the next layer in the tree view. // The following exported templates can be used: kong.SpaceIndenter, kong.LineIndenter, kong.TreeIndenter // The kong.SpaceIndenter will be used by default. Indenter HelpIndenter + + // Don't show the help associated with subcommands + NoExpandSubcommands bool + + // Clamp the help wrap width to a value smaller than the terminal width. + // If this is set to a non-positive number, the terminal width is used; otherwise, + // the min of this value or the terminal width is used. + WrapUpperBound int } // Apply options to Kong as a configuration option. @@ -59,6 +70,11 @@ type HelpProvider interface { Help() string } +// PlaceHolderProvider can be implemented by mappers to provide custom placeholder text. +type PlaceHolderProvider interface { + PlaceHolder(flag *Flag) string +} + // HelpIndenter is used to indent new layers in the help tree. type HelpIndenter func(prefix string) string @@ -70,10 +86,10 @@ type HelpValueFormatter func(value *Value) string // DefaultHelpValueFormatter is the default HelpValueFormatter. func DefaultHelpValueFormatter(value *Value) string { - if value.Tag.Env == "" { + if len(value.Tag.Envs) == 0 || HasInterpolatedVar(value.OrigHelp, "env") { return value.Help } - suffix := "($" + value.Tag.Env + ")" + suffix := "(" + formatEnvs(value.Tag.Envs) + ")" switch { case strings.HasSuffix(value.Help, "."): return value.Help[:len(value.Help)-1] + " " + suffix + "." @@ -84,6 +100,21 @@ func DefaultHelpValueFormatter(value *Value) string { } } +// DefaultShortHelpPrinter is the default HelpPrinter for short help on error. +func DefaultShortHelpPrinter(options HelpOptions, ctx *Context) error { + w := newHelpWriter(ctx, options) + cmd := ctx.Selected() + app := ctx.Model + if cmd == nil { + w.Printf("Usage: %s%s", app.Name, app.Summary()) + w.Printf(`Run "%s --help" for more information.`, app.Name) + } else { + w.Printf("Usage: %s %s", app.Name, cmd.Summary()) + w.Printf(`Run "%s --help" for more information.`, cmd.FullPath()) + } + return w.Write(ctx.Stdout) +} + // DefaultHelpPrinter is the default HelpPrinter. func DefaultHelpPrinter(options HelpOptions, ctx *Context) error { if ctx.Empty() { @@ -143,21 +174,31 @@ func printNodeDetail(w *helpWriter, node *Node, hide bool) { w.Print("Arguments:") writePositionals(w.Indent(), node.Positional) } - if flags := node.AllFlags(true); len(flags) > 0 { - groupedFlags := collectFlagGroups(flags) - for _, group := range groupedFlags { - w.Print("") - if group.Metadata.Title != "" { - w.Wrap(group.Metadata.Title) - } - if group.Metadata.Description != "" { - w.Indent().Wrap(group.Metadata.Description) + printFlags := func() { + if flags := node.AllFlags(true); len(flags) > 0 { + groupedFlags := collectFlagGroups(flags) + for _, group := range groupedFlags { w.Print("") + if group.Metadata.Title != "" { + w.Wrap(group.Metadata.Title) + } + if group.Metadata.Description != "" { + w.Indent().Wrap(group.Metadata.Description) + w.Print("") + } + writeFlags(w.Indent(), group.Flags) } - writeFlags(w.Indent(), group.Flags) } } - cmds := node.Leaves(hide) + if !w.FlagsLast { + printFlags() + } + var cmds []*Node + if w.NoExpandSubcommands { + cmds = node.Children + } else { + cmds = node.Leaves(hide) + } if len(cmds) > 0 { iw := w.Indent() if w.Tree { @@ -184,6 +225,9 @@ func printNodeDetail(w *helpWriter, node *Node, hide bool) { } } } + if w.FlagsLast { + printFlags() + } } func writeCommandList(cmds []*Node, iw *helpWriter) { @@ -328,9 +372,13 @@ type helpWriter struct { func newHelpWriter(ctx *Context, options HelpOptions) *helpWriter { lines := []string{} + wrapWidth := guessWidth(ctx.Stdout) + if options.WrapUpperBound > 0 && wrapWidth > options.WrapUpperBound { + wrapWidth = options.WrapUpperBound + } w := &helpWriter{ indent: "", - width: guessWidth(ctx.Stdout), + width: wrapWidth, lines: &lines, helpFormatter: ctx.Kong.helpFormatter, HelpOptions: options, @@ -442,16 +490,29 @@ func formatFlag(haveShort bool, flag *Flag) string { flagString := "" name := flag.Name isBool := flag.IsBool() + isCounter := flag.IsCounter() if flag.Short != 0 { - flagString += fmt.Sprintf("-%c, --%s", flag.Short, name) + if isBool && flag.Tag.Negatable { + flagString += fmt.Sprintf("-%c, --[no-]%s", flag.Short, name) + } else { + flagString += fmt.Sprintf("-%c, --%s", flag.Short, name) + } } else { - if haveShort { - flagString += fmt.Sprintf(" --%s", name) + if isBool && flag.Tag.Negatable { + if haveShort { + flagString = fmt.Sprintf(" --[no-]%s", name) + } else { + flagString = fmt.Sprintf("--[no-]%s", name) + } } else { - flagString += fmt.Sprintf("--%s", name) + if haveShort { + flagString += fmt.Sprintf(" --%s", name) + } else { + flagString += fmt.Sprintf("--%s", name) + } } } - if !isBool { + if !isBool && !isCounter { flagString += fmt.Sprintf("=%s", flag.FormatPlaceHolder()) } return flagString @@ -463,6 +524,9 @@ func (h *HelpOptions) CommandTree(node *Node, prefix string) (rows [][2]string) switch node.Type { default: nodeName += prefix + node.Name + if len(node.Aliases) != 0 { + nodeName += fmt.Sprintf(" (%s)", strings.Join(node.Aliases, ",")) + } case ArgumentNode: nodeName += prefix + "<" + node.Name + ">" } @@ -476,6 +540,9 @@ func (h *HelpOptions) CommandTree(node *Node, prefix string) (rows [][2]string) rows = append(rows, [2]string{prefix + arg.Summary(), arg.Help}) } for _, subCmd := range node.Children { + if subCmd.Hidden { + continue + } rows = append(rows, h.CommandTree(subCmd, prefix)...) } return @@ -501,3 +568,12 @@ func TreeIndenter(prefix string) string { } return "|" + strings.Repeat(" ", defaultIndent) + prefix } + +func formatEnvs(envs []string) string { + formatted := make([]string, len(envs)) + for i := range envs { + formatted[i] = "$" + envs[i] + } + + return strings.Join(formatted, ", ") +} diff --git a/vendor/github.com/alecthomas/kong/interpolate.go b/vendor/github.com/alecthomas/kong/interpolate.go index 8c6f7424..e811632d 100644 --- a/vendor/github.com/alecthomas/kong/interpolate.go +++ b/vendor/github.com/alecthomas/kong/interpolate.go @@ -5,7 +5,18 @@ import ( "regexp" ) -var interpolationRegex = regexp.MustCompile(`((?:\${([[:alpha:]_][[:word:]]*))(?:=([^}]+))?})|(\$)|([^$]+)`) +var interpolationRegex = regexp.MustCompile(`(\$\$)|((?:\${([[:alpha:]_][[:word:]]*))(?:=([^}]+))?})|(\$)|([^$]+)`) + +// HasInterpolatedVar returns true if the variable "v" is interpolated in "s". +func HasInterpolatedVar(s string, v string) bool { + matches := interpolationRegex.FindAllStringSubmatch(s, -1) + for _, match := range matches { + if name := match[3]; name == v { + return true + } + } + return false +} // Interpolate variables from vars into s for substrings in the form ${var} or ${var=default}. func interpolate(s string, vars Vars, updatedVars map[string]string) (string, error) { @@ -21,14 +32,16 @@ func interpolate(s string, vars Vars, updatedVars map[string]string) (string, er } } for _, match := range matches { - if name := match[2]; name != "" { + if dollar := match[1]; dollar != "" { + out += "$" + } else if name := match[3]; name != "" { value, ok := vars[name] if !ok { // No default value. - if match[3] == "" { + if match[4] == "" { return "", fmt.Errorf("undefined variable ${%s}", name) } - value = match[3] + value = match[4] } out += value } else { diff --git a/vendor/github.com/alecthomas/kong/kong.go b/vendor/github.com/alecthomas/kong/kong.go index 9b67db93..76eaefe7 100644 --- a/vendor/github.com/alecthomas/kong/kong.go +++ b/vendor/github.com/alecthomas/kong/kong.go @@ -1,11 +1,13 @@ package kong import ( + "errors" "fmt" "io" "os" "path/filepath" "reflect" + "regexp" "strings" ) @@ -13,13 +15,12 @@ var ( callbackReturnSignature = reflect.TypeOf((*error)(nil)).Elem() ) -// Error reported by Kong. -type Error struct{ msg string } - -func (e Error) Error() string { return e.msg } - -func fail(format string, args ...interface{}) { - panic(Error{msg: fmt.Sprintf(format, args...)}) +func failField(parent reflect.Value, field reflect.StructField, format string, args ...interface{}) error { + name := parent.Type().Name() + if name == "" { + name = "" + } + return fmt.Errorf("%s.%s: %s", name, field.Name, fmt.Sprintf(format, args...)) } // Must creates a new Parser or panics if there is an error. @@ -31,6 +32,13 @@ func Must(ast interface{}, options ...Option) *Kong { return k } +type usageOnError int + +const ( + shortUsage usageOnError = iota + 1 + fullUsage +) + // Kong is the main parser type. type Kong struct { // Grammar model. @@ -42,22 +50,27 @@ type Kong struct { Stdout io.Writer Stderr io.Writer - bindings bindings - loader ConfigurationLoader - resolvers []Resolver - registry *Registry + bindings bindings + loader ConfigurationLoader + resolvers []Resolver + registry *Registry + ignoreFields []*regexp.Regexp noDefaultHelp bool - usageOnError bool + usageOnError usageOnError help HelpPrinter + shortHelp HelpPrinter helpFormatter HelpValueFormatter helpOptions HelpOptions helpFlag *Flag groups []Group vars Vars + flagNamer func(string) string // Set temporarily by Options. These are applied after build(). postBuildOptions []Option + embedded []embedded + dynamicCommands []*dynamicCommand } // New creates a new Kong parser on grammar. @@ -72,6 +85,10 @@ func New(grammar interface{}, options ...Option) (*Kong, error) { vars: Vars{}, bindings: bindings{}, helpFormatter: DefaultHelpValueFormatter, + ignoreFields: make([]*regexp.Regexp, 0), + flagNamer: func(s string) string { + return strings.ToLower(dashedString(s)) + }, } options = append(options, Bind(k)) @@ -86,6 +103,10 @@ func New(grammar interface{}, options ...Option) (*Kong, error) { k.help = DefaultHelpPrinter } + if k.shortHelp == nil { + k.shortHelp = DefaultShortHelpPrinter + } + model, err := build(k, grammar) if err != nil { return k, err @@ -94,6 +115,45 @@ func New(grammar interface{}, options ...Option) (*Kong, error) { k.Model = model k.Model.HelpFlag = k.helpFlag + // Embed any embedded structs. + for _, embed := range k.embedded { + tag, err := parseTagString(strings.Join(embed.tags, " ")) //nolint:govet + if err != nil { + return nil, err + } + tag.Embed = true + v := reflect.Indirect(reflect.ValueOf(embed.strct)) + node, err := buildNode(k, v, CommandNode, tag, map[string]bool{}) + if err != nil { + return nil, err + } + for _, child := range node.Children { + child.Parent = k.Model.Node + k.Model.Children = append(k.Model.Children, child) + } + k.Model.Flags = append(k.Model.Flags, node.Flags...) + } + + // Synthesise command nodes. + for _, dcmd := range k.dynamicCommands { + tag, terr := parseTagString(strings.Join(dcmd.tags, " ")) + if terr != nil { + return nil, terr + } + tag.Name = dcmd.name + tag.Help = dcmd.help + tag.Group = dcmd.group + tag.Cmd = true + v := reflect.Indirect(reflect.ValueOf(dcmd.cmd)) + err = buildChild(k, k.Model.Node, CommandNode, reflect.Value{}, reflect.StructField{ + Name: dcmd.name, + Type: v.Type(), + }, v, tag, dcmd.name, map[string]bool{}) + if err != nil { + return nil, err + } + } + for _, option := range k.postBuildOptions { if err = option.Apply(k); err != nil { return nil, err @@ -148,16 +208,37 @@ func (k *Kong) interpolateValue(value *Value, vars Vars) (err error) { if len(value.Tag.Vars) > 0 { vars = vars.CloneWith(value.Tag.Vars) } + if varsContributor, ok := value.Mapper.(VarsContributor); ok { + vars = vars.CloneWith(varsContributor.Vars(value)) + } + + if value.Enum, err = interpolate(value.Enum, vars, nil); err != nil { + return fmt.Errorf("enum for %s: %s", value.Summary(), err) + } + + updatedVars := map[string]string{ + "default": value.Default, + "enum": value.Enum, + } if value.Default, err = interpolate(value.Default, vars, nil); err != nil { return fmt.Errorf("default value for %s: %s", value.Summary(), err) } if value.Enum, err = interpolate(value.Enum, vars, nil); err != nil { return fmt.Errorf("enum value for %s: %s", value.Summary(), err) } - value.Help, err = interpolate(value.Help, vars, map[string]string{ - "default": value.Default, - "enum": value.Enum, - }) + if value.Flag != nil { + for i, env := range value.Flag.Envs { + if value.Flag.Envs[i], err = interpolate(env, vars, nil); err != nil { + return fmt.Errorf("env value for %s: %s", value.Summary(), err) + } + } + value.Tag.Envs = value.Flag.Envs + updatedVars["env"] = "" + if len(value.Flag.Envs) != 0 { + updatedVars["env"] = value.Flag.Envs[0] + } + } + value.Help, err = interpolate(value.Help, vars, updatedVars) if err != nil { return fmt.Errorf("help for %s: %s", value.Summary(), err) } @@ -176,6 +257,7 @@ func (k *Kong) extraFlags() []*Flag { Value: &Value{ Name: "help", Help: "Show context-sensitive help.", + OrigHelp: "Show context-sensitive help.", Target: value, Tag: &Tag{}, Mapper: k.registry.ForValue(value), @@ -195,7 +277,6 @@ func (k *Kong) extraFlags() []*Flag { // Will return a ParseError if a *semantically* invalid command-line is encountered (as opposed to a syntactically // invalid one, which will report a normal error). func (k *Kong) Parse(args []string) (ctx *Context, err error) { - defer catch(&err) ctx, err = Trace(k, args) if err != nil { return nil, err @@ -203,6 +284,9 @@ func (k *Kong) Parse(args []string) (ctx *Context, err error) { if ctx.Error != nil { return nil, &ParseError{error: ctx.Error, Context: ctx} } + if err = k.applyHook(ctx, "BeforeReset"); err != nil { + return nil, &ParseError{error: err, Context: ctx} + } if err = ctx.Reset(); err != nil { return nil, &ParseError{error: err, Context: ctx} } @@ -252,7 +336,7 @@ func (k *Kong) applyHook(ctx *Context, name string) error { binds.add(ctx, trace) binds.add(trace.Node().Vars().CloneWith(k.vars)) binds.merge(ctx.bindings) - if err := callMethod(name, value, method, binds); err != nil { + if err := callFunction(method, binds); err != nil { return err } } @@ -272,7 +356,7 @@ func (k *Kong) applyHookToDefaultFlags(ctx *Context, node *Node, name string) er } binds := k.bindings.clone().add(ctx).add(node.Vars().CloneWith(k.vars)) for _, flag := range node.Flags { - if flag.Default == "" || ctx.values[flag.Value].IsValid() || !flag.Target.IsValid() { + if !flag.HasDefault || ctx.values[flag.Value].IsValid() || !flag.Target.IsValid() { continue } method := getMethod(flag.Target, name) @@ -280,7 +364,7 @@ func (k *Kong) applyHookToDefaultFlags(ctx *Context, node *Node, name string) er continue } path := &Path{Flag: flag} - if err := callMethod(name, flag.Target, method, binds.clone().add(path)); err != nil { + if err := callFunction(method, binds.clone().add(path)); err != nil { return next(err) } } @@ -328,13 +412,19 @@ func (k *Kong) FatalIfErrorf(err error, args ...interface{}) { } msg := err.Error() if len(args) > 0 { - msg = fmt.Sprintf(args[0].(string), args[1:]...) + ": " + err.Error() + msg = fmt.Sprintf(args[0].(string), args[1:]...) + ": " + err.Error() //nolint } // Maybe display usage information. - if err, ok := err.(*ParseError); ok && k.usageOnError { - options := k.helpOptions - _ = k.help(options, err.Context) - fmt.Fprintln(k.Stdout) + var parseErr *ParseError + if errors.As(err, &parseErr) { + switch k.usageOnError { + case fullUsage: + _ = k.help(k.helpOptions, parseErr.Context) + fmt.Fprintln(k.Stdout) + case shortUsage: + _ = k.shortHelp(k.helpOptions, parseErr.Context) + fmt.Fprintln(k.Stdout) + } } k.Fatalf("%s", msg) } @@ -349,7 +439,7 @@ func (k *Kong) LoadConfig(path string) (Resolver, error) { if err != nil { return nil, err } - r, err := os.Open(path) // nolint: gas + r, err := os.Open(path) //nolint: gas if err != nil { return nil, err } @@ -357,12 +447,3 @@ func (k *Kong) LoadConfig(path string) (Resolver, error) { return k.loader(r) } - -func catch(err *error) { - msg := recover() - if test, ok := msg.(Error); ok { - *err = test - } else if msg != nil { - panic(msg) - } -} diff --git a/vendor/github.com/alecthomas/kong/mapper.go b/vendor/github.com/alecthomas/kong/mapper.go index 183ec09b..584bb006 100644 --- a/vendor/github.com/alecthomas/kong/mapper.go +++ b/vendor/github.com/alecthomas/kong/mapper.go @@ -3,8 +3,9 @@ package kong import ( "encoding" "encoding/json" + "errors" "fmt" - "io/ioutil" + "io" "math/bits" "net/url" "os" @@ -12,13 +13,11 @@ import ( "strconv" "strings" "time" - - "github.com/pkg/errors" ) var ( mapperValueType = reflect.TypeOf((*MapperValue)(nil)).Elem() - boolMapperType = reflect.TypeOf((*BoolMapper)(nil)).Elem() + boolMapperValueType = reflect.TypeOf((*BoolMapperValue)(nil)).Elem() jsonUnmarshalerType = reflect.TypeOf((*json.Unmarshaler)(nil)).Elem() textUnmarshalerType = reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem() binaryUnmarshalerType = reflect.TypeOf((*encoding.BinaryUnmarshaler)(nil)).Elem() @@ -43,19 +42,26 @@ func (r *DecodeContext) WithScanner(scan *Scanner) *DecodeContext { } // MapperValue may be implemented by fields in order to provide custom mapping. +// Mappers may additionally implement PlaceHolderProvider to provide custom placeholder text. type MapperValue interface { Decode(ctx *DecodeContext) error } +// BoolMapperValue may be implemented by fields in order to provide custom mappings for boolean values. +type BoolMapperValue interface { + MapperValue + IsBool() bool +} + type mapperValueAdapter struct { isBool bool } func (m *mapperValueAdapter) Decode(ctx *DecodeContext, target reflect.Value) error { if target.Type().Implements(mapperValueType) { - return target.Interface().(MapperValue).Decode(ctx) + return target.Interface().(MapperValue).Decode(ctx) //nolint } - return target.Addr().Interface().(MapperValue).Decode(ctx) + return target.Addr().Interface().(MapperValue).Decode(ctx) //nolint } func (m *mapperValueAdapter) IsBool() bool { @@ -71,9 +77,9 @@ func (m *textUnmarshalerAdapter) Decode(ctx *DecodeContext, target reflect.Value return err } if target.Type().Implements(textUnmarshalerType) { - return target.Interface().(encoding.TextUnmarshaler).UnmarshalText([]byte(value)) + return target.Interface().(encoding.TextUnmarshaler).UnmarshalText([]byte(value)) //nolint } - return target.Addr().Interface().(encoding.TextUnmarshaler).UnmarshalText([]byte(value)) + return target.Addr().Interface().(encoding.TextUnmarshaler).UnmarshalText([]byte(value)) //nolint } type binaryUnmarshalerAdapter struct{} @@ -84,10 +90,10 @@ func (m *binaryUnmarshalerAdapter) Decode(ctx *DecodeContext, target reflect.Val if err != nil { return err } - if target.Type().Implements(textUnmarshalerType) { - return target.Interface().(encoding.BinaryUnmarshaler).UnmarshalBinary([]byte(value)) + if target.Type().Implements(binaryUnmarshalerType) { + return target.Interface().(encoding.BinaryUnmarshaler).UnmarshalBinary([]byte(value)) //nolint } - return target.Addr().Interface().(encoding.BinaryUnmarshaler).UnmarshalBinary([]byte(value)) + return target.Addr().Interface().(encoding.BinaryUnmarshaler).UnmarshalBinary([]byte(value)) //nolint } type jsonUnmarshalerAdapter struct{} @@ -99,9 +105,9 @@ func (j *jsonUnmarshalerAdapter) Decode(ctx *DecodeContext, target reflect.Value return err } if target.Type().Implements(jsonUnmarshalerType) { - return target.Interface().(json.Unmarshaler).UnmarshalJSON([]byte(value)) + return target.Interface().(json.Unmarshaler).UnmarshalJSON([]byte(value)) //nolint } - return target.Addr().Interface().(json.Unmarshaler).UnmarshalJSON([]byte(value)) + return target.Addr().Interface().(json.Unmarshaler).UnmarshalJSON([]byte(value)) //nolint } // A Mapper represents how a field is mapped from command-line values to Go. @@ -114,17 +120,29 @@ type Mapper interface { Decode(ctx *DecodeContext, target reflect.Value) error } +// VarsContributor can be implemented by a Mapper to contribute Vars during interpolation. +type VarsContributor interface { + Vars(ctx *Value) Vars +} + // A BoolMapper is a Mapper to a value that is a boolean. // // This is used solely for formatting help. type BoolMapper interface { + Mapper IsBool() bool } +// BoolMapperExt allows a Mapper to dynamically determine if a value is a boolean. +type BoolMapperExt interface { + Mapper + IsBoolFromValue(v reflect.Value) bool +} + // A MapperFunc is a single function that complies with the Mapper interface. type MapperFunc func(ctx *DecodeContext, target reflect.Value) error -func (m MapperFunc) Decode(ctx *DecodeContext, target reflect.Value) error { // nolint: golint +func (m MapperFunc) Decode(ctx *DecodeContext, target reflect.Value) error { //nolint: revive return m(ctx, target) } @@ -181,7 +199,8 @@ func (r *Registry) ForType(typ reflect.Type) Mapper { // Check if the type implements MapperValue. for _, impl := range []reflect.Type{typ, reflect.PtrTo(typ)} { if impl.Implements(mapperValueType) { - return &mapperValueAdapter{impl.Implements(boolMapperType)} + // FIXME: This should pass in the bool mapper. + return &mapperValueAdapter{impl.Implements(boolMapperValueType)} } } // Next, try explicitly registered types. @@ -218,8 +237,8 @@ func (r *Registry) RegisterKind(kind reflect.Kind, mapper Mapper) *Registry { // // eg. // -// Mapper string `kong:"type='colour'` -// registry.RegisterName("colour", ...) +// Mapper string `kong:"type='colour'` +// registry.RegisterName("colour", ...) func (r *Registry) RegisterName(name string, mapper Mapper) *Registry { r.names[name] = mapper return r @@ -257,8 +276,7 @@ func (r *Registry) RegisterDefaults() *Registry { RegisterKind(reflect.Float32, floatDecoder(32)). RegisterKind(reflect.Float64, floatDecoder(64)). RegisterKind(reflect.String, MapperFunc(func(ctx *DecodeContext, target reflect.Value) error { - err := ctx.Scan.PopValueInto("string", target.Addr().Interface()) - return err + return ctx.Scan.PopValueInto("string", target.Addr().Interface()) })). RegisterKind(reflect.Bool, boolMapper{}). RegisterKind(reflect.Slice, sliceDecoder(r)). @@ -270,7 +288,9 @@ func (r *Registry) RegisterDefaults() *Registry { RegisterName("path", pathMapper(r)). RegisterName("existingfile", existingFileMapper(r)). RegisterName("existingdir", existingDirMapper(r)). - RegisterName("counter", counterMapper()) + RegisterName("counter", counterMapper()). + RegisterName("filecontent", fileContentMapper(r)). + RegisterKind(reflect.Ptr, ptrMapper{r}) } type boolMapper struct{} @@ -289,14 +309,14 @@ func (boolMapper) Decode(ctx *DecodeContext, target reflect.Value) error { target.SetBool(false) default: - return errors.Errorf("bool value must be true, 1, yes, false, 0 or no but got %q", v) + return fmt.Errorf("bool value must be true, 1, yes, false, 0 or no but got %q", v) } case bool: target.SetBool(v) default: - return errors.Errorf("expected bool but got %q (%T)", token.Value, token.Value) + return fmt.Errorf("expected bool but got %q (%T)", token.Value, token.Value) } } else { target.SetBool(true) @@ -307,15 +327,23 @@ func (boolMapper) IsBool() bool { return true } func durationDecoder() MapperFunc { return func(ctx *DecodeContext, target reflect.Value) error { - var value string - if err := ctx.Scan.PopValueInto("duration", &value); err != nil { + t, err := ctx.Scan.PopValue("duration") + if err != nil { return err } - r, err := time.ParseDuration(value) - if err != nil { - return errors.Errorf("expected duration but got %q: %s", value, err) + var d time.Duration + switch v := t.Value.(type) { + case string: + d, err = time.ParseDuration(v) + if err != nil { + return fmt.Errorf("expected duration but got %q: %v", v, err) + } + case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64: + d = reflect.ValueOf(v).Convert(reflect.TypeOf(time.Duration(0))).Interface().(time.Duration) //nolint: forcetypeassert + default: + return fmt.Errorf("expected duration but got %q", v) } - target.Set(reflect.ValueOf(r)) + target.Set(reflect.ValueOf(d)) return nil } } @@ -339,7 +367,7 @@ func timeDecoder() MapperFunc { } } -func intDecoder(bits int) MapperFunc { // nolint: dupl +func intDecoder(bits int) MapperFunc { //nolint: dupl return func(ctx *DecodeContext, target reflect.Value) error { t, err := ctx.Scan.PopValue("int") if err != nil { @@ -350,22 +378,25 @@ func intDecoder(bits int) MapperFunc { // nolint: dupl case string: sv = v - case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64: + case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: sv = fmt.Sprintf("%v", v) + case float32, float64: + sv = fmt.Sprintf("%0.f", v) + default: - return errors.Errorf("expected an int but got %q (%T)", t, t.Value) + return fmt.Errorf("expected an int but got %q (%T)", t, t.Value) } n, err := strconv.ParseInt(sv, 10, bits) if err != nil { - return errors.Errorf("expected a valid %d bit int but got %q", bits, sv) + return fmt.Errorf("expected a valid %d bit int but got %q", bits, sv) } target.SetInt(n) return nil } } -func uintDecoder(bits int) MapperFunc { // nolint: dupl +func uintDecoder(bits int) MapperFunc { //nolint: dupl return func(ctx *DecodeContext, target reflect.Value) error { t, err := ctx.Scan.PopValue("uint") if err != nil { @@ -376,15 +407,18 @@ func uintDecoder(bits int) MapperFunc { // nolint: dupl case string: sv = v - case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64: + case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: sv = fmt.Sprintf("%v", v) + case float32, float64: + sv = fmt.Sprintf("%0.f", v) + default: - return errors.Errorf("expected an int but got %q (%T)", t, t.Value) + return fmt.Errorf("expected an int but got %q (%T)", t, t.Value) } n, err := strconv.ParseUint(sv, 10, bits) if err != nil { - return errors.Errorf("expected a valid %d bit uint but got %q", bits, sv) + return fmt.Errorf("expected a valid %d bit uint but got %q", bits, sv) } target.SetUint(n) return nil @@ -401,7 +435,7 @@ func floatDecoder(bits int) MapperFunc { case string: n, err := strconv.ParseFloat(v, bits) if err != nil { - return errors.Errorf("expected a float but got %q (%T)", t, t.Value) + return fmt.Errorf("expected a float but got %q (%T)", t, t.Value) } target.SetFloat(n) @@ -415,7 +449,7 @@ func floatDecoder(bits int) MapperFunc { target.Set(reflect.ValueOf(v)) default: - return errors.Errorf("expected an int but got %q (%T)", t, t.Value) + return fmt.Errorf("expected an int but got %q (%T)", t, t.Value) } return nil } @@ -427,23 +461,23 @@ func mapDecoder(r *Registry) MapperFunc { target.Set(reflect.MakeMap(target.Type())) } el := target.Type() - sep := ctx.Value.Tag.MapSep + mapsep := ctx.Value.Tag.MapSep var childScanner *Scanner if ctx.Value.Flag != nil { t := ctx.Scan.Pop() - // If decoding a flag, we need an argument. + // If decoding a flag, we need an value. if t.IsEOL() { - return errors.Errorf("unexpected EOL") + return fmt.Errorf("missing value, expecting \"=%c...\"", mapsep) } switch v := t.Value.(type) { case string: - childScanner = Scan(SplitEscaped(v, sep)...) + childScanner = ScanAsType(t.Type, SplitEscaped(v, mapsep)...) case []map[string]interface{}: for _, m := range v { err := jsonTranscode(m, target.Addr().Interface()) if err != nil { - return errors.WithStack(err) + return err } } return nil @@ -452,7 +486,7 @@ func mapDecoder(r *Registry) MapperFunc { return jsonTranscode(v, target.Addr().Interface()) default: - return errors.Errorf("invalid map value %q (of type %T)", t, t.Value) + return fmt.Errorf("invalid map value %q (of type %T)", t, t.Value) } } else { tokens := ctx.Scan.PopWhile(func(t Token) bool { return t.IsValue() }) @@ -466,7 +500,7 @@ func mapDecoder(r *Registry) MapperFunc { } parts := strings.SplitN(token, "=", 2) if len(parts) != 2 { - return errors.Errorf("expected \"=\" but got %q", token) + return fmt.Errorf("expected \"=\" but got %q", token) } key, value := parts[0], parts[1] @@ -474,23 +508,23 @@ func mapDecoder(r *Registry) MapperFunc { if typ := ctx.Value.Tag.Type; typ != "" { parts := strings.Split(typ, ":") if len(parts) != 2 { - return errors.Errorf("type:\"\" on map field must be in the form \"[]:[]\"") + return errors.New("type:\"\" on map field must be in the form \"[]:[]\"") } keyTypeName, valueTypeName = parts[0], parts[1] } - keyScanner := Scan(key) + keyScanner := ScanAsType(FlagValueToken, key) keyDecoder := r.ForNamedType(keyTypeName, el.Key()) keyValue := reflect.New(el.Key()).Elem() if err := keyDecoder.Decode(ctx.WithScanner(keyScanner), keyValue); err != nil { - return errors.Errorf("invalid map key %q", key) + return fmt.Errorf("invalid map key %q", key) } - valueScanner := Scan(value) + valueScanner := ScanAsType(FlagValueToken, value) valueDecoder := r.ForNamedType(valueTypeName, el.Elem()) valueValue := reflect.New(el.Elem()).Elem() if err := valueDecoder.Decode(ctx.WithScanner(valueScanner), valueValue); err != nil { - return errors.Errorf("invalid map value %q", value) + return fmt.Errorf("invalid map value %q", value) } target.SetMapIndex(keyValue, valueValue) @@ -506,13 +540,13 @@ func sliceDecoder(r *Registry) MapperFunc { var childScanner *Scanner if ctx.Value.Flag != nil { t := ctx.Scan.Pop() - // If decoding a flag, we need an argument. + // If decoding a flag, we need a value. if t.IsEOL() { - return errors.Errorf("unexpected EOL") + return fmt.Errorf("missing value, expecting \"%c...\"", sep) } switch v := t.Value.(type) { case string: - childScanner = Scan(SplitEscaped(v, sep)...) + childScanner = ScanAsType(t.Type, SplitEscaped(v, sep)...) case []interface{}: return jsonTranscode(v, target.Addr().Interface()) @@ -527,13 +561,13 @@ func sliceDecoder(r *Registry) MapperFunc { } childDecoder := r.ForNamedType(ctx.Value.Tag.Type, el) if childDecoder == nil { - return errors.Errorf("no mapper for element type of %s", target.Type()) + return fmt.Errorf("no mapper for element type of %s", target.Type()) } for !childScanner.Peek().IsEOL() { childValue := reflect.New(el).Elem() err := childDecoder.Decode(ctx.WithScanner(childScanner), childValue) if err != nil { - return errors.WithStack(err) + return err } target.Set(reflect.Append(target, childValue)) } @@ -546,15 +580,23 @@ func pathMapper(r *Registry) MapperFunc { if target.Kind() == reflect.Slice { return sliceDecoder(r)(ctx, target) } + if target.Kind() == reflect.Ptr && target.Elem().Kind() == reflect.String { + if target.IsNil() { + return nil + } + target = target.Elem() + } if target.Kind() != reflect.String { - return errors.Errorf("\"path\" type must be applied to a string not %s", target.Type()) + return fmt.Errorf("\"path\" type must be applied to a string not %s", target.Type()) } var path string err := ctx.Scan.PopValueInto("file", &path) if err != nil { return err } - path = ExpandPath(path) + if path != "-" { + path = ExpandPath(path) + } target.SetString(path) return nil } @@ -575,7 +617,7 @@ func fileMapper(r *Registry) MapperFunc { file = os.Stdin } else { path = ExpandPath(path) - file, err = os.Open(path) // nolint: gosec + file, err = os.Open(path) //nolint: gosec if err != nil { return err } @@ -591,13 +633,21 @@ func existingFileMapper(r *Registry) MapperFunc { return sliceDecoder(r)(ctx, target) } if target.Kind() != reflect.String { - return errors.Errorf("\"existingfile\" type must be applied to a string not %s", target.Type()) + return fmt.Errorf("\"existingfile\" type must be applied to a string not %s", target.Type()) } var path string err := ctx.Scan.PopValueInto("file", &path) if err != nil { return err } + + if !ctx.Value.Active || (ctx.Value.Set && ctx.Value.Target.Type() == target.Type()) { + // early return to avoid checking extra files that may not exist; + // this hack only works because the value provided on the cli is + // checked before the default value is checked (if default is set). + return nil + } + if path != "-" { path = ExpandPath(path) stat, err := os.Stat(path) @@ -605,7 +655,7 @@ func existingFileMapper(r *Registry) MapperFunc { return err } if stat.IsDir() { - return errors.Errorf("%q exists but is a directory", path) + return fmt.Errorf("%q exists but is a directory", path) } } target.SetString(path) @@ -619,26 +669,106 @@ func existingDirMapper(r *Registry) MapperFunc { return sliceDecoder(r)(ctx, target) } if target.Kind() != reflect.String { - return errors.Errorf("\"existingdir\" must be applied to a string not %s", target.Type()) + return fmt.Errorf("\"existingdir\" must be applied to a string not %s", target.Type()) } var path string err := ctx.Scan.PopValueInto("file", &path) if err != nil { return err } + + if !ctx.Value.Active || (ctx.Value.Set && ctx.Value.Target.Type() == target.Type()) { + // early return to avoid checking extra dirs that may not exist; + // this hack only works because the value provided on the cli is + // checked before the default value is checked (if default is set). + return nil + } + path = ExpandPath(path) stat, err := os.Stat(path) if err != nil { return err } if !stat.IsDir() { - return errors.Errorf("%q exists but is not a directory", path) + return fmt.Errorf("%q exists but is not a directory", path) } target.SetString(path) return nil } } +func fileContentMapper(r *Registry) MapperFunc { + return func(ctx *DecodeContext, target reflect.Value) error { + if target.Kind() != reflect.Slice && target.Elem().Kind() != reflect.Uint8 { + return fmt.Errorf("\"filecontent\" must be applied to []byte not %s", target.Type()) + } + var path string + err := ctx.Scan.PopValueInto("file", &path) + if err != nil { + return err + } + + if !ctx.Value.Active || ctx.Value.Set { + // early return to avoid checking extra dirs that may not exist; + // this hack only works because the value provided on the cli is + // checked before the default value is checked (if default is set). + return nil + } + + var data []byte + if path != "-" { + path = ExpandPath(path) + data, err = os.ReadFile(path) //nolint:gosec + } else { + data, err = io.ReadAll(os.Stdin) + } + if err != nil { + if info, statErr := os.Stat(path); statErr == nil && info.IsDir() { + return fmt.Errorf("%q exists but is a directory: %w", path, err) + } + return err + } + target.SetBytes(data) + return nil + } +} + +type ptrMapper struct { + r *Registry +} + +var _ BoolMapperExt = (*ptrMapper)(nil) + +// IsBoolFromValue implements BoolMapperExt +func (p ptrMapper) IsBoolFromValue(target reflect.Value) bool { + elem := reflect.New(target.Type().Elem()).Elem() + nestedMapper := p.r.ForValue(elem) + if nestedMapper == nil { + return false + } + if bm, ok := nestedMapper.(BoolMapper); ok && bm.IsBool() { + return true + } + if bm, ok := nestedMapper.(BoolMapperExt); ok && bm.IsBoolFromValue(target) { + return true + } + return target.Kind() == reflect.Ptr && target.Type().Elem().Kind() == reflect.Bool +} + +func (p ptrMapper) Decode(ctx *DecodeContext, target reflect.Value) error { + elem := reflect.New(target.Type().Elem()).Elem() + nestedMapper := p.r.ForValue(elem) + if nestedMapper == nil { + return fmt.Errorf("cannot find mapper for %v", target.Type().Elem().String()) + } + err := nestedMapper.Decode(ctx, elem) + if err != nil { + return err + } + target.Set(elem.Addr()) + return nil +} + func counterMapper() MapperFunc { return func(ctx *DecodeContext, target reflect.Value) error { if ctx.Scan.Peek().Type == FlagValueToken { @@ -650,7 +780,7 @@ func counterMapper() MapperFunc { case string: n, err := strconv.ParseInt(v, 10, 64) if err != nil { - return errors.Errorf("expected a counter but got %q (%T)", t, t.Value) + return fmt.Errorf("expected a counter but got %q (%T)", t, t.Value) } target.SetInt(n) @@ -658,7 +788,7 @@ func counterMapper() MapperFunc { target.Set(reflect.ValueOf(v)) default: - return errors.Errorf("expected a counter but got %q (%T)", t, t.Value) + return fmt.Errorf("expected a counter but got %q (%T)", t, t.Value) } return nil } @@ -674,7 +804,7 @@ func counterMapper() MapperFunc { target.SetFloat(target.Float() + 1) default: - return errors.Errorf("type:\"counter\" must be used with a numeric field") + return fmt.Errorf("type:\"counter\" must be used with a numeric field") } return nil } @@ -689,7 +819,7 @@ func urlMapper() MapperFunc { } url, err := url.Parse(urlStr) if err != nil { - return errors.WithStack(err) + return err } target.Set(reflect.ValueOf(url)) return nil @@ -700,19 +830,22 @@ func urlMapper() MapperFunc { // // It differs from strings.Split() in that the separator can exist in a field by escaping it with a \. eg. // -// SplitEscaped(`hello\,there,bob`, ',') == []string{"hello,there", "bob"} +// SplitEscaped(`hello\,there,bob`, ',') == []string{"hello,there", "bob"} func SplitEscaped(s string, sep rune) (out []string) { if sep == -1 { return []string{s} } escaped := false token := "" - for _, ch := range s { + for i, ch := range s { switch { case escaped: + if ch != sep { + token += `\` + } token += string(ch) escaped = false - case ch == '\\': + case ch == '\\' && i < len(s)-1: escaped = true case ch == sep && !escaped: out = append(out, token) @@ -730,11 +863,11 @@ func SplitEscaped(s string, sep rune) (out []string) { // JoinEscaped joins a slice of strings on sep, but also escapes any instances of sep in the fields with \. eg. // -// JoinEscaped([]string{"hello,there", "bob"}, ',') == `hello\,there,bob` +// JoinEscaped([]string{"hello,there", "bob"}, ',') == `hello\,there,bob` func JoinEscaped(s []string, sep rune) string { escaped := []string{} for _, e := range s { - escaped = append(escaped, strings.Replace(e, string(sep), `\`+string(sep), -1)) + escaped = append(escaped, strings.ReplaceAll(e, string(sep), `\`+string(sep))) } return strings.Join(escaped, string(sep)) } @@ -745,7 +878,7 @@ type NamedFileContentFlag struct { Contents []byte } -func (f *NamedFileContentFlag) Decode(ctx *DecodeContext) error { // nolint: golint +func (f *NamedFileContentFlag) Decode(ctx *DecodeContext) error { //nolint: revive var filename string err := ctx.Scan.PopValueInto("filename", &filename) if err != nil { @@ -757,9 +890,9 @@ func (f *NamedFileContentFlag) Decode(ctx *DecodeContext) error { // nolint: gol return nil } filename = ExpandPath(filename) - data, err := ioutil.ReadFile(filename) // nolint: gosec + data, err := os.ReadFile(filename) //nolint: gosec if err != nil { - return errors.Errorf("failed to open %q: %s", filename, err) + return fmt.Errorf("failed to open %q: %v", filename, err) } f.Contents = data f.Filename = filename @@ -769,7 +902,7 @@ func (f *NamedFileContentFlag) Decode(ctx *DecodeContext) error { // nolint: gol // FileContentFlag is a flag value that loads a file's contents into its value. type FileContentFlag []byte -func (f *FileContentFlag) Decode(ctx *DecodeContext) error { // nolint: golint +func (f *FileContentFlag) Decode(ctx *DecodeContext) error { //nolint: revive var filename string err := ctx.Scan.PopValueInto("filename", &filename) if err != nil { @@ -781,9 +914,9 @@ func (f *FileContentFlag) Decode(ctx *DecodeContext) error { // nolint: golint return nil } filename = ExpandPath(filename) - data, err := ioutil.ReadFile(filename) // nolint: gosec + data, err := os.ReadFile(filename) //nolint: gosec if err != nil { - return errors.Errorf("failed to open %q: %s", filename, err) + return fmt.Errorf("failed to open %q: %v", filename, err) } *f = data return nil @@ -792,7 +925,10 @@ func (f *FileContentFlag) Decode(ctx *DecodeContext) error { // nolint: golint func jsonTranscode(in, out interface{}) error { data, err := json.Marshal(in) if err != nil { - return errors.WithStack(err) + return err } - return errors.Wrapf(json.Unmarshal(data, out), "%#v -> %T", in, out) + if err = json.Unmarshal(data, out); err != nil { + return fmt.Errorf("%#v -> %T: %v", in, out, err) + } + return nil } diff --git a/vendor/github.com/alecthomas/kong/model.go b/vendor/github.com/alecthomas/kong/model.go index c64cab3e..8d1f82f9 100644 --- a/vendor/github.com/alecthomas/kong/model.go +++ b/vendor/github.com/alecthomas/kong/model.go @@ -7,8 +7,6 @@ import ( "reflect" "strconv" "strings" - - "github.com/pkg/errors" ) // A Visitable component in the model. @@ -41,19 +39,22 @@ const ( // Node is a branch in the CLI. ie. a command or positional argument. type Node struct { - Type NodeType - Parent *Node - Name string - Help string // Short help displayed in summaries. - Detail string // Detailed help displayed when describing command/arg alone. - Group *Group - Hidden bool - Flags []*Flag - Positional []*Positional - Children []*Node - Target reflect.Value // Pointer to the value in the grammar that this Node is associated with. - Tag *Tag - Aliases []string + Type NodeType + Parent *Node + Name string + Help string // Short help displayed in summaries. + Detail string // Detailed help displayed when describing command/arg alone. + Group *Group + Hidden bool + Flags []*Flag + Positional []*Positional + Children []*Node + DefaultCmd *Node + Target reflect.Value // Pointer to the value in the grammar that this Node is associated with. + Tag *Tag + Aliases []string + Passthrough bool // Set to true to stop flag parsing when encountered. + Active bool // Denotes the node is part of an active branch in the CLI. Argument *Value // Populated when Type is ArgumentNode. } @@ -98,6 +99,7 @@ func (n *Node) AllFlags(hide bool) (out [][]*Flag) { group := []*Flag{} for _, flag := range n.Flags { if !hide || !flag.Hidden { + flag.Active = true group = append(group, flag) } } @@ -146,14 +148,30 @@ func (n *Node) Summary() string { summary += " " + flags } args := []string{} + optional := 0 for _, arg := range n.Positional { - args = append(args, arg.Summary()) + argSummary := arg.Summary() + if arg.Tag.Optional { + optional++ + argSummary = strings.TrimRight(argSummary, "]") + } + args = append(args, argSummary) } if len(args) != 0 { - summary += " " + strings.Join(args, " ") + summary += " " + strings.Join(args, " ") + strings.Repeat("]", optional) } else if len(n.Children) > 0 { summary += " " } + allFlags := n.Flags + if n.Parent != nil { + allFlags = append(allFlags, n.Parent.Flags...) + } + for _, flag := range allFlags { + if !flag.Required { + summary += " [flags]" + break + } + } return summary } @@ -197,8 +215,12 @@ func (n *Node) Path() (out string) { switch n.Type { case CommandNode: out += " " + n.Name + if len(n.Aliases) > 0 { + out += fmt.Sprintf(" (%s)", strings.Join(n.Aliases, ",")) + } case ArgumentNode: out += " " + "<" + n.Name + ">" + default: } return strings.TrimSpace(out) } @@ -220,6 +242,8 @@ type Value struct { Flag *Flag // Nil if positional argument. Name string Help string + OrigHelp string // Original help string, without interpolated variables. + HasDefault bool Default string DefaultValue reflect.Value Enum string @@ -230,6 +254,8 @@ type Value struct { Set bool // Set to true when this value is set through some mechanism. Format string // Formatting directive, if applicable. Position int // Position (for positional arguments). + Passthrough bool // Set to true to stop flag parsing when encountered. + Active bool // Denotes the value is part of an active branch in the CLI. } // EnumMap returns a map of the enums in this value. @@ -242,6 +268,16 @@ func (v *Value) EnumMap() map[string]bool { return out } +// EnumSlice returns a slice of the enums in this value. +func (v *Value) EnumSlice() []string { + parts := strings.Split(v.Enum, ",") + out := make([]string, len(parts)) + for i, part := range parts { + out[i] = strings.TrimSpace(part) + } + return out +} + // ShortSummary returns a human-readable summary of the value, not including any placeholders/defaults. func (v *Value) ShortSummary() string { if v.Flag != nil { @@ -292,27 +328,28 @@ func (v *Value) IsMap() bool { // IsBool returns true if the underlying value is a boolean. func (v *Value) IsBool() bool { + if m, ok := v.Mapper.(BoolMapperExt); ok && m.IsBoolFromValue(v.Target) { + return true + } if m, ok := v.Mapper.(BoolMapper); ok && m.IsBool() { return true } return v.Target.Kind() == reflect.Bool } +// IsCounter returns true if the value is a counter. +func (v *Value) IsCounter() bool { + return v.Tag.Type == "counter" +} + // Parse tokens into value, parse, and validate, but do not write to the field. func (v *Value) Parse(scan *Scanner, target reflect.Value) (err error) { - defer func() { - if rerr := recover(); rerr != nil { - switch rerr := rerr.(type) { - case Error: - err = errors.Wrap(rerr, v.ShortSummary()) - default: - panic(fmt.Sprintf("mapper %T failed to apply to %s: %s", v.Mapper, v.Summary(), rerr)) - } - } - }() + if target.Kind() == reflect.Ptr && target.IsNil() { + target.Set(reflect.New(target.Type().Elem())) + } err = v.Mapper.Decode(&DecodeContext{Value: v, Scan: scan}, target) if err != nil { - return errors.Wrap(err, v.ShortSummary()) + return fmt.Errorf("%s: %w", v.ShortSummary(), err) } v.Set = true return nil @@ -339,17 +376,20 @@ func (v *Value) ApplyDefault() error { // Does not include resolvers. func (v *Value) Reset() error { v.Target.Set(reflect.Zero(v.Target.Type())) - if v.Tag.Env != "" { - envar := os.Getenv(v.Tag.Env) - if envar != "" { - err := v.Parse(ScanFromTokens(Token{Type: FlagValueToken, Value: envar}), v.Target) - if err != nil { - return fmt.Errorf("%s (from envar %s=%q)", err, v.Tag.Env, envar) + if len(v.Tag.Envs) != 0 { + for _, env := range v.Tag.Envs { + envar, ok := os.LookupEnv(env) + // Parse the first non-empty ENV in the list + if ok { + err := v.Parse(ScanFromTokens(Token{Type: FlagValueToken, Value: envar}), v.Target) + if err != nil { + return fmt.Errorf("%s (from envar %s=%q)", err, env, envar) + } + return nil } - return nil } } - if v.Default != "" { + if v.HasDefault { return v.Parse(ScanFromTokens(Token{Type: FlagValueToken, Value: v.Default}), v.Target) } return nil @@ -364,11 +404,13 @@ type Positional = Value type Flag struct { *Value Group *Group // Logical grouping when displaying. May also be used by configuration loaders to group options logically. - Xor string + Xor []string PlaceHolder string - Env string + Envs []string + Aliases []string Short rune Hidden bool + Negated bool } func (f *Flag) String() string { @@ -376,7 +418,7 @@ func (f *Flag) String() string { if f.Short != 0 { out = fmt.Sprintf("-%c, %s", f.Short, out) } - if !f.IsBool() { + if !f.IsBool() && !f.IsCounter() { out += "=" + f.FormatPlaceHolder() } return out @@ -384,25 +426,32 @@ func (f *Flag) String() string { // FormatPlaceHolder formats the placeholder string for a Flag. func (f *Flag) FormatPlaceHolder() string { + placeholderHelper, ok := f.Value.Mapper.(PlaceHolderProvider) + if ok { + return placeholderHelper.PlaceHolder(f) + } tail := "" if f.Value.IsSlice() && f.Value.Tag.Sep != -1 { tail += string(f.Value.Tag.Sep) + "..." } - if f.Default != "" { + if f.PlaceHolder != "" { + return f.PlaceHolder + tail + } + if f.HasDefault { if f.Value.Target.Kind() == reflect.String { return strconv.Quote(f.Default) + tail } return f.Default + tail } - if f.PlaceHolder != "" { - return f.PlaceHolder + tail - } if f.Value.IsMap() { if f.Value.Tag.MapSep != -1 { tail = string(f.Value.Tag.MapSep) + "..." } return "KEY=VALUE" + tail } + if f.Tag != nil && f.Tag.TypeName != "" { + return strings.ToUpper(dashedString(f.Tag.TypeName)) + tail + } return strings.ToUpper(f.Name) + tail } @@ -452,6 +501,9 @@ func reflectValueIsZero(v reflect.Value) bool { default: // This should never happens, but will act as a safeguard for // later, as a default value doesn't makes sense here. - panic(&reflect.ValueError{"reflect.Value.IsZero", v.Kind()}) + panic(&reflect.ValueError{ + Method: "reflect.Value.IsZero", + Kind: v.Kind(), + }) } } diff --git a/vendor/github.com/alecthomas/kong/options.go b/vendor/github.com/alecthomas/kong/options.go index bdb57f63..d01aeec2 100644 --- a/vendor/github.com/alecthomas/kong/options.go +++ b/vendor/github.com/alecthomas/kong/options.go @@ -1,14 +1,15 @@ package kong import ( + "errors" + "fmt" "io" "os" "os/user" "path/filepath" "reflect" + "regexp" "strings" - - "github.com/pkg/errors" ) // An Option applies optional changes to the Kong application. @@ -19,7 +20,7 @@ type Option interface { // OptionFunc is function that adheres to the Option interface. type OptionFunc func(k *Kong) error -func (o OptionFunc) Apply(k *Kong) error { return o(k) } // nolint: golint +func (o OptionFunc) Apply(k *Kong) error { return o(k) } //nolint: revive // Vars sets the variables to use for interpolation into help strings and default values. // @@ -54,6 +55,51 @@ func Exit(exit func(int)) Option { }) } +type embedded struct { + strct any + tags []string +} + +// Embed a struct into the root of the CLI. +// +// "strct" must be a pointer to a structure. +func Embed(strct any, tags ...string) Option { + t := reflect.TypeOf(strct) + if t.Kind() != reflect.Ptr || t.Elem().Kind() != reflect.Struct { + panic("kong: Embed() must be called with a pointer to a struct") + } + return OptionFunc(func(k *Kong) error { + k.embedded = append(k.embedded, embedded{strct, tags}) + return nil + }) +} + +type dynamicCommand struct { + name string + help string + group string + tags []string + cmd interface{} +} + +// DynamicCommand registers a dynamically constructed command with the root of the CLI. +// +// This is useful for command-line structures that are extensible via user-provided plugins. +// +// "tags" is a list of extra tag strings to parse, in the form :"". +func DynamicCommand(name, help, group string, cmd interface{}, tags ...string) Option { + return OptionFunc(func(k *Kong) error { + k.dynamicCommands = append(k.dynamicCommands, &dynamicCommand{ + name: name, + help: help, + group: group, + cmd: cmd, + tags: tags, + }) + return nil + }) +} + // NoDefaultHelp disables the default help flags. func NoDefaultHelp() Option { return OptionFunc(func(k *Kong) error { @@ -137,8 +183,8 @@ func Writers(stdout, stderr io.Writer) Option { // // There are two hook points: // -// BeforeApply(...) error -// AfterApply(...) error +// BeforeApply(...) error +// AfterApply(...) error // // Called before validation/assignment, and immediately after validation/assignment, respectively. func Bind(args ...interface{}) Option { @@ -150,11 +196,10 @@ func Bind(args ...interface{}) Option { // BindTo allows binding of implementations to interfaces. // -// BindTo(impl, (*iface)(nil)) +// BindTo(impl, (*iface)(nil)) func BindTo(impl, iface interface{}) Option { return OptionFunc(func(k *Kong) error { - valueOf := reflect.ValueOf(impl) - k.bindings[reflect.TypeOf(iface).Elem()] = func() (reflect.Value, error) { return valueOf, nil } + k.bindings.addTo(impl, iface) return nil }) } @@ -165,22 +210,7 @@ func BindTo(impl, iface interface{}) Option { // not all be initialisable from the main() function. func BindToProvider(provider interface{}) Option { return OptionFunc(func(k *Kong) error { - pv := reflect.ValueOf(provider) - t := pv.Type() - if t.Kind() != reflect.Func || t.NumIn() != 0 || t.NumOut() != 2 || t.Out(1) != reflect.TypeOf((*error)(nil)).Elem() { - return errors.Errorf("%T must be a function with the signature func()(T, error)", provider) - } - rt := pv.Type().Out(0) - k.bindings[rt] = func() (reflect.Value, error) { - out := pv.Call(nil) - errv := out[1] - var err error - if !errv.IsNil() { - err = errv.Interface().(error) - } - return out[0], err - } - return nil + return k.bindings.addProvider(provider) }) } @@ -192,7 +222,20 @@ func Help(help HelpPrinter) Option { }) } +// ShortHelp configures the short usage message. +// +// It should be used together with kong.ShortUsageOnError() to display a +// custom short usage message on errors. +func ShortHelp(shortHelp HelpPrinter) Option { + return OptionFunc(func(k *Kong) error { + k.shortHelp = shortHelp + return nil + }) +} + // HelpFormatter configures how the help text is formatted. +// +// Deprecated: Use ValueFormatter() instead. func HelpFormatter(helpFormatter HelpValueFormatter) Option { return OptionFunc(func(k *Kong) error { k.helpFormatter = helpFormatter @@ -200,6 +243,14 @@ func HelpFormatter(helpFormatter HelpValueFormatter) Option { }) } +// ValueFormatter configures how the help text is formatted. +func ValueFormatter(helpFormatter HelpValueFormatter) Option { + return OptionFunc(func(k *Kong) error { + k.helpFormatter = helpFormatter + return nil + }) +} + // ConfigureHelp sets the HelpOptions to use for printing help. func ConfigureHelp(options HelpOptions) Option { return OptionFunc(func(k *Kong) error { @@ -208,6 +259,21 @@ func ConfigureHelp(options HelpOptions) Option { }) } +// AutoGroup automatically assigns groups to flags. +func AutoGroup(format func(parent Visitable, flag *Flag) *Group) Option { + return PostBuild(func(kong *Kong) error { + parents := []Visitable{kong.Model} + return Visit(kong.Model, func(node Visitable, next Next) error { + if flag, ok := node.(*Flag); ok && flag.Group == nil { + flag.Group = format(parents[len(parents)-1], flag) + } + parents = append(parents, node) + defer func() { parents = parents[:len(parents)-1] }() + return next(nil) + }) + }) +} + // Groups associates `group` field tags with group metadata. // // This option is used to simplify Kong tags while providing @@ -221,7 +287,7 @@ func ConfigureHelp(options HelpOptions) Option { // See also ExplicitGroups for a more structured alternative. type Groups map[string]string -func (g Groups) Apply(k *Kong) error { // nolint: golint +func (g Groups) Apply(k *Kong) error { //nolint: revive for key, info := range g { lines := strings.Split(info, "\n") title := strings.TrimSpace(lines[0]) @@ -251,7 +317,17 @@ func ExplicitGroups(groups []Group) Option { // UsageOnError configures Kong to display context-sensitive usage if FatalIfErrorf is called with an error. func UsageOnError() Option { return OptionFunc(func(k *Kong) error { - k.usageOnError = true + k.usageOnError = fullUsage + return nil + }) +} + +// ShortUsageOnError configures Kong to display context-sensitive short +// usage if FatalIfErrorf is called with an error. The default short +// usage message can be overridden with kong.ShortHelp(...). +func ShortUsageOnError() Option { + return OptionFunc(func(k *Kong) error { + k.usageOnError = shortUsage return nil }) } @@ -272,6 +348,31 @@ func Resolvers(resolvers ...Resolver) Option { }) } +// IgnoreFields will cause kong.New() to skip field names that match any +// of the provided regex patterns. This is useful if you are not able to add a +// kong="-" struct tag to a struct/element before the call to New. +// +// Example: When referencing protoc generated structs, you will likely want to +// ignore/skip XXX_* fields. +func IgnoreFields(regexes ...string) Option { + return OptionFunc(func(k *Kong) error { + for _, r := range regexes { + if r == "" { + return errors.New("regex input cannot be empty") + } + + re, err := regexp.Compile(r) + if err != nil { + return fmt.Errorf("unable to compile regex: %v", err) + } + + k.ignoreFields = append(k.ignoreFields, re) + } + + return nil + }) +} + // ConfigurationLoader is a function that builds a resolver from a file. type ConfigurationLoader func(r io.Reader) (Resolver, error) @@ -286,12 +387,19 @@ func Configuration(loader ConfigurationLoader, paths ...string) Option { return OptionFunc(func(k *Kong) error { k.loader = loader for _, path := range paths { - if _, err := os.Stat(ExpandPath(path)); os.IsNotExist(err) { - continue + f, err := os.Open(ExpandPath(path)) + if err != nil { + if os.IsNotExist(err) || os.IsPermission(err) { + continue + } + + return err } + f.Close() + resolver, err := k.LoadConfig(path) if err != nil { - return errors.Wrap(err, path) + return fmt.Errorf("%s: %v", path, err) } if resolver != nil { k.resolvers = append(k.resolvers, resolver) @@ -321,3 +429,65 @@ func ExpandPath(path string) string { } return abspath } + +func siftStrings(ss []string, filter func(s string) bool) []string { + i := 0 + ss = append([]string(nil), ss...) + for _, s := range ss { + if filter(s) { + ss[i] = s + i++ + } + } + return ss[0:i] +} + +// DefaultEnvars option inits environment names for flags. +// The name will not generate if tag "env" is "-". +// Predefined environment variables are skipped. +// +// For example: +// +// --some.value -> PREFIX_SOME_VALUE +func DefaultEnvars(prefix string) Option { + processFlag := func(flag *Flag) { + switch env := flag.Envs; { + case flag.Name == "help": + return + case len(env) == 1 && env[0] == "-": + flag.Envs = nil + return + case len(env) > 0: + return + } + replacer := strings.NewReplacer("-", "_", ".", "_") + names := append([]string{prefix}, camelCase(replacer.Replace(flag.Name))...) + names = siftStrings(names, func(s string) bool { return !(s == "_" || strings.TrimSpace(s) == "") }) + name := strings.ToUpper(strings.Join(names, "_")) + flag.Envs = append(flag.Envs, name) + flag.Value.Tag.Envs = append(flag.Value.Tag.Envs, name) + } + + var processNode func(node *Node) + processNode = func(node *Node) { + for _, flag := range node.Flags { + processFlag(flag) + } + for _, node := range node.Children { + processNode(node) + } + } + + return PostBuild(func(k *Kong) error { + processNode(k.Model.Node) + return nil + }) +} + +// FlagNamer allows you to override the default kebab-case automated flag name generation. +func FlagNamer(namer func(fieldName string) string) Option { + return OptionFunc(func(k *Kong) error { + k.flagNamer = namer + return nil + }) +} diff --git a/vendor/github.com/alecthomas/kong/renovate.json5 b/vendor/github.com/alecthomas/kong/renovate.json5 new file mode 100644 index 00000000..561d59fd --- /dev/null +++ b/vendor/github.com/alecthomas/kong/renovate.json5 @@ -0,0 +1,18 @@ +{ + $schema: "https://docs.renovatebot.com/renovate-schema.json", + extends: [ + "config:recommended", + ":semanticCommits", + ":semanticCommitTypeAll(chore)", + ":semanticCommitScope(deps)", + "group:allNonMajor", + "schedule:earlyMondays", // Run once a week. + ], + packageRules: [ + { + "matchPackageNames": ["golangci-lint"], + "matchManagers": ["hermit"], + "enabled": false + }, + ] +} diff --git a/vendor/github.com/alecthomas/kong/resolver.go b/vendor/github.com/alecthomas/kong/resolver.go index 8ef764dd..05be7f68 100644 --- a/vendor/github.com/alecthomas/kong/resolver.go +++ b/vendor/github.com/alecthomas/kong/resolver.go @@ -22,14 +22,14 @@ type ResolverFunc func(context *Context, parent *Path, flag *Flag) (interface{}, var _ Resolver = ResolverFunc(nil) -func (r ResolverFunc) Resolve(context *Context, parent *Path, flag *Flag) (interface{}, error) { // nolint: golint +func (r ResolverFunc) Resolve(context *Context, parent *Path, flag *Flag) (interface{}, error) { //nolint: revive return r(context, parent, flag) } -func (r ResolverFunc) Validate(app *Application) error { return nil } // nolint: golint +func (r ResolverFunc) Validate(app *Application) error { return nil } //nolint: revive // JSON returns a Resolver that retrieves values from a JSON source. // -// Hyphens in flag names are replaced with underscores. +// Flag names are used as JSON keys indirectly, by tring snake_case and camelCase variants. func JSON(r io.Reader) (Resolver, error) { values := map[string]interface{}{} err := json.NewDecoder(r).Decode(&values) @@ -37,13 +37,32 @@ func JSON(r io.Reader) (Resolver, error) { return nil, err } var f ResolverFunc = func(context *Context, parent *Path, flag *Flag) (interface{}, error) { - name := strings.Replace(flag.Name, "-", "_", -1) + name := strings.ReplaceAll(flag.Name, "-", "_") + snakeCaseName := snakeCase(flag.Name) raw, ok := values[name] - if !ok { - return nil, nil + if ok { + return raw, nil + } else if raw, ok = values[snakeCaseName]; ok { + return raw, nil + } + raw = values + for _, part := range strings.Split(name, ".") { + if values, ok := raw.(map[string]interface{}); ok { + raw, ok = values[part] + if !ok { + return nil, nil + } + } else { + return nil, nil + } } return raw, nil } return f, nil } + +func snakeCase(name string) string { + name = strings.Join(strings.Split(strings.Title(name), "-"), "") //nolint: staticcheck + return strings.ToLower(name[:1]) + name[1:] +} diff --git a/vendor/github.com/alecthomas/kong/scanner.go b/vendor/github.com/alecthomas/kong/scanner.go index 11b78c38..c8a8bd60 100644 --- a/vendor/github.com/alecthomas/kong/scanner.go +++ b/vendor/github.com/alecthomas/kong/scanner.go @@ -82,7 +82,7 @@ func (t Token) InferredType() TokenType { return t.Type } if v, ok := t.Value.(string); ok { - if strings.HasPrefix(v, "--") { // nolint: gocritic + if strings.HasPrefix(v, "--") { //nolint: gocritic return FlagToken } else if v == "-" { return PositionalArgumentToken @@ -109,20 +109,25 @@ func (t Token) IsValue() bool { // // For example, the token "--foo=bar" will be split into the following by the parser: // -// [{FlagToken, "foo"}, {FlagValueToken, "bar"}] +// [{FlagToken, "foo"}, {FlagValueToken, "bar"}] type Scanner struct { args []Token } -// Scan creates a new Scanner from args with untyped tokens. -func Scan(args ...string) *Scanner { +// ScanAsType creates a new Scanner from args with the given type. +func ScanAsType(ttype TokenType, args ...string) *Scanner { s := &Scanner{} for _, arg := range args { - s.args = append(s.args, Token{Value: arg}) + s.args = append(s.args, Token{Value: arg, Type: ttype}) } return s } +// Scan creates a new Scanner from args with untyped tokens. +func Scan(args ...string) *Scanner { + return ScanAsType(UntypedToken, args...) +} + // ScanFromTokens creates a new Scanner from a slice of tokens. func ScanFromTokens(tokens ...Token) *Scanner { return &Scanner{args: tokens} diff --git a/vendor/github.com/alecthomas/kong/tag.go b/vendor/github.com/alecthomas/kong/tag.go index b780000c..3e37c194 100644 --- a/vendor/github.com/alecthomas/kong/tag.go +++ b/vendor/github.com/alecthomas/kong/tag.go @@ -1,6 +1,7 @@ package kong import ( + "errors" "fmt" "reflect" "strconv" @@ -18,45 +19,76 @@ type Tag struct { Name string Help string Type string + TypeName string + HasDefault bool Default string Format string PlaceHolder string - Env string + Envs []string Short rune Hidden bool Sep rune MapSep rune Enum string Group string - Xor string + Xor []string Vars Vars Prefix string // Optional prefix on anonymous structs. All sub-flags will have this prefix. + EnvPrefix string Embed bool Aliases []string + Negatable bool + Passthrough bool // Storage for all tag keys for arbitrary lookups. items map[string][]string } +func (t *Tag) String() string { + out := []string{} + for key, list := range t.items { + for _, value := range list { + out = append(out, fmt.Sprintf("%s:%q", key, value)) + } + } + return strings.Join(out, " ") +} + type tagChars struct { sep, quote, assign rune + needsUnquote bool } -var kongChars = tagChars{sep: ',', quote: '\'', assign: '='} -var bareChars = tagChars{sep: ' ', quote: '"', assign: ':'} +var kongChars = tagChars{sep: ',', quote: '\'', assign: '=', needsUnquote: false} +var bareChars = tagChars{sep: ' ', quote: '"', assign: ':', needsUnquote: true} -func parseTagItems(tagString string, chr tagChars) map[string][]string { +//nolint:gocyclo +func parseTagItems(tagString string, chr tagChars) (map[string][]string, error) { d := map[string][]string{} key := []rune{} value := []rune{} quotes := false inKey := true - add := func() { - d[string(key)] = append(d[string(key)], string(value)) + add := func() error { + // Bare tags are quoted, therefore we need to unquote them in the same fashion reflect.Lookup() (implicitly) + // unquotes "kong tags". + s := string(value) + + if chr.needsUnquote && s != "" { + if unquoted, err := strconv.Unquote(fmt.Sprintf(`"%s"`, s)); err == nil { + s = unquoted + } else { + return fmt.Errorf("unquoting tag value `%s`: %w", s, err) + } + } + + d[string(key)] = append(d[string(key)], s) key = []rune{} value = []rune{} inKey = true + + return nil } runes := []rune(tagString) @@ -70,7 +102,10 @@ func parseTagItems(tagString string, chr tagChars) map[string][]string { eof = true } if !quotes && r == chr.sep { - add() + if err := add(); err != nil { + return nil, err + } + continue } if r == chr.assign && inKey { @@ -80,6 +115,12 @@ func parseTagItems(tagString string, chr tagChars) map[string][]string { if r == '\\' { if next == chr.quote { idx++ + + // We need to keep the backslashes, otherwise subsequent unquoting cannot work + if chr.needsUnquote { + value = append(value, r) + } + r = chr.quote } } else if r == chr.quote { @@ -88,11 +129,10 @@ func parseTagItems(tagString string, chr tagChars) map[string][]string { if next == chr.sep || eof { continue } - fail("%v has an unexpected char at pos %v", tagString, idx) - } else { - quotes = true - continue + return nil, fmt.Errorf("%v has an unexpected char at pos %v", tagString, idx) } + quotes = true + continue } if inKey { key = append(key, r) @@ -101,12 +141,14 @@ func parseTagItems(tagString string, chr tagChars) map[string][]string { } } if quotes { - fail("%v is not quoted properly", tagString) + return nil, fmt.Errorf("%v is not quoted properly", tagString) } - add() + if err := add(); err != nil { + return nil, err + } - return d + return d, nil } func getTagInfo(ft reflect.StructField) (string, tagChars) { @@ -122,63 +164,123 @@ func newEmptyTag() *Tag { return &Tag{items: map[string][]string{}} } -func parseTag(fv reflect.Value, ft reflect.StructField) *Tag { +func tagSplitFn(r rune) bool { + return r == ',' || r == ' ' +} + +func parseTagString(s string) (*Tag, error) { + items, err := parseTagItems(s, bareChars) + if err != nil { + return nil, err + } + t := &Tag{ + items: items, + } + err = hydrateTag(t, nil) + if err != nil { + return nil, fmt.Errorf("%s: %s", s, err) + } + return t, nil +} + +func parseTag(parent reflect.Value, ft reflect.StructField) (*Tag, error) { if ft.Tag.Get("kong") == "-" { t := newEmptyTag() t.Ignored = true - return t + return t, nil + } + items, err := parseTagItems(getTagInfo(ft)) + if err != nil { + return nil, err } t := &Tag{ - items: parseTagItems(getTagInfo(ft)), + items: items, } + err = hydrateTag(t, ft.Type) + if err != nil { + return nil, failField(parent, ft, "%s", err) + } + return t, nil +} + +func hydrateTag(t *Tag, typ reflect.Type) error { //nolint: gocyclo + var typeName string + var isBool bool + var isBoolPtr bool + if typ != nil { + typeName = typ.Name() + isBool = typ.Kind() == reflect.Bool + isBoolPtr = typ.Kind() == reflect.Ptr && typ.Elem().Kind() == reflect.Bool + } + var err error t.Cmd = t.Has("cmd") t.Arg = t.Has("arg") required := t.Has("required") optional := t.Has("optional") if required && optional { - fail("can't specify both required and optional") + return fmt.Errorf("can't specify both required and optional") } t.Required = required t.Optional = optional + t.HasDefault = t.Has("default") t.Default = t.Get("default") // Arguments with defaults are always optional. - if t.Arg && t.Default != "" { + if t.Arg && t.HasDefault { t.Optional = true + } else if t.Arg && !optional { // Arguments are required unless explicitly made optional. + t.Required = true } t.Name = t.Get("name") t.Help = t.Get("help") t.Type = t.Get("type") - t.Env = t.Get("env") - t.Short, _ = t.GetRune("short") + t.TypeName = typeName + for _, env := range t.GetAll("env") { + t.Envs = append(t.Envs, strings.FieldsFunc(env, tagSplitFn)...) + } + t.Short, err = t.GetRune("short") + if err != nil && t.Get("short") != "" { + return fmt.Errorf("invalid short flag name %q: %s", t.Get("short"), err) + } t.Hidden = t.Has("hidden") t.Format = t.Get("format") t.Sep, _ = t.GetSep("sep", ',') t.MapSep, _ = t.GetSep("mapsep", ';') t.Group = t.Get("group") - t.Xor = t.Get("xor") + for _, xor := range t.GetAll("xor") { + t.Xor = append(t.Xor, strings.FieldsFunc(xor, tagSplitFn)...) + } t.Prefix = t.Get("prefix") + t.EnvPrefix = t.Get("envprefix") t.Embed = t.Has("embed") - splitFn := func(r rune) bool { - return r == ',' || r == ' ' + negatable := t.Has("negatable") + if negatable && !isBool && !isBoolPtr { + return fmt.Errorf("negatable can only be set on booleans") } + t.Negatable = negatable aliases := t.Get("aliases") if len(aliases) > 0 { - t.Aliases = append(t.Aliases, strings.FieldsFunc(aliases, splitFn)...) + t.Aliases = append(t.Aliases, strings.FieldsFunc(aliases, tagSplitFn)...) } t.Vars = Vars{} for _, set := range t.GetAll("set") { parts := strings.SplitN(set, "=", 2) if len(parts) == 0 { - fail("set should be in the form key=value but got %q", set) + return fmt.Errorf("set should be in the form key=value but got %q", set) } t.Vars[parts[0]] = parts[1] } t.PlaceHolder = t.Get("placeholder") - if t.PlaceHolder == "" { - t.PlaceHolder = strings.ToUpper(dashedString(fv.Type().Name())) - } t.Enum = t.Get("enum") - return t + scalarType := typ == nil || !(typ.Kind() == reflect.Slice || typ.Kind() == reflect.Map || typ.Kind() == reflect.Ptr) + if t.Enum != "" && !(t.Required || t.HasDefault) && scalarType { + return fmt.Errorf("enum value is only valid if it is either required or has a valid default value") + } + passthrough := t.Has("passthrough") + if passthrough && !t.Arg && !t.Cmd { + return fmt.Errorf("passthrough only makes sense for positional arguments or commands") + } + t.Passthrough = passthrough + return nil } // Has returns true if the tag contained the given key. @@ -220,9 +322,10 @@ func (t *Tag) GetInt(k string) (int64, error) { // GetRune parses the given tag as a rune. func (t *Tag) GetRune(k string) (rune, error) { - r, _ := utf8.DecodeRuneInString(t.Get(k)) - if r == utf8.RuneError { - return 0, fmt.Errorf("%v has a rune error", t.Get(k)) + value := t.Get(k) + r, size := utf8.DecodeRuneInString(value) + if r == utf8.RuneError || size < len(value) { + return 0, errors.New("invalid rune") } return r, nil } diff --git a/vendor/github.com/alecthomas/kong/util.go b/vendor/github.com/alecthomas/kong/util.go index 154aebcc..8b706642 100644 --- a/vendor/github.com/alecthomas/kong/util.go +++ b/vendor/github.com/alecthomas/kong/util.go @@ -2,6 +2,8 @@ package kong import ( "fmt" + "os" + "reflect" ) // ConfigFlag uses the configured (via kong.Configuration(loader)) configuration loader to load configuration @@ -15,7 +17,7 @@ func (c ConfigFlag) BeforeResolve(kong *Kong, ctx *Context, trace *Path) error { if kong.loader == nil { return fmt.Errorf("kong must be configured with kong.Configuration(...)") } - path := string(ctx.FlagValue(trace.Flag).(ConfigFlag)) + path := string(ctx.FlagValue(trace.Flag).(ConfigFlag)) //nolint resolver, err := kong.LoadConfig(path) if err != nil { return err @@ -27,9 +29,29 @@ func (c ConfigFlag) BeforeResolve(kong *Kong, ctx *Context, trace *Path) error { // VersionFlag is a flag type that can be used to display a version number, stored in the "version" variable. type VersionFlag bool -// BeforeApply writes the version variable and terminates with a 0 exit status. -func (v VersionFlag) BeforeApply(app *Kong, vars Vars) error { +// BeforeReset writes the version variable and terminates with a 0 exit status. +func (v VersionFlag) BeforeReset(app *Kong, vars Vars) error { fmt.Fprintln(app.Stdout, vars["version"]) app.Exit(0) return nil } + +// ChangeDirFlag changes the current working directory to a path specified by a flag +// early in the parsing process, changing how other flags resolve relative paths. +// +// Use this flag to provide a "git -C" like functionality. +// +// It is not compatible with custom named decoders, e.g., existingdir. +type ChangeDirFlag string + +// Decode is used to create a side effect of changing the current working directory. +func (c ChangeDirFlag) Decode(ctx *DecodeContext) error { + var path string + err := ctx.Scan.PopValueInto("string", &path) + if err != nil { + return err + } + path = ExpandPath(path) + ctx.Value.Target.Set(reflect.ValueOf(ChangeDirFlag(path))) + return os.Chdir(path) +} diff --git a/vendor/github.com/alecthomas/repr/renovate.json5 b/vendor/github.com/alecthomas/repr/renovate.json5 new file mode 100644 index 00000000..897864b8 --- /dev/null +++ b/vendor/github.com/alecthomas/repr/renovate.json5 @@ -0,0 +1,11 @@ +{ + $schema: "https://docs.renovatebot.com/renovate-schema.json", + extends: [ + "config:recommended", + ":semanticCommits", + ":semanticCommitTypeAll(chore)", + ":semanticCommitScope(deps)", + "group:allNonMajor", + "schedule:earlyMondays", // Run once a week. + ], +} diff --git a/vendor/github.com/alecthomas/repr/repr.go b/vendor/github.com/alecthomas/repr/repr.go index 6bc21511..c03eb067 100644 --- a/vendor/github.com/alecthomas/repr/repr.go +++ b/vendor/github.com/alecthomas/repr/repr.go @@ -12,6 +12,7 @@ import ( "os" "reflect" "sort" + "strings" "time" "unsafe" ) @@ -44,6 +45,7 @@ var ( } goStringerType = reflect.TypeOf((*fmt.GoStringer)(nil)).Elem() + anyType = reflect.TypeOf((*any)(nil)).Elem() byteSliceType = reflect.TypeOf([]byte{}) ) @@ -69,13 +71,20 @@ func ExplicitTypes(ok bool) Option { return func(o *Printer) { o.explicitTypes = // IgnoreGoStringer disables use of the .GoString() method. func IgnoreGoStringer() Option { return func(o *Printer) { o.ignoreGoStringer = true } } -// Hide excludes the given types from representation, instead just printing the name of the type. -func Hide(ts ...interface{}) Option { +// IgnorePrivate disables private field members from output. +func IgnorePrivate() Option { return func(o *Printer) { o.ignorePrivate = true } } + +// ScalarLiterals forces the use of literals for scalars, rather than a string representation if available. +// +// For example, `time.Hour` will be printed as `time.Duration(3600000000000)` rather than `time.Duration(1h0m0s)`. +func ScalarLiterals() Option { return func(o *Printer) { o.useLiterals = true } } + +// Hide excludes fields of the given type from representation. +func Hide[T any]() Option { return func(o *Printer) { - for _, t := range ts { - rt := reflect.Indirect(reflect.ValueOf(t)).Type() - o.exclude[rt] = true - } + t := (*T)(nil) // A bit of skulduggery so we can Hide() interfaces. + rt := reflect.TypeOf(t).Elem() + o.exclude[rt] = true } } @@ -87,10 +96,12 @@ type Printer struct { indent string omitEmpty bool ignoreGoStringer bool + ignorePrivate bool alwaysIncludeType bool explicitTypes bool exclude map[reflect.Type]bool w io.Writer + useLiterals bool } // New creates a new Printer on w with the given Options. @@ -122,27 +133,28 @@ func (p *Printer) thisIndent(indent string) string { } // Print the values. -func (p *Printer) Print(vs ...interface{}) { +func (p *Printer) Print(vs ...any) { for i, v := range vs { if i > 0 { fmt.Fprint(p.w, " ") } - p.reprValue(map[reflect.Value]bool{}, reflect.ValueOf(v), "", true) + p.reprValue(map[reflect.Value]bool{}, reflect.ValueOf(v), "", true, false) } } // Println prints each value on a new line. -func (p *Printer) Println(vs ...interface{}) { +func (p *Printer) Println(vs ...any) { for i, v := range vs { if i > 0 { fmt.Fprint(p.w, " ") } - p.reprValue(map[reflect.Value]bool{}, reflect.ValueOf(v), "", true) + p.reprValue(map[reflect.Value]bool{}, reflect.ValueOf(v), "", true, false) } fmt.Fprintln(p.w) } -func (p *Printer) reprValue(seen map[reflect.Value]bool, v reflect.Value, indent string, showType bool) { // nolint: gocyclo +// showType is true if struct types should be shown. isAnyValue is true if the containing value is an "any" type. +func (p *Printer) reprValue(seen map[reflect.Value]bool, v reflect.Value, indent string, showStructType bool, isAnyValue bool) { // nolint: gocyclo if seen[v] { fmt.Fprint(p.w, "...") return @@ -154,10 +166,6 @@ func (p *Printer) reprValue(seen map[reflect.Value]bool, v reflect.Value, indent fmt.Fprint(p.w, "nil") return } - if p.exclude[v.Type()] { - fmt.Fprintf(p.w, "%s...", v.Type().Name()) - return - } t := v.Type() if t == byteSliceType { @@ -173,7 +181,7 @@ func (p *Printer) reprValue(seen map[reflect.Value]bool, v reflect.Value, indent } } // Attempt to use fmt.GoStringer interface. - if !p.ignoreGoStringer && t.Implements(goStringerType) { + if !p.ignoreGoStringer && t.Implements(goStringerType) && v.CanInterface() { fmt.Fprint(p.w, v.Interface().(fmt.GoStringer).GoString()) return } @@ -181,10 +189,7 @@ func (p *Printer) reprValue(seen map[reflect.Value]bool, v reflect.Value, indent ni := p.nextIndent(indent) switch v.Kind() { case reflect.Slice, reflect.Array: - if p.omitEmpty && v.Len() == 0 { - return - } - fmt.Fprintf(p.w, "%s{", v.Type()) + fmt.Fprintf(p.w, "%s{", substAny(v.Type())) if v.Len() == 0 { fmt.Fprint(p.w, "}") } else { @@ -194,7 +199,7 @@ func (p *Printer) reprValue(seen map[reflect.Value]bool, v reflect.Value, indent for i := 0; i < v.Len(); i++ { e := v.Index(i) fmt.Fprintf(p.w, "%s", ni) - p.reprValue(seen, e, ni, p.alwaysIncludeType || p.explicitTypes) + p.reprValue(seen, e, ni, p.alwaysIncludeType || p.explicitTypes, v.Type().Elem() == anyType) if p.indent != "" { fmt.Fprintf(p.w, ",\n") } else if i < v.Len()-1 { @@ -206,11 +211,11 @@ func (p *Printer) reprValue(seen map[reflect.Value]bool, v reflect.Value, indent case reflect.Chan: fmt.Fprintf(p.w, "make(") - fmt.Fprintf(p.w, "%s", v.Type()) + fmt.Fprintf(p.w, "%s", substAny(v.Type())) fmt.Fprintf(p.w, ", %d)", v.Cap()) case reflect.Map: - fmt.Fprintf(p.w, "%s{", v.Type()) + fmt.Fprintf(p.w, "%s{", substAny(v.Type())) if p.indent != "" && v.Len() != 0 { fmt.Fprintf(p.w, "\n") } @@ -221,9 +226,9 @@ func (p *Printer) reprValue(seen map[reflect.Value]bool, v reflect.Value, indent for i, k := range keys { kv := v.MapIndex(k) fmt.Fprintf(p.w, "%s", ni) - p.reprValue(seen, k, ni, p.alwaysIncludeType || p.explicitTypes) + p.reprValue(seen, k, ni, p.alwaysIncludeType || p.explicitTypes, v.Type().Key() == anyType) fmt.Fprintf(p.w, ": ") - p.reprValue(seen, kv, ni, true) + p.reprValue(seen, kv, ni, true, v.Type().Elem() == anyType) if p.indent != "" { fmt.Fprintf(p.w, ",\n") } else if i < v.Len()-1 { @@ -233,29 +238,67 @@ func (p *Printer) reprValue(seen map[reflect.Value]bool, v reflect.Value, indent fmt.Fprintf(p.w, "%s}", in) case reflect.Struct: - if td, ok := v.Interface().(time.Time); ok { + if td, ok := asTime(v); ok { timeToGo(p.w, td) } else { - if showType { - fmt.Fprintf(p.w, "%s{", v.Type()) + if showStructType { + fmt.Fprintf(p.w, "%s{", substAny(v.Type())) } else { fmt.Fprint(p.w, "{") } if p.indent != "" && v.NumField() != 0 { fmt.Fprintf(p.w, "\n") } + previous := false for i := 0; i < v.NumField(); i++ { t := v.Type().Field(i) + if p.exclude[t.Type] { + continue + } f := v.Field(i) - if p.omitEmpty && isZero(f) { + ft := f.Type() + // skip private fields + if p.ignorePrivate && !f.CanInterface() { continue } + if p.omitEmpty && (f.IsZero() || + ft.Kind() == reflect.Slice && f.Len() == 0 || + ft.Kind() == reflect.Map && f.Len() == 0) { + continue + } + if previous && p.indent == "" { + fmt.Fprintf(p.w, ", ") + } + previous = true fmt.Fprintf(p.w, "%s%s: ", ni, t.Name) - p.reprValue(seen, f, ni, true) + p.reprValue(seen, f, ni, true, t.Type == anyType) + + // if private fields should be ignored, look up if a public + // field need to be displayed and breaks at the first public + // field found preventing from looping over all remaining + // fields. + // + // If no other field need to be displayed, continue and do + // not print a comma. + // + // This prevents from having a trailing comma if a private + // field ends a structure. + if p.ignorePrivate { + nc := false + for j := i + 1; j < v.NumField(); j++ { + if v.Field(j).CanInterface() { + nc = true + // exit for j loop + break + } + } + // Skip comma display if no remaining public field found. + if !nc { + continue + } + } if p.indent != "" { fmt.Fprintf(p.w, ",\n") - } else if i < v.NumField()-1 { - fmt.Fprintf(p.w, ", ") } } fmt.Fprintf(p.w, "%s}", indent) @@ -265,10 +308,10 @@ func (p *Printer) reprValue(seen map[reflect.Value]bool, v reflect.Value, indent fmt.Fprintf(p.w, "nil") return } - if showType { + if showStructType { fmt.Fprintf(p.w, "&") } - p.reprValue(seen, v.Elem(), indent, showType) + p.reprValue(seen, v.Elem(), indent, showStructType, false) case reflect.String: if t.Name() != "string" || p.alwaysIncludeType { @@ -279,22 +322,37 @@ func (p *Printer) reprValue(seen map[reflect.Value]bool, v reflect.Value, indent case reflect.Interface: if v.IsNil() { - fmt.Fprintf(p.w, "interface {}(nil)") + fmt.Fprintf(p.w, "%s(nil)", substAny(v.Type())) } else { - p.reprValue(seen, v.Elem(), indent, true) + p.reprValue(seen, v.Elem(), indent, true, true) } + case reflect.Func: + fmt.Fprint(p.w, substAny(v.Type())) + default: - if t.Name() != realKindName[t.Kind()] || p.alwaysIncludeType { - fmt.Fprintf(p.w, "%s(%v)", t, v) + value := fmt.Sprintf("%v", v) + if p.useLiterals { + value = fmt.Sprintf("%#v", v) + } + if t.Name() != realKindName[t.Kind()] || p.alwaysIncludeType || isAnyValue { + fmt.Fprintf(p.w, "%s(%s)", t, value) } else { - fmt.Fprintf(p.w, "%v", v) + fmt.Fprintf(p.w, "%s", value) } } } +func asTime(v reflect.Value) (time.Time, bool) { + if !v.CanInterface() { + return time.Time{}, false + } + t, ok := v.Interface().(time.Time) + return t, ok +} + // String returns a string representing v. -func String(v interface{}, options ...Option) string { +func String(v any, options ...Option) string { w := bytes.NewBuffer(nil) options = append([]Option{NoIndent()}, options...) p := New(w, options...) @@ -302,7 +360,7 @@ func String(v interface{}, options ...Option) string { return w.String() } -func extractOptions(vs ...interface{}) (args []interface{}, options []Option) { +func extractOptions(vs ...any) (args []any, options []Option) { for _, v := range vs { if o, ok := v.(Option); ok { options = append(options, o) @@ -314,35 +372,17 @@ func extractOptions(vs ...interface{}) (args []interface{}, options []Option) { } // Println prints v to os.Stdout, one per line. -func Println(vs ...interface{}) { +func Println(vs ...any) { args, options := extractOptions(vs...) New(os.Stdout, options...).Println(args...) } // Print writes a representation of v to os.Stdout, separated by spaces. -func Print(vs ...interface{}) { +func Print(vs ...any) { args, options := extractOptions(vs...) New(os.Stdout, options...).Print(args...) } -func isZero(v reflect.Value) bool { - switch v.Kind() { - case reflect.Array, reflect.String: - return v.Len() == 0 - case reflect.Bool: - return !v.Bool() - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - return v.Int() == 0 - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: - return v.Uint() == 0 - case reflect.Float32, reflect.Float64: - return v.Float() == 0 - case reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice: - return v.IsNil() - } - return false -} - func timeToGo(w io.Writer, t time.Time) { if t.IsZero() { fmt.Fprint(w, "time.Time{}") @@ -364,3 +404,39 @@ func timeToGo(w io.Writer, t time.Time) { y, m, d := t.Date() fmt.Fprintf(w, `time.Date(%d, %d, %d, %d, %d, %d, %d, %s)`, y, m, d, t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), zone) } + +// Replace "interface {}" with "any" +func substAny(t reflect.Type) string { + switch t.Kind() { + case reflect.Array: + return fmt.Sprintf("[%d]%s", t.Len(), substAny(t.Elem())) + + case reflect.Slice: + return "[]" + substAny(t.Elem()) + + case reflect.Map: + return "map[" + substAny(t.Key()) + "]" + substAny(t.Elem()) + + case reflect.Chan: + return fmt.Sprintf("%s %s", t.ChanDir(), substAny(t.Elem())) + + case reflect.Func: + in := []string{} + out := []string{} + for i := 0; i < t.NumIn(); i++ { + in = append(in, substAny(t.In(i))) + } + for i := 0; i < t.NumOut(); i++ { + out = append(out, substAny(t.Out(i))) + } + if len(out) == 0 { + return "func" + t.Name() + "(" + strings.Join(in, ", ") + ")" + } + return "func" + t.Name() + "(" + strings.Join(in, ", ") + ") (" + strings.Join(out, ", ") + ")" + } + + if t == anyType { + return "any" + } + return t.String() +} diff --git a/vendor/modules.txt b/vendor/modules.txt index e3a268a8..d6af1e99 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -130,11 +130,11 @@ github.com/alecthomas/assert # github.com/alecthomas/colour v0.1.0 ## explicit github.com/alecthomas/colour -# github.com/alecthomas/kong v0.2.15 -## explicit; go 1.13 +# github.com/alecthomas/kong v0.9.0 +## explicit; go 1.18 github.com/alecthomas/kong -# github.com/alecthomas/repr v0.0.0-20201120212035-bb82daffcca2 -## explicit; go 1.15 +# github.com/alecthomas/repr v0.4.0 +## explicit; go 1.18 github.com/alecthomas/repr # github.com/alessio/shellescape v1.4.1 ## explicit; go 1.14