diff --git a/README.md b/README.md index ee4befe..4a86251 100644 --- a/README.md +++ b/README.md @@ -5,40 +5,39 @@ [![](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) -- [Kong is a command-line parser for Go](#kong-is-a-command-line-parser-for-go) - - [Version 1.0.0 Release](#version-100-release) - - [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) - - [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: 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) - - [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) - - [`Name(help)` and `Description(help)` - set the application name description](#namehelp-and-descriptionhelp---set-the-application-name-description) - - [`Configuration(loader, 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) - - [`ConfigureHelp(HelpOptions)` and `Help(HelpFunc)` - 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) +- [Version 1.0.0 Release](#version-100-release) +- [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) +- [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: 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) +- [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) + - [`Name(help)` and `Description(help)` - set the application name description](#namehelp-and-descriptionhelp---set-the-application-name-description) + - [`Configuration(loader, 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) + - [`ConfigureHelp(HelpOptions)` and `Help(HelpFunc)` - customising help](#configurehelphelpoptions-and-helphelpfunc---customising-help) + - [Injecting values into `Run()` methods](#injecting-values-into-run-methods) + - [Other options](#other-options) ## Version 1.0.0 Release @@ -308,8 +307,8 @@ func main() { ## Hooks: BeforeReset(), BeforeResolve(), BeforeApply(), AfterApply() and the Bind() option -If a node in the grammar has a `BeforeReset(...)`, `BeforeResolve -(...)`, `BeforeApply(...) error` and/or `AfterApply(...) error` method, those +If a node in the CLI, or any of its embedded fields, has a `BeforeReset(...) error`, `BeforeResolve +(...) error`, `BeforeApply(...) error` and/or `AfterApply(...) error` method, those methods will be called before values are reset, before validation/assignment, and after validation/assignment, respectively. @@ -342,40 +341,6 @@ 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. @@ -585,6 +550,7 @@ Both can coexist with standard Tag parsing. | `and:"X,Y,..."` | AND groups for flags. All flags in the group must be used in the same command. When combined with `required`, all flags in the group will be required. | | `prefix:"X"` | Prefix for all sub-flags. | | `envprefix:"X"` | Envar prefix for all sub-flags. | +| `xorprefix:"X"` | Prefix for all sub-flags in XOR/AND groups. | | `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:""`[^1] | 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. | @@ -683,7 +649,7 @@ normal validation. ## Modifying Kong's behaviour -Each Kong parser can be configured via functional options passed to `New(cli interface{}, options...Option)`. +Each Kong parser can be configured via functional options passed to `New(cli any, options...Option)`. The full set of options can be found [here](https://godoc.org/github.com/alecthomas/kong#Option). @@ -741,7 +707,7 @@ All builtin Go types (as well as a bunch of useful stdlib types like `time.Time` 1. `NamedMapper(string, Mapper)` and using the tag key `type:""`. 2. `KindMapper(reflect.Kind, Mapper)`. 3. `TypeMapper(reflect.Type, Mapper)`. -4. `ValueMapper(interface{}, Mapper)`, passing in a pointer to a field of the grammar. +4. `ValueMapper(any, Mapper)`, passing in a pointer to a field of the grammar. ### `ConfigureHelp(HelpOptions)` and `Help(HelpFunc)` - customising help diff --git a/_examples/server/go.mod b/_examples/server/go.mod index b65caba..4a34a65 100644 --- a/_examples/server/go.mod +++ b/_examples/server/go.mod @@ -4,13 +4,13 @@ go 1.13 require ( github.com/alecthomas/colour v0.1.0 - github.com/alecthomas/kong v1.5.1 + github.com/alecthomas/kong v1.6.0 github.com/chzyer/readline v1.5.1 github.com/chzyer/test v1.0.0 // indirect github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect - github.com/gliderlabs/ssh v0.3.7 + github.com/gliderlabs/ssh v0.3.8 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/kr/pty v1.1.8 github.com/mattn/go-isatty v0.0.12 // indirect - golang.org/x/crypto v0.30.0 + golang.org/x/crypto v0.31.0 ) diff --git a/_examples/server/go.sum b/_examples/server/go.sum index 5aea24b..fe021dc 100644 --- a/_examples/server/go.sum +++ b/_examples/server/go.sum @@ -18,6 +18,8 @@ github.com/alecthomas/kong v1.4.0 h1:UL7tzGMnnY0YRMMvJyITIRX1EpO6RbBRZDNcCevy3HA github.com/alecthomas/kong v1.4.0/go.mod h1:p2vqieVMeTAnaC83txKtXe8FLke2X07aruPWXyMPQrU= github.com/alecthomas/kong v1.5.1 h1:9quB93P2aNGXf5C1kWNei85vjBgITNJQA4dSwJQGCOY= github.com/alecthomas/kong v1.5.1/go.mod h1:p2vqieVMeTAnaC83txKtXe8FLke2X07aruPWXyMPQrU= +github.com/alecthomas/kong v1.6.0 h1:mwOzbdMR7uv2vul9J0FU3GYxE7ls/iX1ieMg5WIM6gE= +github.com/alecthomas/kong v1.6.0/go.mod h1:p2vqieVMeTAnaC83txKtXe8FLke2X07aruPWXyMPQrU= github.com/alecthomas/repr v0.1.0/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8= github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA= @@ -46,6 +48,8 @@ github.com/gliderlabs/ssh v0.3.6 h1:ZzjlDa05TcFRICb3anf/dSPN3ewz1Zx6CMLPWgkm3b8= github.com/gliderlabs/ssh v0.3.6/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8= github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE= github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8= +github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= +github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= @@ -92,6 +96,8 @@ golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= golang.org/x/crypto v0.30.0 h1:RwoQn3GkWiMkzlX562cLB7OxWvjH1L8xutO2WoJcRoY= golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= diff --git a/build.go b/build.go index 42d30f0..5d17f53 100644 --- a/build.go +++ b/build.go @@ -9,9 +9,9 @@ import ( // Plugins are dynamically embedded command-line structures. // // Each element in the Plugins list *must* be a pointer to a structure. -type Plugins []interface{} +type Plugins []any -func build(k *Kong, ast interface{}) (app *Application, err error) { +func build(k *Kong, ast any) (app *Application, err error) { v := reflect.ValueOf(ast) iv := reflect.Indirect(v) if v.Kind() != reflect.Ptr || iv.Kind() != reflect.Struct { @@ -71,6 +71,7 @@ func flattenedFields(v reflect.Value, ptag *Tag) (out []flattenedField, err erro // Accumulate prefixes. tag.Prefix = ptag.Prefix + tag.Prefix tag.EnvPrefix = ptag.EnvPrefix + tag.EnvPrefix + tag.XorPrefix = ptag.XorPrefix + tag.XorPrefix // Combine parent vars. tag.Vars = ptag.Vars.CloneWith(tag.Vars) // Command and embedded structs can be pointers, so we hydrate them now. @@ -111,7 +112,7 @@ func flattenedFields(v reflect.Value, ptag *Tag) (out []flattenedField, err erro // 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) { +func buildNode(k *Kong, v reflect.Value, typ NodeType, tag *Tag, seenFlags map[string]bool) (*Node, error) { //nolint:gocyclo node := &Node{ Type: typ, Target: v, @@ -147,6 +148,18 @@ MAIN: } } + if len(tag.Xor) != 0 { + for i := range tag.Xor { + tag.Xor[i] = tag.XorPrefix + tag.Xor[i] + } + } + + if len(tag.And) != 0 { + for i := range tag.And { + tag.And[i] = tag.XorPrefix + tag.And[i] + } + } + // Nested structs are either commands or args, unless they implement the Mapper interface. if field.value.Kind() == reflect.Struct && (tag.Cmd || tag.Arg) && k.registry.ForValue(fv) == nil { typ := CommandNode diff --git a/callbacks.go b/callbacks.go index 1df975d..c1fac81 100644 --- a/callbacks.go +++ b/callbacks.go @@ -19,7 +19,7 @@ func (b bindings) String() string { return "bindings{" + strings.Join(out, ", ") + "}" } -func (b bindings) add(values ...interface{}) bindings { +func (b bindings) add(values ...any) bindings { for _, v := range values { v := v b[reflect.TypeOf(v)] = func() (any, error) { return v, nil } @@ -27,11 +27,11 @@ func (b bindings) add(values ...interface{}) bindings { return b } -func (b bindings) addTo(impl, iface interface{}) { +func (b bindings) addTo(impl, iface any) { b[reflect.TypeOf(iface).Elem()] = func() (any, error) { return impl, nil } } -func (b bindings) addProvider(provider interface{}) error { +func (b bindings) addProvider(provider any) error { pv := reflect.ValueOf(provider) t := pv.Type() if t.Kind() != reflect.Func || t.NumOut() != 2 || t.Out(1) != reflect.TypeOf((*error)(nil)).Elem() { @@ -68,6 +68,33 @@ func getMethod(value reflect.Value, name string) reflect.Value { return method } +// Get methods from the given value and any embedded fields. +func getMethods(value reflect.Value, name string) []reflect.Value { + // Collect all possible receivers + receivers := []reflect.Value{value} + if value.Kind() == reflect.Ptr { + value = value.Elem() + } + if value.Kind() == reflect.Struct { + t := value.Type() + for i := 0; i < value.NumField(); i++ { + field := value.Field(i) + fieldType := t.Field(i) + if fieldType.IsExported() && fieldType.Anonymous { + receivers = append(receivers, field) + } + } + } + // Search all receivers for methods + var methods []reflect.Value + for _, receiver := range receivers { + if method := getMethod(receiver, name); method.IsValid() { + methods = append(methods, method) + } + } + return methods +} + func callFunction(f reflect.Value, bindings bindings) error { if f.Kind() != reflect.Func { return fmt.Errorf("expected function, got %s", f.Type()) diff --git a/config_test.go b/config_test.go index 15d6eec..570f142 100644 --- a/config_test.go +++ b/config_test.go @@ -42,7 +42,7 @@ func TestConfigValidation(t *testing.T) { assert.Error(t, err) } -func makeConfig(t *testing.T, config interface{}) (path string, cleanup func()) { +func makeConfig(t *testing.T, config any) (path string, cleanup func()) { t.Helper() w, err := os.CreateTemp("", "") assert.NoError(t, err) diff --git a/context.go b/context.go index fd168fa..b6a56e3 100644 --- a/context.go +++ b/context.go @@ -102,7 +102,7 @@ func Trace(k *Kong, args []string) (*Context, error) { } // Bind adds bindings to the Context. -func (c *Context) Bind(args ...interface{}) { +func (c *Context) Bind(args ...any) { c.bindings.add(args...) } @@ -111,7 +111,7 @@ func (c *Context) Bind(args ...interface{}) { // This will typically have to be called like so: // // BindTo(impl, (*MyInterface)(nil)) -func (c *Context) BindTo(impl, iface interface{}) { +func (c *Context) BindTo(impl, iface any) { c.bindings.addTo(impl, iface) } @@ -119,7 +119,7 @@ func (c *Context) BindTo(impl, iface interface{}) { // // 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 { +func (c *Context) BindToProvider(provider any) error { return c.bindings.addProvider(provider) } @@ -306,7 +306,7 @@ func (c *Context) AddResolver(resolver Resolver) { } // FlagValue returns the set value of a flag if it was encountered and exists, or its default value. -func (c *Context) FlagValue(flag *Flag) interface{} { +func (c *Context) FlagValue(flag *Flag) any { for _, trace := range c.Path { if trace.Flag == flag { v, ok := c.values[trace.Flag.Value] @@ -572,7 +572,7 @@ func (c *Context) Resolve() error { } // Pick the last resolved value. - var selected interface{} + var selected any for _, resolver := range resolvers { s, err := resolver.Resolve(c, path, flag) if err != nil { @@ -757,7 +757,7 @@ func (e *unknownFlagError) Unwrap() error { return e.Cause } func (e *unknownFlagError) Error() string { return e.Cause.Error() } // Call an arbitrary function filling arguments with bound values. -func (c *Context) Call(fn any, binds ...interface{}) (out []interface{}, err error) { +func (c *Context) Call(fn any, binds ...any) (out []any, err error) { fv := reflect.ValueOf(fn) bindings := c.Kong.bindings.clone().add(binds...).add(c).merge(c.bindings) return callAnyFunction(fv, bindings) @@ -769,7 +769,7 @@ func (c *Context) Call(fn any, binds ...interface{}) (out []interface{}, err err // // 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) RunNode(node *Node, binds ...interface{}) (err error) { +func (c *Context) RunNode(node *Node, binds ...any) (err error) { type targetMethod struct { node *Node method reflect.Value @@ -803,11 +803,6 @@ func (c *Context) RunNode(node *Node, binds ...interface{}) (err error) { if len(methods) == 0 { return fmt.Errorf("no Run() method found in hierarchy of %s", c.Selected().Summary()) } - _, err = c.Apply() - if err != nil { - return err - } - for _, method := range methods { if err = callFunction(method.method, method.binds); err != nil { return err @@ -820,7 +815,7 @@ 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) { +func (c *Context) Run(binds ...any) (err error) { node := c.Selected() if node == nil { if len(c.Path) == 0 { @@ -832,7 +827,9 @@ func (c *Context) Run(binds ...interface{}) (err error) { if method.IsValid() { node = selected } - } else { + } + + if node == nil { return fmt.Errorf("no command selected") } } @@ -1092,7 +1089,7 @@ func checkAndMissing(paths []*Path) error { return nil } -func findPotentialCandidates(needle string, haystack []string, format string, args ...interface{}) error { +func findPotentialCandidates(needle string, haystack []string, format string, args ...any) error { if len(haystack) == 0 { return fmt.Errorf(format, args...) } diff --git a/defaults.go b/defaults.go index f6728d7..9489fb3 100644 --- a/defaults.go +++ b/defaults.go @@ -1,7 +1,7 @@ package kong // ApplyDefaults if they are not already set. -func ApplyDefaults(target interface{}, options ...Option) error { +func ApplyDefaults(target any, options ...Option) error { app, err := New(target, options...) if err != nil { return err diff --git a/global.go b/global.go index d4b3cb5..babe1e1 100644 --- a/global.go +++ b/global.go @@ -5,7 +5,7 @@ import ( ) // Parse constructs a new parser and parses the default command-line. -func Parse(cli interface{}, options ...Option) *Context { +func Parse(cli any, options ...Option) *Context { parser, err := New(cli, options...) if err != nil { panic(err) diff --git a/help.go b/help.go index 26f355d..6363ea2 100644 --- a/help.go +++ b/help.go @@ -386,7 +386,7 @@ func newHelpWriter(ctx *Context, options HelpOptions) *helpWriter { return w } -func (h *helpWriter) Printf(format string, args ...interface{}) { +func (h *helpWriter) Printf(format string, args ...any) { h.Print(fmt.Sprintf(format, args...)) } diff --git a/kong.go b/kong.go index 764a994..b85e145 100644 --- a/kong.go +++ b/kong.go @@ -15,7 +15,7 @@ var ( callbackReturnSignature = reflect.TypeOf((*error)(nil)).Elem() ) -func failField(parent reflect.Value, field reflect.StructField, format string, args ...interface{}) error { +func failField(parent reflect.Value, field reflect.StructField, format string, args ...any) error { name := parent.Type().Name() if name == "" { name = "" @@ -24,7 +24,7 @@ func failField(parent reflect.Value, field reflect.StructField, format string, a } // Must creates a new Parser or panics if there is an error. -func Must(ast interface{}, options ...Option) *Kong { +func Must(ast any, options ...Option) *Kong { k, err := New(ast, options...) if err != nil { panic(err) @@ -76,7 +76,7 @@ type Kong struct { // New creates a new Kong parser on grammar. // // See the README (https://github.com/alecthomas/kong) for usage instructions. -func New(grammar interface{}, options ...Option) (*Kong, error) { +func New(grammar any, options ...Option) (*Kong, error) { k := &Kong{ Exit: os.Exit, Stdout: os.Stdout, @@ -361,16 +361,14 @@ func (k *Kong) applyHook(ctx *Context, name string) error { default: panic("unsupported Path") } - method := getMethod(value, name) - if !method.IsValid() { - continue - } - binds := k.bindings.clone() - binds.add(ctx, trace) - binds.add(trace.Node().Vars().CloneWith(k.vars)) - binds.merge(ctx.bindings) - if err := callFunction(method, binds); err != nil { - return err + for _, method := range getMethods(value, name) { + binds := k.bindings.clone() + binds.add(ctx, trace) + binds.add(trace.Node().Vars().CloneWith(k.vars)) + binds.merge(ctx.bindings) + if err := callFunction(method, binds); err != nil { + return err + } } } // Path[0] will always be the app root. @@ -392,20 +390,18 @@ func (k *Kong) applyHookToDefaultFlags(ctx *Context, node *Node, name string) er if !flag.HasDefault || ctx.values[flag.Value].IsValid() || !flag.Target.IsValid() { continue } - method := getMethod(flag.Target, name) - if !method.IsValid() { - continue - } - path := &Path{Flag: flag} - if err := callFunction(method, binds.clone().add(path)); err != nil { - return next(err) + for _, method := range getMethods(flag.Target, name) { + path := &Path{Flag: flag} + if err := callFunction(method, binds.clone().add(path)); err != nil { + return next(err) + } } } return next(nil) }) } -func formatMultilineMessage(w io.Writer, leaders []string, format string, args ...interface{}) { +func formatMultilineMessage(w io.Writer, leaders []string, format string, args ...any) { lines := strings.Split(strings.TrimRight(fmt.Sprintf(format, args...), "\n"), "\n") leader := "" for _, l := range leaders { @@ -421,25 +417,25 @@ func formatMultilineMessage(w io.Writer, leaders []string, format string, args . } // Printf writes a message to Kong.Stdout with the application name prefixed. -func (k *Kong) Printf(format string, args ...interface{}) *Kong { +func (k *Kong) Printf(format string, args ...any) *Kong { formatMultilineMessage(k.Stdout, []string{k.Model.Name}, format, args...) return k } // Errorf writes a message to Kong.Stderr with the application name prefixed. -func (k *Kong) Errorf(format string, args ...interface{}) *Kong { +func (k *Kong) Errorf(format string, args ...any) *Kong { formatMultilineMessage(k.Stderr, []string{k.Model.Name, "error"}, format, args...) return k } // Fatalf writes a message to Kong.Stderr with the application name prefixed then exits with a non-zero status. -func (k *Kong) Fatalf(format string, args ...interface{}) { +func (k *Kong) Fatalf(format string, args ...any) { k.Errorf(format, args...) k.Exit(1) } // FatalIfErrorf terminates with an error message if err != nil. -func (k *Kong) FatalIfErrorf(err error, args ...interface{}) { +func (k *Kong) FatalIfErrorf(err error, args ...any) { if err == nil { return } diff --git a/kong_test.go b/kong_test.go index 2b52758..a7c03b2 100644 --- a/kong_test.go +++ b/kong_test.go @@ -14,7 +14,7 @@ import ( "github.com/alecthomas/kong" ) -func mustNew(t *testing.T, cli interface{}, options ...kong.Option) *kong.Kong { +func mustNew(t *testing.T, cli any, options ...kong.Option) *kong.Kong { t.Helper() options = append([]kong.Option{ kong.Name("test"), @@ -955,14 +955,12 @@ func TestDefaultEnumValidated(t *testing.T) { } func TestEnvarEnumValidated(t *testing.T) { - restore := tempEnv(map[string]string{ - "FLAG": "invalid", - }) - defer restore() var cli struct { Flag string `env:"FLAG" required:"" enum:"valid"` } - p := mustNew(t, &cli) + p := newEnvParser(t, &cli, envMap{ + "FLAG": "invalid", + }) _, err := p.Parse(nil) assert.EqualError(t, err, "--flag must be one of \"valid\" but got \"invalid\"") } @@ -1234,10 +1232,9 @@ func TestIssue153(t *testing.T) { Ls LsCmd `cmd help:"List paths."` } - p, revert := newEnvParser(t, &cli, envMap{ + p := newEnvParser(t, &cli, envMap{ "CMD_PATHS": "hello", }) - defer revert() _, err := p.Parse([]string{"ls"}) assert.NoError(t, err) assert.Equal(t, []string{"hello"}, cli.Ls.Paths) @@ -1683,7 +1680,7 @@ func TestOptionReturnsErr(t *testing.T) { func TestEnumValidation(t *testing.T) { tests := []struct { name string - cli interface{} + cli any fail bool }{ { @@ -1957,7 +1954,7 @@ func TestVersionFlagShouldStillWork(t *testing.T) { func TestSliceDecoderHelpfulErrorMsg(t *testing.T) { tests := []struct { name string - cli interface{} + cli any args []string err string }{ @@ -2007,7 +2004,7 @@ func TestSliceDecoderHelpfulErrorMsg(t *testing.T) { func TestMapDecoderHelpfulErrorMsg(t *testing.T) { tests := []struct { name string - cli interface{} + cli any args []string expected string }{ @@ -2406,3 +2403,108 @@ func TestProviderMethods(t *testing.T) { err = kctx.Run(t) assert.NoError(t, err) } + +type EmbeddedCallback struct { + Embedded bool +} + +func (e *EmbeddedCallback) AfterApply() error { + e.Embedded = true + return nil +} + +type EmbeddedRoot struct { + EmbeddedCallback + Root bool +} + +func (e *EmbeddedRoot) AfterApply() error { + e.Root = true + return nil +} + +func TestEmbeddedCallbacks(t *testing.T) { + actual := &EmbeddedRoot{} + k := mustNew(t, actual) + _, err := k.Parse(nil) + assert.NoError(t, err) + expected := &EmbeddedRoot{ + EmbeddedCallback: EmbeddedCallback{ + Embedded: true, + }, + Root: true, + } + assert.Equal(t, expected, actual) +} + +type applyCalledOnce struct { + Dev bool +} + +func (c *applyCalledOnce) AfterApply() error { + c.Dev = false + return nil +} + +func (c applyCalledOnce) Run() error { + if c.Dev { + return fmt.Errorf("--dev should not be set") + } + return nil +} + +func TestApplyCalledOnce(t *testing.T) { + cli := &applyCalledOnce{} + kctx, err := mustNew(t, cli).Parse([]string{"--dev"}) + assert.NoError(t, err) + err = kctx.Run() + assert.NoError(t, err) +} + +func TestCustomTypeNoEllipsis(t *testing.T) { + type CLI struct { + Flag []byte `type:"existingfile"` + } + var cli CLI + p := mustNew(t, &cli, kong.Exit(func(int) {})) + w := &strings.Builder{} + p.Stderr = w + p.Stdout = w + _, err := p.Parse([]string{"--help"}) + assert.NoError(t, err) + help := w.String() + assert.NotContains(t, help, "...") +} + +func TestPrefixXorIssue343(t *testing.T) { + type DBConfig struct { + Password string `help:"Password" xor:"password" optional:""` + PasswordFile string `help:"File which content will be used for a password" xor:"password" optional:""` + PasswordCommand string `help:"Command to run to retrieve password" xor:"password" optional:""` + } + + type SourceTargetConfig struct { + Source DBConfig `help:"Database config of source to be copied from" prefix:"source-" xorprefix:"source-" embed:""` + Target DBConfig `help:"Database config of source to be copied from" prefix:"target-" xorprefix:"target-" embed:""` + } + + cli := SourceTargetConfig{} + kctx := mustNew(t, &cli) + _, err := kctx.Parse([]string{"--source-password=foo", "--target-password=bar"}) + assert.NoError(t, err) + _, err = kctx.Parse([]string{"--source-password-file=foo", "--source-password=bar"}) + assert.Error(t, err) +} + +func TestIssue483EmptyRootNodeNoRun(t *testing.T) { + var emptyCLI struct{} + parser, err := kong.New(&emptyCLI) + assert.NoError(t, err) + + kctx, err := parser.Parse([]string{}) + assert.NoError(t, err) + + err = kctx.Run() + assert.Error(t, err) + assert.Contains(t, err.Error(), "no command selected") +} diff --git a/mapper.go b/mapper.go index 584bb00..db3f24e 100644 --- a/mapper.go +++ b/mapper.go @@ -251,7 +251,7 @@ func (r *Registry) RegisterType(typ reflect.Type, mapper Mapper) *Registry { } // RegisterValue registers a Mapper by pointer to the field value. -func (r *Registry) RegisterValue(ptr interface{}, mapper Mapper) *Registry { +func (r *Registry) RegisterValue(ptr any, mapper Mapper) *Registry { key := reflect.ValueOf(ptr) if key.Kind() != reflect.Ptr { panic("expected a pointer") @@ -473,7 +473,7 @@ func mapDecoder(r *Registry) MapperFunc { case string: childScanner = ScanAsType(t.Type, SplitEscaped(v, mapsep)...) - case []map[string]interface{}: + case []map[string]any: for _, m := range v { err := jsonTranscode(m, target.Addr().Interface()) if err != nil { @@ -482,7 +482,7 @@ func mapDecoder(r *Registry) MapperFunc { } return nil - case map[string]interface{}: + case map[string]any: return jsonTranscode(v, target.Addr().Interface()) default: @@ -548,11 +548,11 @@ func sliceDecoder(r *Registry) MapperFunc { case string: childScanner = ScanAsType(t.Type, SplitEscaped(v, sep)...) - case []interface{}: + case []any: return jsonTranscode(v, target.Addr().Interface()) default: - v = []interface{}{v} + v = []any{v} return jsonTranscode(v, target.Addr().Interface()) } } else { @@ -922,7 +922,7 @@ func (f *FileContentFlag) Decode(ctx *DecodeContext) error { //nolint: revive return nil } -func jsonTranscode(in, out interface{}) error { +func jsonTranscode(in, out any) error { data, err := json.Marshal(in) if err != nil { return err diff --git a/model.go b/model.go index 25ffe96..065fcdd 100644 --- a/model.go +++ b/model.go @@ -69,7 +69,7 @@ func (n *Node) Leaf() bool { // Find a command/argument/flag by pointer to its field. // // Returns nil if not found. Panics if ptr is not a pointer. -func (n *Node) Find(ptr interface{}) *Node { +func (n *Node) Find(ptr any) *Node { key := reflect.ValueOf(ptr) if key.Kind() != reflect.Ptr { panic("expected a pointer") @@ -433,7 +433,7 @@ func (f *Flag) FormatPlaceHolder() string { return placeholderHelper.PlaceHolder(f) } tail := "" - if f.Value.IsSlice() && f.Value.Tag.Sep != -1 { + if f.Value.IsSlice() && f.Value.Tag.Sep != -1 && f.Tag.Type == "" { tail += string(f.Value.Tag.Sep) + "..." } if f.PlaceHolder != "" { @@ -446,7 +446,7 @@ func (f *Flag) FormatPlaceHolder() string { return f.Default + tail } if f.Value.IsMap() { - if f.Value.Tag.MapSep != -1 { + if f.Value.Tag.MapSep != -1 && f.Tag.Type == "" { tail = string(f.Value.Tag.MapSep) + "..." } return "KEY=VALUE" + tail diff --git a/options.go b/options.go index 3bc991b..6263202 100644 --- a/options.go +++ b/options.go @@ -79,7 +79,7 @@ type dynamicCommand struct { help string group string tags []string - cmd interface{} + cmd any } // DynamicCommand registers a dynamically constructed command with the root of the CLI. @@ -87,7 +87,7 @@ type dynamicCommand struct { // 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 { +func DynamicCommand(name, help, group string, cmd any, tags ...string) Option { return OptionFunc(func(k *Kong) error { if run := getMethod(reflect.Indirect(reflect.ValueOf(cmd)), "Run"); !run.IsValid() { return fmt.Errorf("kong: DynamicCommand %q must be a type with a 'Run' method; got %T", name, cmd) @@ -156,7 +156,7 @@ func KindMapper(kind reflect.Kind, mapper Mapper) Option { } // ValueMapper registers a mapper to a field value. -func ValueMapper(ptr interface{}, mapper Mapper) Option { +func ValueMapper(ptr any, mapper Mapper) Option { return OptionFunc(func(k *Kong) error { k.registry.RegisterValue(ptr, mapper) return nil @@ -191,7 +191,7 @@ func Writers(stdout, stderr io.Writer) Option { // AfterApply(...) error // // Called before validation/assignment, and immediately after validation/assignment, respectively. -func Bind(args ...interface{}) Option { +func Bind(args ...any) Option { return OptionFunc(func(k *Kong) error { k.bindings.add(args...) return nil @@ -201,7 +201,7 @@ func Bind(args ...interface{}) Option { // BindTo allows binding of implementations to interfaces. // // BindTo(impl, (*iface)(nil)) -func BindTo(impl, iface interface{}) Option { +func BindTo(impl, iface any) Option { return OptionFunc(func(k *Kong) error { k.bindings.addTo(impl, iface) return nil @@ -212,11 +212,11 @@ func BindTo(impl, iface interface{}) Option { // // The provider function must have the signature: // -// func() (interface{}, error) +// func() (any, error) // // This is useful when the Run() function of different commands require different values that may // not all be initialisable from the main() function. -func BindToProvider(provider interface{}) Option { +func BindToProvider(provider any) Option { return OptionFunc(func(k *Kong) error { return k.bindings.addProvider(provider) }) diff --git a/resolver.go b/resolver.go index dca4309..29be1b9 100644 --- a/resolver.go +++ b/resolver.go @@ -14,15 +14,15 @@ type Resolver interface { Validate(app *Application) error // Resolve the value for a Flag. - Resolve(context *Context, parent *Path, flag *Flag) (interface{}, error) + Resolve(context *Context, parent *Path, flag *Flag) (any, error) } // ResolverFunc is a convenience type for non-validating Resolvers. -type ResolverFunc func(context *Context, parent *Path, flag *Flag) (interface{}, error) +type ResolverFunc func(context *Context, parent *Path, flag *Flag) (any, error) var _ Resolver = ResolverFunc(nil) -func (r ResolverFunc) Resolve(context *Context, parent *Path, flag *Flag) (interface{}, error) { //nolint: revive +func (r ResolverFunc) Resolve(context *Context, parent *Path, flag *Flag) (any, error) { //nolint: revive return r(context, parent, flag) } func (r ResolverFunc) Validate(app *Application) error { return nil } //nolint: revive @@ -31,12 +31,12 @@ func (r ResolverFunc) Validate(app *Application) error { return nil } //nolint: // // 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{}{} + values := map[string]any{} err := json.NewDecoder(r).Decode(&values) if err != nil { return nil, err } - var f ResolverFunc = func(context *Context, parent *Path, flag *Flag) (interface{}, error) { + var f ResolverFunc = func(context *Context, parent *Path, flag *Flag) (any, error) { name := strings.ReplaceAll(flag.Name, "-", "_") snakeCaseName := snakeCase(flag.Name) raw, ok := values[name] @@ -47,7 +47,7 @@ func JSON(r io.Reader) (Resolver, error) { } raw = values for _, part := range strings.Split(name, ".") { - if values, ok := raw.(map[string]interface{}); ok { + if values, ok := raw.(map[string]any); ok { raw, ok = values[part] if !ok { return nil, nil diff --git a/resolver_test.go b/resolver_test.go index c1684b6..7ce7ec1 100644 --- a/resolver_test.go +++ b/resolver_test.go @@ -2,7 +2,6 @@ package kong_test import ( "errors" - "os" "reflect" "strings" "testing" @@ -13,23 +12,13 @@ import ( type envMap map[string]string -func tempEnv(env envMap) func() { - for k, v := range env { - os.Setenv(k, v) - } - - return func() { - for k := range env { - os.Unsetenv(k) - } - } -} - -func newEnvParser(t *testing.T, cli interface{}, env envMap, options ...kong.Option) (*kong.Kong, func()) { +func newEnvParser(t *testing.T, cli any, env envMap, options ...kong.Option) *kong.Kong { t.Helper() - restoreEnv := tempEnv(env) + for name, value := range env { + t.Setenv(name, value) + } parser := mustNew(t, cli, options...) - return parser, restoreEnv + return parser } func TestEnvarsFlagBasic(t *testing.T) { @@ -39,7 +28,7 @@ func TestEnvarsFlagBasic(t *testing.T) { Interp string `env:"${kongInterp}"` } kongInterpEnv := "KONG_INTERP" - parser, unsetEnvs := newEnvParser(t, &cli, + parser := newEnvParser(t, &cli, envMap{ "KONG_STRING": "bye", "KONG_SLICE": "5,2,9", @@ -49,7 +38,6 @@ func TestEnvarsFlagBasic(t *testing.T) { "kongInterp": kongInterpEnv, }, ) - defer unsetEnvs() _, err := parser.Parse([]string{}) assert.NoError(t, err) @@ -63,14 +51,13 @@ func TestEnvarsFlagMultiple(t *testing.T) { FirstENVPresent string `env:"KONG_TEST1_1,KONG_TEST1_2"` SecondENVPresent string `env:"KONG_TEST2_1,KONG_TEST2_2"` } - parser, unsetEnvs := newEnvParser(t, &cli, + parser := newEnvParser(t, &cli, envMap{ "KONG_TEST1_1": "value1.1", "KONG_TEST1_2": "value1.2", "KONG_TEST2_2": "value2.2", }, ) - defer unsetEnvs() _, err := parser.Parse([]string{}) assert.NoError(t, err) @@ -82,8 +69,7 @@ func TestEnvarsFlagOverride(t *testing.T) { var cli struct { Flag string `env:"KONG_FLAG"` } - parser, restoreEnv := newEnvParser(t, &cli, envMap{"KONG_FLAG": "bye"}) - defer restoreEnv() + parser := newEnvParser(t, &cli, envMap{"KONG_FLAG": "bye"}) _, err := parser.Parse([]string{"--flag=hello"}) assert.NoError(t, err) @@ -94,8 +80,7 @@ func TestEnvarsTag(t *testing.T) { var cli struct { Slice []int `env:"KONG_NUMBERS"` } - parser, restoreEnv := newEnvParser(t, &cli, envMap{"KONG_NUMBERS": "5,2,9"}) - defer restoreEnv() + parser := newEnvParser(t, &cli, envMap{"KONG_NUMBERS": "5,2,9"}) _, err := parser.Parse([]string{}) assert.NoError(t, err) @@ -109,8 +94,7 @@ func TestEnvarsEnvPrefix(t *testing.T) { var cli struct { Anonymous `envprefix:"KONG_"` } - parser, restoreEnv := newEnvParser(t, &cli, envMap{"KONG_NUMBERS": "1,2,3"}) - defer restoreEnv() + parser := newEnvParser(t, &cli, envMap{"KONG_NUMBERS": "1,2,3"}) _, err := parser.Parse([]string{}) assert.NoError(t, err) @@ -125,8 +109,7 @@ func TestEnvarsEnvPrefixMultiple(t *testing.T) { var cli struct { Anonymous `envprefix:"KONG_"` } - parser, restoreEnv := newEnvParser(t, &cli, envMap{"KONG_NUMBERS1_1": "1,2,3", "KONG_NUMBERS2_2": "5,6,7"}) - defer restoreEnv() + parser := newEnvParser(t, &cli, envMap{"KONG_NUMBERS1_1": "1,2,3", "KONG_NUMBERS2_2": "5,6,7"}) _, err := parser.Parse([]string{}) assert.NoError(t, err) @@ -144,8 +127,7 @@ func TestEnvarsNestedEnvPrefix(t *testing.T) { var cli struct { Anonymous `envprefix:"KONG_"` } - parser, restoreEnv := newEnvParser(t, &cli, envMap{"KONG_ANON_STRING": "abc"}) - defer restoreEnv() + parser := newEnvParser(t, &cli, envMap{"KONG_ANON_STRING": "abc"}) _, err := parser.Parse([]string{}) assert.NoError(t, err) @@ -156,15 +138,13 @@ func TestEnvarsWithDefault(t *testing.T) { var cli struct { Flag string `env:"KONG_FLAG" default:"default"` } - parser, restoreEnv := newEnvParser(t, &cli, envMap{}) - defer restoreEnv() + parser := newEnvParser(t, &cli, envMap{}) _, err := parser.Parse(nil) assert.NoError(t, err) assert.Equal(t, "default", cli.Flag) - parser, restoreEnv = newEnvParser(t, &cli, envMap{"KONG_FLAG": "moo"}) - defer restoreEnv() + parser = newEnvParser(t, &cli, envMap{"KONG_FLAG": "moo"}) _, err = parser.Parse(nil) assert.NoError(t, err) assert.Equal(t, "moo", cli.Flag) @@ -194,7 +174,7 @@ func TestEnv(t *testing.T) { } // With the prefix - parser, unsetEnvs := newEnvParser(t, &cli, envMap{ + parser := newEnvParser(t, &cli, envMap{ "KONG_ONE_FLAG": "one", "KONG_TWO_FLAG": "two", "KONG_THREE_FLAG": "three", @@ -202,14 +182,13 @@ func TestEnv(t *testing.T) { "KONG_FIVE": "true", "KONG_SIX": "true", }, kong.DefaultEnvars("KONG")) - defer unsetEnvs() _, err := parser.Parse(nil) assert.NoError(t, err) assert.Equal(t, expected, cli) // Without the prefix - parser, unsetEnvs = newEnvParser(t, &cli, envMap{ + parser = newEnvParser(t, &cli, envMap{ "ONE_FLAG": "one", "TWO_FLAG": "two", "THREE_FLAG": "three", @@ -217,7 +196,6 @@ func TestEnv(t *testing.T) { "FIVE": "true", "SIX": "true", }, kong.DefaultEnvars("")) - defer unsetEnvs() _, err = parser.Parse(nil) assert.NoError(t, err) @@ -281,10 +259,10 @@ func TestResolversWithMappers(t *testing.T) { Flag string `env:"KONG_MOO" type:"upper"` } - restoreEnv := tempEnv(envMap{"KONG_MOO": "meow"}) - defer restoreEnv() + t.Setenv("KONG_MOO", "meow") - parser := mustNew(t, &cli, + parser := newEnvParser(t, &cli, + envMap{"KONG_MOO": "meow"}, kong.NamedMapper("upper", testUppercaseMapper{}), ) _, err := parser.Parse([]string{}) @@ -297,7 +275,7 @@ func TestResolverWithBool(t *testing.T) { Bool bool } - var resolver kong.ResolverFunc = func(context *kong.Context, parent *kong.Path, flag *kong.Flag) (interface{}, error) { + var resolver kong.ResolverFunc = func(context *kong.Context, parent *kong.Path, flag *kong.Flag) (any, error) { if flag.Name == "bool" { return true, nil } @@ -316,14 +294,14 @@ func TestLastResolverWins(t *testing.T) { Int []int } - var first kong.ResolverFunc = func(context *kong.Context, parent *kong.Path, flag *kong.Flag) (interface{}, error) { + var first kong.ResolverFunc = func(context *kong.Context, parent *kong.Path, flag *kong.Flag) (any, error) { if flag.Name == "int" { return 1, nil } return nil, nil } - var second kong.ResolverFunc = func(context *kong.Context, parent *kong.Path, flag *kong.Flag) (interface{}, error) { + var second kong.ResolverFunc = func(context *kong.Context, parent *kong.Path, flag *kong.Flag) (any, error) { if flag.Name == "int" { return 2, nil } @@ -340,7 +318,7 @@ func TestResolverSatisfiesRequired(t *testing.T) { var cli struct { Int int `required` } - var resolver kong.ResolverFunc = func(context *kong.Context, parent *kong.Path, flag *kong.Flag) (interface{}, error) { + var resolver kong.ResolverFunc = func(context *kong.Context, parent *kong.Path, flag *kong.Flag) (any, error) { if flag.Name == "int" { return 1, nil } @@ -358,7 +336,7 @@ func TestResolverTriggersHooks(t *testing.T) { Flag hookValue } - var first kong.ResolverFunc = func(context *kong.Context, parent *kong.Path, flag *kong.Flag) (interface{}, error) { + var first kong.ResolverFunc = func(context *kong.Context, parent *kong.Path, flag *kong.Flag) (any, error) { if flag.Name == "flag" { return "one", nil } @@ -377,7 +355,7 @@ type validatingResolver struct { } func (v *validatingResolver) Validate(app *kong.Application) error { return v.err } -func (v *validatingResolver) Resolve(context *kong.Context, parent *kong.Path, flag *kong.Flag) (interface{}, error) { +func (v *validatingResolver) Resolve(context *kong.Context, parent *kong.Path, flag *kong.Flag) (any, error) { return nil, nil } diff --git a/scanner.go b/scanner.go index c8a8bd6..262d16f 100644 --- a/scanner.go +++ b/scanner.go @@ -41,7 +41,7 @@ func (t TokenType) String() string { // Token created by Scanner. type Token struct { - Value interface{} + Value any Type TokenType } @@ -171,7 +171,7 @@ func (s *Scanner) PopValue(context string) (Token, error) { // PopValueInto pops a value token into target or returns an error. // // "context" is used to assist the user if the value can not be popped, eg. "expected value but got " -func (s *Scanner) PopValueInto(context string, target interface{}) error { +func (s *Scanner) PopValueInto(context string, target any) error { t, err := s.PopValue(context) if err != nil { return err @@ -204,13 +204,13 @@ func (s *Scanner) Peek() Token { } // Push an untyped Token onto the front of the Scanner. -func (s *Scanner) Push(arg interface{}) *Scanner { +func (s *Scanner) Push(arg any) *Scanner { s.PushToken(Token{Value: arg}) return s } // PushTyped pushes a typed token onto the front of the Scanner. -func (s *Scanner) PushTyped(arg interface{}, typ TokenType) *Scanner { +func (s *Scanner) PushTyped(arg any, typ TokenType) *Scanner { s.PushToken(Token{Value: arg, Type: typ}) return s } diff --git a/tag.go b/tag.go index 226171b..a2bc4a9 100644 --- a/tag.go +++ b/tag.go @@ -48,6 +48,7 @@ type Tag struct { Vars Vars Prefix string // Optional prefix on anonymous structs. All sub-flags will have this prefix. EnvPrefix string + XorPrefix string // Optional prefix on XOR/AND groups. Embed bool Aliases []string Negatable string @@ -268,6 +269,7 @@ func hydrateTag(t *Tag, typ reflect.Type) error { //nolint: gocyclo } t.Prefix = t.Get("prefix") t.EnvPrefix = t.Get("envprefix") + t.XorPrefix = t.Get("xorprefix") t.Embed = t.Has("embed") if t.Has("negatable") { if !isBool && !isBoolPtr {