From 2ab5733f117949ba9d1e24c1123d9c977d1808e6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 10 Apr 2024 23:12:03 +1000 Subject: [PATCH 01/81] chore(deps): update all non-major dependencies (#424) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- _examples/server/go.mod | 2 +- _examples/server/go.sum | 6 ++++++ bin/{.go-1.22.1.pkg => .go-1.22.2.pkg} | 0 bin/go | 2 +- bin/gofmt | 2 +- go.mod | 2 +- go.sum | 2 ++ 7 files changed, 12 insertions(+), 4 deletions(-) rename bin/{.go-1.22.1.pkg => .go-1.22.2.pkg} (100%) diff --git a/_examples/server/go.mod b/_examples/server/go.mod index b854201..d216f01 100644 --- a/_examples/server/go.mod +++ b/_examples/server/go.mod @@ -12,5 +12,5 @@ require ( 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.21.0 + golang.org/x/crypto v0.22.0 ) diff --git a/_examples/server/go.sum b/_examples/server/go.sum index e503440..bd8ec62 100644 --- a/_examples/server/go.sum +++ b/_examples/server/go.sum @@ -65,6 +65,8 @@ golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1m golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= 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/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -100,6 +102,8 @@ golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -114,6 +118,8 @@ golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= +golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q= +golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= diff --git a/bin/.go-1.22.1.pkg b/bin/.go-1.22.2.pkg similarity index 100% rename from bin/.go-1.22.1.pkg rename to bin/.go-1.22.2.pkg diff --git a/bin/go b/bin/go index 2b44c98..d9bfd88 120000 --- a/bin/go +++ b/bin/go @@ -1 +1 @@ -.go-1.22.1.pkg \ No newline at end of file +.go-1.22.2.pkg \ No newline at end of file diff --git a/bin/gofmt b/bin/gofmt index 2b44c98..d9bfd88 120000 --- a/bin/gofmt +++ b/bin/gofmt @@ -1 +1 @@ -.go-1.22.1.pkg \ No newline at end of file +.go-1.22.2.pkg \ No newline at end of file diff --git a/go.mod b/go.mod index aee9b0d..8cc06b5 100644 --- a/go.mod +++ b/go.mod @@ -1,7 +1,7 @@ module github.com/alecthomas/kong require ( - github.com/alecthomas/assert/v2 v2.6.0 + github.com/alecthomas/assert/v2 v2.8.1 github.com/alecthomas/repr v0.4.0 ) diff --git a/go.sum b/go.sum index ec451a0..6e6441f 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,8 @@ github.com/alecthomas/assert/v2 v2.5.0 h1:OJKYg53BQx06/bMRBSPDCO49CbCDNiUQXwdoNr github.com/alecthomas/assert/v2 v2.5.0/go.mod h1:fw5suVxB+wfYJ3291t0hRTqtGzFYdSwstnRQdaQx2DM= github.com/alecthomas/assert/v2 v2.6.0 h1:o3WJwILtexrEUk3cUVal3oiQY2tfgr/FHWiz/v2n4FU= github.com/alecthomas/assert/v2 v2.6.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= +github.com/alecthomas/assert/v2 v2.8.1 h1:YCxnYR6jjpfnEK5AK5SysALKdUEBPGH4Y7As6tBnDw0= +github.com/alecthomas/assert/v2 v2.8.1/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= github.com/alecthomas/repr v0.3.0 h1:NeYzUPfjjlqHY4KtzgKJiWd6sVq2eNUPTi34PiFGjY8= github.com/alecthomas/repr v0.3.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= From 38e5c6198ed71153d8d3dc77d0ef7d75b19b679a Mon Sep 17 00:00:00 2001 From: Alex Broad <46813168+alex-broad@users.noreply.github.com> Date: Mon, 27 May 2024 12:02:24 +0100 Subject: [PATCH 02/81] Update help example to use vars (#429) --- _examples/shell/help/main.go | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/_examples/shell/help/main.go b/_examples/shell/help/main.go index 56b8ce6..4d1d154 100644 --- a/_examples/shell/help/main.go +++ b/_examples/shell/help/main.go @@ -7,14 +7,16 @@ import ( ) var cli struct { - Flag flagWithHelp `help:"Regular flag help"` + Flag flagWithHelp `help:"${flag_help}"` Echo commandWithHelp `cmd:"" help:"Regular command help"` } type flagWithHelp bool -func (f *flagWithHelp) Help() string { - return "🏁 additional flag help" +// See https://github.com/alecthomas/kong?tab=readme-ov-file#variable-interpolation +var vars = kong.Vars{ + "flag_help": "Extended flag help that might be too long for directly " + + "including in the struct tag field", } type commandWithHelp struct { @@ -41,7 +43,8 @@ func main() { kong.ConfigureHelp(kong.HelpOptions{ Compact: true, Summary: false, - })) + }), + vars) switch ctx.Command() { case "echo ": fmt.Println(cli.Echo.Msg) From d315006dcaba7a02249e1ad151962365410b601e Mon Sep 17 00:00:00 2001 From: Ben Weintraub Date: Tue, 28 May 2024 15:41:28 -0700 Subject: [PATCH 03/81] Fix reference to non-existent function in README (#430) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 22e00f0..916ca6f 100644 --- a/README.md +++ b/README.md @@ -733,7 +733,7 @@ All builtin Go types (as well as a bunch of useful stdlib types like `time.Time` The default help output is usually sufficient, but if not there are two solutions. 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. +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 `DefaultHelpPrinter` for an example. 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. From fcb5e05c0706be6be5027c32691c29d61dc4e6d4 Mon Sep 17 00:00:00 2001 From: Bob Lail Date: Fri, 5 Jul 2024 05:48:33 -0700 Subject: [PATCH 04/81] fix: When a Grammar combines flags with passthrough args, see if an unrecognized flag may be treated as a positional argument (#435) * ci: Add a test for positional args that are passthrough on a command that isn't passthrough * fix: When a Grammar combines flags with passthrough args, see if an unrecognized flag may be treated as a positional argument Given a grammar like this: ```golang var cli struct { Args []string `arg:"" optional:"" passthrough:""` } ``` The first positional argument implies that it was preceded by `--`, so subsequent flags are not parsed. If Kong parses `cli 1 --unknown 3`, it will populate `Args` with `[]string{"1", "--unknown", "3"}`. However, if Kong parses `cli --unknown 2 3`, it will fail saying that `--unknown` is an unrecognized flag. This commit changes the parser so that if an unknown flag _could_ be treated as the first passthrough argument, it is. After this change, if Kong parses `cli --unknown 2 3`, it will populate `Args` with `[]string{"--unknown", "2", "3"}`. * ci: Skip the `maintidx` linter for `trace()` --- .golangci.yml | 4 ++++ context.go | 26 +++++++++++++++++++++++--- kong_test.go | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 75 insertions(+), 3 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index e8980bf..2c5bfe3 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -87,3 +87,7 @@ issues: # Duplicate words are okay in tests. - linters: [dupword] path: _test\.go + + - linters: [maintidx] + path: context\.go + text: 'Function name: trace' diff --git a/context.go b/context.go index b2bfea6..4f82813 100644 --- a/context.go +++ b/context.go @@ -420,12 +420,22 @@ func (c *Context) trace(node *Node) (err error) { //nolint: gocyclo case FlagToken: if err := c.parseFlag(flags, token.String()); err != nil { - return err + if isUnknownFlagError(err) && positional < len(node.Positional) && node.Positional[positional].Passthrough { + c.scan.Pop() + c.scan.PushTyped(token.String(), PositionalArgumentToken) + } else { + return err + } } case ShortFlagToken: if err := c.parseFlag(flags, token.String()); err != nil { - return err + if isUnknownFlagError(err) && positional < len(node.Positional) && node.Positional[positional].Passthrough { + c.scan.Pop() + c.scan.PushTyped(token.String(), PositionalArgumentToken) + } else { + return err + } } case FlagValueToken: @@ -728,9 +738,19 @@ func (c *Context) parseFlag(flags []*Flag, match string) (err error) { c.Path = append(c.Path, &Path{Flag: flag}) return nil } - return findPotentialCandidates(match, candidates, "unknown flag %s", match) + return &unknownFlagError{Cause: findPotentialCandidates(match, candidates, "unknown flag %s", match)} } +func isUnknownFlagError(err error) bool { + var unknown *unknownFlagError + return errors.As(err, &unknown) +} + +type unknownFlagError struct{ Cause error } + +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) { fv := reflect.ValueOf(fn) diff --git a/kong_test.go b/kong_test.go index 0e9fa9a..f90f86d 100644 --- a/kong_test.go +++ b/kong_test.go @@ -1526,6 +1526,54 @@ func TestEnumValidation(t *testing.T) { } } +func TestPassthroughArgs(t *testing.T) { + tests := []struct { + name string + args []string + flag string + cmdArgs []string + }{ + { + "NoArgs", + []string{}, + "", + []string(nil), + }, + { + "RecognizedFlagAndArgs", + []string{"--flag", "foobar", "something"}, + "foobar", + []string{"something"}, + }, + { + "DashDashBeforeRecognizedFlag", + []string{"--", "--flag", "foobar"}, + "", + []string{"--flag", "foobar"}, + }, + { + "UnrecognizedFlagAndArgs", + []string{"--unrecognized-flag", "something"}, + "", + []string{"--unrecognized-flag", "something"}, + }, + } + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + var cli struct { + Flag string + Args []string `arg:"" optional:"" passthrough:""` + } + p := mustNew(t, &cli) + _, err := p.Parse(test.args) + assert.NoError(t, err) + assert.Equal(t, test.flag, cli.Flag) + assert.Equal(t, test.cmdArgs, cli.Args) + }) + } +} + func TestPassthroughCmd(t *testing.T) { tests := []struct { name string From 605cdd64a921c795cab5abc722861d3fd5e2645d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 5 Jul 2024 22:49:21 +1000 Subject: [PATCH 05/81] chore(deps): update all non-major dependencies (#425) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- _examples/server/go.mod | 2 +- _examples/server/go.sum | 27 ++++++++++++++++++++++++++ bin/{.go-1.22.2.pkg => .go-1.22.5.pkg} | 0 bin/go | 2 +- bin/gofmt | 2 +- go.mod | 2 +- go.sum | 2 ++ 7 files changed, 33 insertions(+), 4 deletions(-) rename bin/{.go-1.22.2.pkg => .go-1.22.5.pkg} (100%) diff --git a/_examples/server/go.mod b/_examples/server/go.mod index d216f01..1feb248 100644 --- a/_examples/server/go.mod +++ b/_examples/server/go.mod @@ -12,5 +12,5 @@ require ( 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.22.0 + golang.org/x/crypto v0.25.0 ) diff --git a/_examples/server/go.sum b/_examples/server/go.sum index bd8ec62..bf1cd72 100644 --- a/_examples/server/go.sum +++ b/_examples/server/go.sum @@ -36,6 +36,7 @@ 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/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= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= @@ -56,6 +57,7 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU= golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= @@ -67,8 +69,14 @@ golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= +golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= 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= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= @@ -77,10 +85,15 @@ golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfS golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -95,6 +108,7 @@ golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= @@ -104,6 +118,10 @@ golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -111,6 +129,7 @@ golang.org/x/term v0.1.0 h1:g6Z6vPFA9dYBAF7DWcH6sCcOntplXsDKcliusYijMlw= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE= @@ -120,6 +139,9 @@ golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q= golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= +golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -127,9 +149,14 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/bin/.go-1.22.2.pkg b/bin/.go-1.22.5.pkg similarity index 100% rename from bin/.go-1.22.2.pkg rename to bin/.go-1.22.5.pkg diff --git a/bin/go b/bin/go index d9bfd88..5c26cb9 120000 --- a/bin/go +++ b/bin/go @@ -1 +1 @@ -.go-1.22.2.pkg \ No newline at end of file +.go-1.22.5.pkg \ No newline at end of file diff --git a/bin/gofmt b/bin/gofmt index d9bfd88..5c26cb9 120000 --- a/bin/gofmt +++ b/bin/gofmt @@ -1 +1 @@ -.go-1.22.2.pkg \ No newline at end of file +.go-1.22.5.pkg \ No newline at end of file diff --git a/go.mod b/go.mod index 8cc06b5..dad659b 100644 --- a/go.mod +++ b/go.mod @@ -1,7 +1,7 @@ module github.com/alecthomas/kong require ( - github.com/alecthomas/assert/v2 v2.8.1 + github.com/alecthomas/assert/v2 v2.10.0 github.com/alecthomas/repr v0.4.0 ) diff --git a/go.sum b/go.sum index 6e6441f..fe9dd12 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,8 @@ github.com/alecthomas/assert/v2 v2.6.0 h1:o3WJwILtexrEUk3cUVal3oiQY2tfgr/FHWiz/v github.com/alecthomas/assert/v2 v2.6.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= github.com/alecthomas/assert/v2 v2.8.1 h1:YCxnYR6jjpfnEK5AK5SysALKdUEBPGH4Y7As6tBnDw0= github.com/alecthomas/assert/v2 v2.8.1/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= +github.com/alecthomas/assert/v2 v2.10.0 h1:jjRCHsj6hBJhkmhznrCzoNpbA3zqy0fYiUcYZP/GkPY= +github.com/alecthomas/assert/v2 v2.10.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= github.com/alecthomas/repr v0.3.0 h1:NeYzUPfjjlqHY4KtzgKJiWd6sVq2eNUPTi34PiFGjY8= github.com/alecthomas/repr v0.3.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= From e864bb02209592cb6e8259b74d325edf4236f47c Mon Sep 17 00:00:00 2001 From: Leo Antunes Date: Fri, 5 Jul 2024 14:51:38 +0200 Subject: [PATCH 06/81] feat: allow non-structs to be used as commands (#428) * feat: allow non-structs to be used as commands This small MR allows using the func-to-interface trick to implement a command (see commandFunc in kong_test.go). This is useful e.g. for commands that have no flags or arguments of their own, but instead receive all required information via bound parameters. * fix: check DynamicCommand is runnable when adding --- build.go | 3 +++ context.go | 2 +- kong_test.go | 18 +++++++++++++++++- options.go | 4 ++++ 4 files changed, 25 insertions(+), 2 deletions(-) diff --git a/build.go b/build.go index 1cfe1c3..aed1f72 100644 --- a/build.go +++ b/build.go @@ -51,6 +51,9 @@ type flattenedField struct { func flattenedFields(v reflect.Value, ptag *Tag) (out []flattenedField, err error) { v = reflect.Indirect(v) + if v.Kind() != reflect.Struct { + return out, nil + } for i := 0; i < v.NumField(); i++ { ft := v.Type().Field(i) fv := v.Field(i) diff --git a/context.go b/context.go index 4f82813..03044ec 100644 --- a/context.go +++ b/context.go @@ -903,7 +903,7 @@ func checkMissingChildren(node *Node) error { if len(missing) == 1 { return fmt.Errorf("expected %s", missing[0]) } - return fmt.Errorf("expected one of %s", strings.Join(missing, ", ")) + return fmt.Errorf("expected one of %s", strings.Join(missing, ", ")) } // If we're missing any positionals and they're required, return an error. diff --git a/kong_test.go b/kong_test.go index f90f86d..c4ed7f6 100644 --- a/kong_test.go +++ b/kong_test.go @@ -1297,6 +1297,12 @@ func (d *dynamicCommand) Run() error { return nil } +type commandFunc func() error + +func (cf commandFunc) Run() error { + return cf() +} + func TestDynamicCommands(t *testing.T) { cli := struct { One struct{} `cmd:"one"` @@ -1304,9 +1310,12 @@ func TestDynamicCommands(t *testing.T) { help := &strings.Builder{} two := &dynamicCommand{} three := &dynamicCommand{} + fourRan := false + four := commandFunc(func() error { fourRan = true; return nil }) p := mustNew(t, &cli, kong.DynamicCommand("two", "", "", &two), kong.DynamicCommand("three", "", "", three, "hidden"), + kong.DynamicCommand("four", "", "", &four), kong.Writers(help, help), kong.Exit(func(int) {})) kctx, err := p.Parse([]string{"two", "--flag=flag"}) @@ -1317,8 +1326,15 @@ func TestDynamicCommands(t *testing.T) { assert.NoError(t, err) assert.True(t, two.ran) + kctx, err = p.Parse([]string{"four"}) + assert.NoError(t, err) + assert.False(t, fourRan) + err = kctx.Run() + assert.NoError(t, err) + assert.True(t, fourRan) + _, err = p.Parse([]string{"--help"}) - assert.EqualError(t, err, `expected one of "one", "two"`) + assert.EqualError(t, err, `expected one of "one", "two", "four"`) assert.NotContains(t, help.String(), "three", help.String()) } diff --git a/options.go b/options.go index d01aeec..3ab383b 100644 --- a/options.go +++ b/options.go @@ -89,6 +89,10 @@ type dynamicCommand struct { // "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 { + 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) + } + k.dynamicCommands = append(k.dynamicCommands, &dynamicCommand{ name: name, help: help, From 5f9c5cc822bdb888a3671c44d4688a6f602ecb90 Mon Sep 17 00:00:00 2001 From: Mitar Date: Fri, 5 Jul 2024 14:52:45 +0200 Subject: [PATCH 07/81] Remove extra newline when message contains trailing newlines. (#387) --- kong.go | 2 +- kong_test.go | 23 ++++++++++++++++++----- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/kong.go b/kong.go index 76eaefe..19ff88e 100644 --- a/kong.go +++ b/kong.go @@ -373,7 +373,7 @@ func (k *Kong) applyHookToDefaultFlags(ctx *Context, node *Node, name string) er } func formatMultilineMessage(w io.Writer, leaders []string, format string, args ...interface{}) { - lines := strings.Split(fmt.Sprintf(format, args...), "\n") + lines := strings.Split(strings.TrimRight(fmt.Sprintf(format, args...), "\n"), "\n") leader := "" for _, l := range leaders { if l == "" { diff --git a/kong_test.go b/kong_test.go index c4ed7f6..f3fb869 100644 --- a/kong_test.go +++ b/kong_test.go @@ -593,11 +593,24 @@ func TestSliceWithDisabledSeparator(t *testing.T) { } func TestMultilineMessage(t *testing.T) { - w := &bytes.Buffer{} - var cli struct{} - p := mustNew(t, &cli, kong.Writers(w, w)) - p.Printf("hello\nworld") - assert.Equal(t, "test: hello\n world\n", w.String()) + tests := []struct { + name string + text string + want string + }{ + {"Simple", "hello\nworld", "test: hello\n world\n"}, + {"WithNewline", "hello\nworld\n", "test: hello\n world\n"}, + } + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + w := &bytes.Buffer{} + var cli struct{} + p := mustNew(t, &cli, kong.Writers(w, w)) + p.Printf(test.text) + assert.Equal(t, test.want, w.String()) + }) + } } type cmdWithRun struct { From ff6d5ba7d577f672bed22e3aac4a235114702f19 Mon Sep 17 00:00:00 2001 From: Camilla Date: Thu, 8 Aug 2024 08:58:22 +0200 Subject: [PATCH 08/81] Feature: Add xand tag (#442) * Feat: Add xand group and check for missing * Fix: Split and combine err in TestMultiand for consistency * Feat: Check missing required flags in xand groups * Feat: Handle combined xor and xand * Docs: Add info about combined xand and required use * Docs: Fix language error in xand description Co-authored-by: Stautis * Feat: Rename xand to and * Refactor: Switch from fmt.Sprintf to err.Error * Refactor: Get requiredAndGroup map in separate function --------- Co-authored-by: Stautis --- README.md | 5 ++- build.go | 1 + context.go | 79 ++++++++++++++++++++++++++++++++++++- go.sum | 10 ----- kong_test.go | 109 +++++++++++++++++++++++++++++++++++++++++++++++++++ model.go | 1 + tag.go | 4 ++ 7 files changed, 195 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 916ca6f..cbe6224 100644 --- a/README.md +++ b/README.md @@ -554,7 +554,7 @@ Tags can be in two forms: Both can coexist with standard Tag parsing. | 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. | @@ -566,7 +566,7 @@ Both can coexist with standard Tag parsing. | `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). | +| `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. | @@ -577,6 +577,7 @@ Both can coexist with standard Tag parsing. | `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. | +| `and:"X,Y,..."` | Exclsuive 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. | | `set:"K=V"` | Set a variable for expansion by child elements. Multiples can occur. | diff --git a/build.go b/build.go index aed1f72..18048e0 100644 --- a/build.go +++ b/build.go @@ -323,6 +323,7 @@ func buildField(k *Kong, node *Node, v reflect.Value, ft reflect.StructField, fv Envs: tag.Envs, Group: buildGroupForKey(k, tag.Group), Xor: tag.Xor, + And: tag.And, Hidden: tag.Hidden, } value.Flag = flag diff --git a/context.go b/context.go index 03044ec..f5239c7 100644 --- a/context.go +++ b/context.go @@ -259,7 +259,7 @@ func (c *Context) Validate() error { //nolint: gocyclo if err := checkMissingPositionals(positionals, node.Positional); err != nil { return err } - if err := checkXorDuplicates(c.Path); err != nil { + if err := checkXorDuplicatedAndAndMissing(c.Path); err != nil { return err } @@ -831,23 +831,35 @@ func (c *Context) PrintUsage(summary bool) error { func checkMissingFlags(flags []*Flag) error { xorGroupSet := map[string]bool{} xorGroup := map[string][]string{} + andGroupSet := map[string]bool{} + andGroup := map[string][]string{} missing := []string{} + andGroupRequired := getRequiredAndGroupMap(flags) for _, flag := range flags { + for _, and := range flag.And { + flag.Required = andGroupRequired[and] + } if flag.Set { for _, xor := range flag.Xor { xorGroupSet[xor] = true } + for _, and := range flag.And { + andGroupSet[and] = true + } } if !flag.Required || flag.Set { continue } - if len(flag.Xor) > 0 { + if len(flag.Xor) > 0 || len(flag.And) > 0 { for _, xor := range flag.Xor { if xorGroupSet[xor] { continue } xorGroup[xor] = append(xorGroup[xor], flag.Summary()) } + for _, and := range flag.And { + andGroup[and] = append(andGroup[and], flag.Summary()) + } } else { missing = append(missing, flag.Summary()) } @@ -857,6 +869,11 @@ func checkMissingFlags(flags []*Flag) error { missing = append(missing, strings.Join(flags, " or ")) } } + for _, flags := range andGroup { + if len(flags) > 1 { + missing = append(missing, strings.Join(flags, " and ")) + } + } if len(missing) == 0 { return nil @@ -867,6 +884,18 @@ func checkMissingFlags(flags []*Flag) error { return fmt.Errorf("missing flags: %s", strings.Join(missing, ", ")) } +func getRequiredAndGroupMap(flags []*Flag) map[string]bool { + andGroupRequired := map[string]bool{} + for _, flag := range flags { + for _, and := range flag.And { + if flag.Required { + andGroupRequired[and] = true + } + } + } + return andGroupRequired +} + func checkMissingChildren(node *Node) error { missing := []string{} @@ -977,6 +1006,20 @@ func checkPassthroughArg(target reflect.Value) bool { } } +func checkXorDuplicatedAndAndMissing(paths []*Path) error { + errs := []string{} + if err := checkXorDuplicates(paths); err != nil { + errs = append(errs, err.Error()) + } + if err := checkAndMissing(paths); err != nil { + errs = append(errs, err.Error()) + } + if len(errs) > 0 { + return fmt.Errorf(strings.Join(errs, ", ")) + } + return nil +} + func checkXorDuplicates(paths []*Path) error { for _, path := range paths { seen := map[string]*Flag{} @@ -995,6 +1038,38 @@ func checkXorDuplicates(paths []*Path) error { return nil } +func checkAndMissing(paths []*Path) error { + for _, path := range paths { + missingMsgs := []string{} + andGroups := map[string][]*Flag{} + for _, flag := range path.Flags { + for _, and := range flag.And { + andGroups[and] = append(andGroups[and], flag) + } + } + for _, flags := range andGroups { + oneSet := false + notSet := []*Flag{} + flagNames := []string{} + for _, flag := range flags { + flagNames = append(flagNames, flag.Name) + if flag.Set { + oneSet = true + } else { + notSet = append(notSet, flag) + } + } + if len(notSet) > 0 && oneSet { + missingMsgs = append(missingMsgs, fmt.Sprintf("--%s must be used together", strings.Join(flagNames, " and --"))) + } + } + if len(missingMsgs) > 0 { + return fmt.Errorf("%s", strings.Join(missingMsgs, ", ")) + } + } + return nil +} + func findPotentialCandidates(needle string, haystack []string, format string, args ...interface{}) error { if len(haystack) == 0 { return fmt.Errorf(format, args...) diff --git a/go.sum b/go.sum index fe9dd12..c65b342 100644 --- a/go.sum +++ b/go.sum @@ -1,15 +1,5 @@ -github.com/alecthomas/assert/v2 v2.4.1 h1:mwPZod/d35nlaCppr6sFP0rbCL05WH9fIo7lvsf47zo= -github.com/alecthomas/assert/v2 v2.4.1/go.mod h1:fw5suVxB+wfYJ3291t0hRTqtGzFYdSwstnRQdaQx2DM= -github.com/alecthomas/assert/v2 v2.5.0 h1:OJKYg53BQx06/bMRBSPDCO49CbCDNiUQXwdoNrt6x5w= -github.com/alecthomas/assert/v2 v2.5.0/go.mod h1:fw5suVxB+wfYJ3291t0hRTqtGzFYdSwstnRQdaQx2DM= -github.com/alecthomas/assert/v2 v2.6.0 h1:o3WJwILtexrEUk3cUVal3oiQY2tfgr/FHWiz/v2n4FU= -github.com/alecthomas/assert/v2 v2.6.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= -github.com/alecthomas/assert/v2 v2.8.1 h1:YCxnYR6jjpfnEK5AK5SysALKdUEBPGH4Y7As6tBnDw0= -github.com/alecthomas/assert/v2 v2.8.1/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= github.com/alecthomas/assert/v2 v2.10.0 h1:jjRCHsj6hBJhkmhznrCzoNpbA3zqy0fYiUcYZP/GkPY= github.com/alecthomas/assert/v2 v2.10.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= -github.com/alecthomas/repr v0.3.0 h1:NeYzUPfjjlqHY4KtzgKJiWd6sVq2eNUPTi34PiFGjY8= -github.com/alecthomas/repr v0.3.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= 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/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= diff --git a/kong_test.go b/kong_test.go index f3fb869..0f0bb86 100644 --- a/kong_test.go +++ b/kong_test.go @@ -4,6 +4,7 @@ import ( "bytes" "errors" "fmt" + "sort" "strings" "testing" @@ -919,6 +920,21 @@ func TestXor(t *testing.T) { assert.NoError(t, err) } +func TestAnd(t *testing.T) { + var cli struct { + Hello bool `and:"another"` + One bool `and:"group"` + Two string `and:"group"` + } + p := mustNew(t, &cli) + _, err := p.Parse([]string{"--hello", "--one"}) + assert.EqualError(t, err, "--one and --two must be used together") + + p = mustNew(t, &cli) + _, err = p.Parse([]string{"--one", "--two=hi", "--hello"}) + assert.NoError(t, err) +} + func TestXorChild(t *testing.T) { var cli struct { One bool `xor:"group"` @@ -936,6 +952,23 @@ func TestXorChild(t *testing.T) { assert.Error(t, err, "--two and --three can't be used together") } +func TestAndChild(t *testing.T) { + var cli struct { + One bool `and:"group"` + Cmd struct { + Two string `and:"group"` + Three string `and:"group"` + } `cmd` + } + p := mustNew(t, &cli) + _, err := p.Parse([]string{"--one", "cmd", "--two=hi", "--three=hello"}) + assert.NoError(t, err) + + p = mustNew(t, &cli) + _, err = p.Parse([]string{"--two=hi", "cmd"}) + assert.Error(t, err, "--two and --three must be used together") +} + func TestMultiXor(t *testing.T) { var cli struct { Hello bool `xor:"one,two"` @@ -952,6 +985,47 @@ func TestMultiXor(t *testing.T) { assert.EqualError(t, err, "--hello and --two can't be used together") } +func TestMultiAnd(t *testing.T) { + var cli struct { + Hello bool `and:"one,two"` + One bool `and:"one"` + Two string `and:"two"` + } + + p := mustNew(t, &cli) + _, err := p.Parse([]string{"--hello"}) + // Split and combine error so messages always will be in the same order + // when testing + missingMsgs := strings.Split(err.Error(), ", ") + sort.Strings(missingMsgs) + err = fmt.Errorf("%s", strings.Join(missingMsgs, ", ")) + assert.EqualError(t, err, "--hello and --one must be used together, --hello and --two must be used together") + + p = mustNew(t, &cli) + _, err = p.Parse([]string{"--two=foo"}) + assert.EqualError(t, err, "--hello and --two must be used together") +} + +func TestXorAnd(t *testing.T) { + var cli struct { + Hello bool `xor:"one" and:"two"` + One bool `xor:"one"` + Two string `and:"two"` + } + + p := mustNew(t, &cli) + _, err := p.Parse([]string{"--hello"}) + assert.EqualError(t, err, "--hello and --two must be used together") + + p = mustNew(t, &cli) + _, err = p.Parse([]string{"--one"}) + assert.NoError(t, err) + + p = mustNew(t, &cli) + _, err = p.Parse([]string{"--hello", "--one"}) + assert.EqualError(t, err, "--hello and --one can't be used together, --hello and --two must be used together") +} + func TestXorRequired(t *testing.T) { var cli struct { One bool `xor:"one,two" required:""` @@ -972,6 +1046,26 @@ func TestXorRequired(t *testing.T) { assert.EqualError(t, err, "missing flags: --four, --one or --three, --one or --two") } +func TestAndRequired(t *testing.T) { + var cli struct { + One bool `and:"one,two" required:""` + Two bool `and:"one" required:""` + Three bool `and:"two"` + Four bool `required:""` + } + p := mustNew(t, &cli) + _, err := p.Parse([]string{"--one", "--two", "--three"}) + assert.EqualError(t, err, "missing flags: --four") + + p = mustNew(t, &cli) + _, err = p.Parse([]string{"--four"}) + assert.EqualError(t, err, "missing flags: --one and --three, --one and --two") + + p = mustNew(t, &cli) + _, err = p.Parse([]string{}) + assert.EqualError(t, err, "missing flags: --four, --one and --three, --one and --two") +} + func TestXorRequiredMany(t *testing.T) { var cli struct { One bool `xor:"one" required:""` @@ -991,6 +1085,21 @@ func TestXorRequiredMany(t *testing.T) { assert.EqualError(t, err, "missing flags: --one or --two or --three") } +func TestAndRequiredMany(t *testing.T) { + var cli struct { + One bool `and:"one" required:""` + Two bool `and:"one" required:""` + Three bool `and:"one" required:""` + } + p := mustNew(t, &cli) + _, err := p.Parse([]string{}) + assert.EqualError(t, err, "missing flags: --one and --two and --three") + + p = mustNew(t, &cli) + _, err = p.Parse([]string{"--three"}) + assert.EqualError(t, err, "missing flags: --one and --two") +} + func TestEnumSequence(t *testing.T) { var cli struct { State []string `enum:"a,b,c" default:"a"` diff --git a/model.go b/model.go index 8d1f82f..3190f3e 100644 --- a/model.go +++ b/model.go @@ -405,6 +405,7 @@ type Flag struct { *Value Group *Group // Logical grouping when displaying. May also be used by configuration loaders to group options logically. Xor []string + And []string PlaceHolder string Envs []string Aliases []string diff --git a/tag.go b/tag.go index 3e37c19..39be2e3 100644 --- a/tag.go +++ b/tag.go @@ -32,6 +32,7 @@ type Tag struct { Enum string Group string Xor []string + And []string Vars Vars Prefix string // Optional prefix on anonymous structs. All sub-flags will have this prefix. EnvPrefix string @@ -249,6 +250,9 @@ func hydrateTag(t *Tag, typ reflect.Type) error { //nolint: gocyclo for _, xor := range t.GetAll("xor") { t.Xor = append(t.Xor, strings.FieldsFunc(xor, tagSplitFn)...) } + for _, and := range t.GetAll("and") { + t.And = append(t.And, strings.FieldsFunc(and, tagSplitFn)...) + } t.Prefix = t.Get("prefix") t.EnvPrefix = t.Get("envprefix") t.Embed = t.Has("embed") From d113f61e2acf60e555e5227d3305156af9ed7fe9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 12 Aug 2024 16:24:10 +1000 Subject: [PATCH 09/81] chore(deps): update all non-major dependencies (#445) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- _examples/server/go.mod | 2 +- _examples/server/go.sum | 8 ++++++++ bin/{.go-1.22.5.pkg => .go-1.22.6.pkg} | 0 bin/go | 2 +- bin/gofmt | 2 +- 5 files changed, 11 insertions(+), 3 deletions(-) rename bin/{.go-1.22.5.pkg => .go-1.22.6.pkg} (100%) diff --git a/_examples/server/go.mod b/_examples/server/go.mod index 1feb248..690e3bb 100644 --- a/_examples/server/go.mod +++ b/_examples/server/go.mod @@ -12,5 +12,5 @@ require ( 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.25.0 + golang.org/x/crypto v0.26.0 ) diff --git a/_examples/server/go.sum b/_examples/server/go.sum index bf1cd72..b0fd281 100644 --- a/_examples/server/go.sum +++ b/_examples/server/go.sum @@ -72,6 +72,8 @@ golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+ golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= +golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= +golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= 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= @@ -94,6 +96,7 @@ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -121,6 +124,8 @@ golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= +golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -142,6 +147,8 @@ golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= +golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= +golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -153,6 +160,7 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= diff --git a/bin/.go-1.22.5.pkg b/bin/.go-1.22.6.pkg similarity index 100% rename from bin/.go-1.22.5.pkg rename to bin/.go-1.22.6.pkg diff --git a/bin/go b/bin/go index 5c26cb9..fec23e2 120000 --- a/bin/go +++ b/bin/go @@ -1 +1 @@ -.go-1.22.5.pkg \ No newline at end of file +.go-1.22.6.pkg \ No newline at end of file diff --git a/bin/gofmt b/bin/gofmt index 5c26cb9..fec23e2 120000 --- a/bin/gofmt +++ b/bin/gofmt @@ -1 +1 @@ -.go-1.22.5.pkg \ No newline at end of file +.go-1.22.6.pkg \ No newline at end of file From 62929536454bee27a4b249960e481eeddcc84cb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Rapha=C3=ABl=20Matte?= <35611957+jeralm@users.noreply.github.com> Date: Mon, 12 Aug 2024 23:05:45 -0400 Subject: [PATCH 10/81] docs: specify usage of the placeholder tag (#448) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cbe6224..205899f 100644 --- a/README.md +++ b/README.md @@ -561,7 +561,7 @@ Both can coexist with standard Tag parsing. | `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. | +| `placeholder:"X"` | Placeholder input, if flag. e.g. `` `placeholder:""` `` will show `--flag-name=` when displaying help. | | `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 | From 2ad9498bdfbb7dcae73ae57508692826353a918d Mon Sep 17 00:00:00 2001 From: Alec Thomas Date: Tue, 20 Aug 2024 08:16:24 +1000 Subject: [PATCH 11/81] chore: bump go + golangci-lint --- .golangci.yml | 3 +++ bin/{.go-1.22.6.pkg => .go-1.23.0.pkg} | 0 bin/{.golangci-lint-1.55.2.pkg => .golangci-lint-1.60.1.pkg} | 0 bin/go | 2 +- bin/gofmt | 2 +- bin/golangci-lint | 2 +- context.go | 2 +- help_test.go | 2 +- kong_test.go | 2 +- levenshtein.go | 2 +- resolver.go | 2 +- 11 files changed, 11 insertions(+), 8 deletions(-) rename bin/{.go-1.22.6.pkg => .go-1.23.0.pkg} (100%) rename bin/{.golangci-lint-1.55.2.pkg => .golangci-lint-1.60.1.pkg} (100%) diff --git a/.golangci.yml b/.golangci.yml index 2c5bfe3..c922a37 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -47,6 +47,8 @@ linters: - varcheck # deprecated since v1.49.0 - depguard # nothing to guard against yet - tagalign # hurts readability of kong tags + - mnd + - perfsprint linters-settings: govet: @@ -76,6 +78,7 @@ issues: - 'bad syntax for struct tag key' - 'bad syntax for struct tag pair' - 'result .* \(error\) is always nil' + - 'Error return value of `fmt.Fprintln` is not checked' exclude-rules: # Don't warn on unused parameters. diff --git a/bin/.go-1.22.6.pkg b/bin/.go-1.23.0.pkg similarity index 100% rename from bin/.go-1.22.6.pkg rename to bin/.go-1.23.0.pkg diff --git a/bin/.golangci-lint-1.55.2.pkg b/bin/.golangci-lint-1.60.1.pkg similarity index 100% rename from bin/.golangci-lint-1.55.2.pkg rename to bin/.golangci-lint-1.60.1.pkg diff --git a/bin/go b/bin/go index fec23e2..4602254 120000 --- a/bin/go +++ b/bin/go @@ -1 +1 @@ -.go-1.22.6.pkg \ No newline at end of file +.go-1.23.0.pkg \ No newline at end of file diff --git a/bin/gofmt b/bin/gofmt index fec23e2..4602254 120000 --- a/bin/gofmt +++ b/bin/gofmt @@ -1 +1 @@ -.go-1.22.6.pkg \ No newline at end of file +.go-1.23.0.pkg \ No newline at end of file diff --git a/bin/golangci-lint b/bin/golangci-lint index 5b19a9d..4857716 120000 --- a/bin/golangci-lint +++ b/bin/golangci-lint @@ -1 +1 @@ -.golangci-lint-1.55.2.pkg \ No newline at end of file +.golangci-lint-1.60.1.pkg \ No newline at end of file diff --git a/context.go b/context.go index f5239c7..ea0ec1c 100644 --- a/context.go +++ b/context.go @@ -1015,7 +1015,7 @@ func checkXorDuplicatedAndAndMissing(paths []*Path) error { errs = append(errs, err.Error()) } if len(errs) > 0 { - return fmt.Errorf(strings.Join(errs, ", ")) + return errors.New(strings.Join(errs, ", ")) } return nil } diff --git a/help_test.go b/help_test.go index 38080a3..e43a073 100644 --- a/help_test.go +++ b/help_test.go @@ -600,7 +600,7 @@ func TestAutoGroup(t *testing.T) { if node, ok := parent.(*kong.Node); ok { return &kong.Group{ Key: node.Name, - Title: strings.Title(node.Name) + " flags:", //nolint + Title: strings.Title(node.Name) + " flags:", } } return nil diff --git a/kong_test.go b/kong_test.go index 0f0bb86..2ac9cbd 100644 --- a/kong_test.go +++ b/kong_test.go @@ -608,7 +608,7 @@ func TestMultilineMessage(t *testing.T) { w := &bytes.Buffer{} var cli struct{} p := mustNew(t, &cli, kong.Writers(w, w)) - p.Printf(test.text) + p.Printf("%s", test.text) assert.Equal(t, test.want, w.String()) }) } diff --git a/levenshtein.go b/levenshtein.go index 1816f30..6837d6c 100644 --- a/levenshtein.go +++ b/levenshtein.go @@ -31,7 +31,7 @@ func levenshtein(a, b string) int { return f[len(f)-1] } -func min(a, b int) int { +func min(a, b int) int { //nolint:predeclared if a <= b { return a } diff --git a/resolver.go b/resolver.go index 05be7f6..dca4309 100644 --- a/resolver.go +++ b/resolver.go @@ -63,6 +63,6 @@ func JSON(r io.Reader) (Resolver, error) { } func snakeCase(name string) string { - name = strings.Join(strings.Split(strings.Title(name), "-"), "") //nolint: staticcheck + name = strings.Join(strings.Split(strings.Title(name), "-"), "") return strings.ToLower(name[:1]) + name[1:] } From 6c216a37cee562d79039309b7f7ba489e9fe2f94 Mon Sep 17 00:00:00 2001 From: Cole Snodgrass Date: Tue, 20 Aug 2024 18:55:19 -0700 Subject: [PATCH 12/81] fix: typo in README (#451) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 205899f..0d576c6 100644 --- a/README.md +++ b/README.md @@ -577,7 +577,7 @@ Both can coexist with standard Tag parsing. | `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. | -| `and:"X,Y,..."` | Exclsuive 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. | +| `and:"X,Y,..."` | Exclusive 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. | | `set:"K=V"` | Set a variable for expansion by child elements. Multiples can occur. | From b297ae97f2a54d75c9a68f3015e615e0be9fa310 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 10 Sep 2024 21:00:16 +1000 Subject: [PATCH 13/81] chore(deps): update all non-major dependencies (#455) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- _examples/server/go.mod | 2 +- _examples/server/go.sum | 7 +++++++ bin/{.go-1.23.0.pkg => .go-1.23.1.pkg} | 0 bin/go | 2 +- bin/gofmt | 2 +- 5 files changed, 10 insertions(+), 3 deletions(-) rename bin/{.go-1.23.0.pkg => .go-1.23.1.pkg} (100%) diff --git a/_examples/server/go.mod b/_examples/server/go.mod index 690e3bb..b923839 100644 --- a/_examples/server/go.mod +++ b/_examples/server/go.mod @@ -12,5 +12,5 @@ require ( 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.26.0 + golang.org/x/crypto v0.27.0 ) diff --git a/_examples/server/go.sum b/_examples/server/go.sum index b0fd281..fd8f6d6 100644 --- a/_examples/server/go.sum +++ b/_examples/server/go.sum @@ -74,6 +74,8 @@ golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= +golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= +golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= 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= @@ -126,6 +128,8 @@ golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= +golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -149,6 +153,8 @@ golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= +golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM= +golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -161,6 +167,7 @@ golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= diff --git a/bin/.go-1.23.0.pkg b/bin/.go-1.23.1.pkg similarity index 100% rename from bin/.go-1.23.0.pkg rename to bin/.go-1.23.1.pkg diff --git a/bin/go b/bin/go index 4602254..2cf16c6 120000 --- a/bin/go +++ b/bin/go @@ -1 +1 @@ -.go-1.23.0.pkg \ No newline at end of file +.go-1.23.1.pkg \ No newline at end of file diff --git a/bin/gofmt b/bin/gofmt index 4602254..2cf16c6 120000 --- a/bin/gofmt +++ b/bin/gofmt @@ -1 +1 @@ -.go-1.23.0.pkg \ No newline at end of file +.go-1.23.1.pkg \ No newline at end of file From 7d84b9529490a9048a3765e3fa5966f4cda6157f Mon Sep 17 00:00:00 2001 From: Camilla Date: Tue, 10 Sep 2024 13:01:12 +0200 Subject: [PATCH 14/81] Feature: Add check for overlapping xor and and groups (#443) * Docs: Clean and group description * Feat: Add check for overlapping xor and and groups Co-authored-by: inful * Chore: Rewrite overlap err to avoid duplicated words --------- Co-authored-by: inful --- README.md | 2 +- kong.go | 33 +++++++++++++++++++++++++++++++++ kong_test.go | 10 ++++++++++ 3 files changed, 44 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0d576c6..994da42 100644 --- a/README.md +++ b/README.md @@ -577,7 +577,7 @@ Both can coexist with standard Tag parsing. | `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. | -| `and:"X,Y,..."` | Exclusive 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. | +| `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. | | `set:"K=V"` | Set a variable for expansion by child elements. Multiples can occur. | diff --git a/kong.go b/kong.go index 19ff88e..10235d0 100644 --- a/kong.go +++ b/kong.go @@ -167,9 +167,42 @@ func New(grammar interface{}, options ...Option) (*Kong, error) { k.bindings.add(k.vars) + if err = checkOverlappingXorAnd(k); err != nil { + return nil, err + } + return k, nil } +func checkOverlappingXorAnd(k *Kong) error { + xorGroups := map[string][]string{} + andGroups := map[string][]string{} + for _, flag := range k.Model.Node.Flags { + for _, xor := range flag.Xor { + xorGroups[xor] = append(xorGroups[xor], flag.Name) + } + for _, and := range flag.And { + andGroups[and] = append(andGroups[and], flag.Name) + } + } + for xor, xorSet := range xorGroups { + for and, andSet := range andGroups { + overlappingEntries := []string{} + for _, xorTag := range xorSet { + for _, andTag := range andSet { + if xorTag == andTag { + overlappingEntries = append(overlappingEntries, xorTag) + } + } + } + if len(overlappingEntries) > 1 { + return fmt.Errorf("invalid xor and combination, %s and %s overlap with more than one: %s", xor, and, overlappingEntries) + } + } + } + return nil +} + type varStack []Vars func (v *varStack) head() Vars { return (*v)[len(*v)-1] } diff --git a/kong_test.go b/kong_test.go index 2ac9cbd..c148962 100644 --- a/kong_test.go +++ b/kong_test.go @@ -1026,6 +1026,16 @@ func TestXorAnd(t *testing.T) { assert.EqualError(t, err, "--hello and --one can't be used together, --hello and --two must be used together") } +func TestOverLappingXorAnd(t *testing.T) { + var cli struct { + Hello bool `xor:"one" and:"two"` + One bool `xor:"one" and:"two"` + Two string `xor:"one" and:"two"` + } + _, err := kong.New(&cli) + assert.EqualError(t, err, "invalid xor and combination, one and two overlap with more than one: [hello one two]") +} + func TestXorRequired(t *testing.T) { var cli struct { One bool `xor:"one,two" required:""` From 4ecb53599b72bbe620e2417a3d6eed815a2e6f2a Mon Sep 17 00:00:00 2001 From: Cam Hutchison Date: Tue, 10 Sep 2024 21:02:37 +1000 Subject: [PATCH 15/81] Make negatable flag name customisable (#439) * fix: Check if negatable duplicates another flag Add a check for flags with the `negatable` option if the negative flag conflicts with another tag, such as: Flag bool `negatable:""` NoFlag bool The flag `--no-flag` is ambiguous in this scenario. * feat: Make negatable flag name customisable Allow a value on the `negatable` tag to specify a flag name to use for negation instead of using `--no-` as the flag. e.g. Approve bool `default:"true",negatable:"deny"` This example will allow `--deny` to set the `Approve` field to false. --- README.md | 1 + build.go | 7 ++++ context.go | 6 ++-- help.go | 33 ++++++++----------- help_test.go | 3 ++ kong_test.go | 93 +++++++++++++++++++++++++++++++++++++++------------- negatable.go | 19 +++++++++++ tag.go | 15 ++++++--- 8 files changed, 128 insertions(+), 49 deletions(-) create mode 100644 negatable.go diff --git a/README.md b/README.md index 994da42..3936fde 100644 --- a/README.md +++ b/README.md @@ -571,6 +571,7 @@ Both can coexist with standard Tag parsing. | `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 | +| `negatable:"X"` | If present on a `bool` field, supports `--X` 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. | diff --git a/build.go b/build.go index 18048e0..7791c62 100644 --- a/build.go +++ b/build.go @@ -315,6 +315,13 @@ func buildField(k *Kong, node *Node, v reflect.Value, ft reflect.StructField, fv } seenFlags["-"+string(tag.Short)] = true } + if tag.Negatable != "" { + negFlag := negatableFlagName(value.Name, tag.Negatable) + if seenFlags[negFlag] { + return failField(v, ft, "duplicate negation flag %s", negFlag) + } + seenFlags[negFlag] = true + } flag := &Flag{ Value: value, Aliases: tag.Aliases, diff --git a/context.go b/context.go index ea0ec1c..c97aa84 100644 --- a/context.go +++ b/context.go @@ -710,13 +710,13 @@ func (c *Context) parseFlag(flags []*Flag, match string) (err error) { candidates = append(candidates, alias) } - neg := "--no-" + flag.Name - if !matched && !(match == neg && flag.Tag.Negatable) { + neg := negatableFlagName(flag.Name, flag.Tag.Negatable) + if !matched && match != neg { continue } // Found a matching flag. c.scan.Pop() - if match == neg && flag.Tag.Negatable { + if match == neg && flag.Tag.Negatable != "" { flag.Negated = true } err := flag.Parse(c.scan, c.getValue(flag.Value)) diff --git a/help.go b/help.go index cf5a912..26f355d 100644 --- a/help.go +++ b/help.go @@ -491,27 +491,22 @@ func formatFlag(haveShort bool, flag *Flag) string { name := flag.Name isBool := flag.IsBool() isCounter := flag.IsCounter() + + short := "" if flag.Short != 0 { - 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 isBool && flag.Tag.Negatable { - if haveShort { - flagString = fmt.Sprintf(" --[no-]%s", name) - } else { - flagString = fmt.Sprintf("--[no-]%s", name) - } - } else { - if haveShort { - flagString += fmt.Sprintf(" --%s", name) - } else { - flagString += fmt.Sprintf("--%s", name) - } - } + short = "-" + string(flag.Short) + ", " + } else if haveShort { + short = " " } + + if isBool && flag.Tag.Negatable == negatableDefault { + name = "[no-]" + name + } else if isBool && flag.Tag.Negatable != "" { + name += "/" + flag.Tag.Negatable + } + + flagString += fmt.Sprintf("%s--%s", short, name) + if !isBool && !isCounter { flagString += fmt.Sprintf("=%s", flag.FormatPlaceHolder()) } diff --git a/help_test.go b/help_test.go index e43a073..e680be2 100644 --- a/help_test.go +++ b/help_test.go @@ -71,6 +71,7 @@ func TestHelp(t *testing.T) { Map map[string]int `help:"A map of strings to ints."` Required bool `required help:"A required flag."` Sort bool `negatable short:"s" help:"Is sortable or not."` + Approve bool `negatable:"deny" help:"Approve or deny message."` One struct { Flag string `help:"Nested flag."` @@ -118,6 +119,7 @@ Flags: --map=KEY=VALUE;... A map of strings to ints. --required A required flag. -s, --[no-]sort Is sortable or not. + --approve/deny Approve or deny message. Commands: one --required [flags] @@ -159,6 +161,7 @@ Flags: --map=KEY=VALUE;... A map of strings to ints. --required A required flag. -s, --[no-]sort Is sortable or not. + --approve/deny Approve or deny message. --flag=STRING Nested flag under two. --required-two diff --git a/kong_test.go b/kong_test.go index c148962..7abf0ac 100644 --- a/kong_test.go +++ b/kong_test.go @@ -357,8 +357,9 @@ func TestTraceErrorPartiallySucceeds(t *testing.T) { } type commandWithNegatableFlag struct { - Flag bool `kong:"default='true',negatable"` - ran bool + Flag bool `kong:"default='true',negatable"` + Custom bool `kong:"default='true',negatable='standard'"` + ran bool } func (c *commandWithNegatableFlag) Run() error { @@ -368,34 +369,64 @@ func (c *commandWithNegatableFlag) Run() error { func TestNegatableFlag(t *testing.T) { tests := []struct { - name string - args []string - expected bool + name string + args []string + expectedFlag bool + expectedCustom bool }{ { - name: "no flag", - args: []string{"cmd"}, - expected: true, + name: "no flag", + args: []string{"cmd"}, + expectedFlag: true, + expectedCustom: true, }, { - name: "boolean flag", - args: []string{"cmd", "--flag"}, - expected: true, + name: "boolean flag", + args: []string{"cmd", "--flag"}, + expectedFlag: true, + expectedCustom: true, }, { - name: "inverted boolean flag", - args: []string{"cmd", "--flag=false"}, - expected: false, + name: "custom boolean flag", + args: []string{"cmd", "--custom"}, + expectedFlag: true, + expectedCustom: true, }, { - name: "negated boolean flag", - args: []string{"cmd", "--no-flag"}, - expected: false, + name: "inverted boolean flag", + args: []string{"cmd", "--flag=false"}, + expectedFlag: false, + expectedCustom: true, }, { - name: "inverted negated boolean flag", - args: []string{"cmd", "--no-flag=false"}, - expected: true, + name: "custom inverted boolean flag", + args: []string{"cmd", "--custom=false"}, + expectedFlag: true, + expectedCustom: false, + }, + { + name: "negated boolean flag", + args: []string{"cmd", "--no-flag"}, + expectedFlag: false, + expectedCustom: true, + }, + { + name: "custom negated boolean flag", + args: []string{"cmd", "--standard"}, + expectedFlag: true, + expectedCustom: false, + }, + { + name: "inverted negated boolean flag", + args: []string{"cmd", "--no-flag=false"}, + expectedFlag: true, + expectedCustom: true, + }, + { + name: "inverted custom negated boolean flag", + args: []string{"cmd", "--standard=false"}, + expectedFlag: true, + expectedCustom: true, }, } for _, tt := range tests { @@ -408,16 +439,34 @@ func TestNegatableFlag(t *testing.T) { p := mustNew(t, &cli) kctx, err := p.Parse(tt.args) assert.NoError(t, err) - assert.Equal(t, tt.expected, cli.Cmd.Flag) + assert.Equal(t, tt.expectedFlag, cli.Cmd.Flag) + assert.Equal(t, tt.expectedCustom, cli.Cmd.Custom) err = kctx.Run() assert.NoError(t, err) - assert.Equal(t, tt.expected, cli.Cmd.Flag) + assert.Equal(t, tt.expectedFlag, cli.Cmd.Flag) + assert.Equal(t, tt.expectedCustom, cli.Cmd.Custom) assert.True(t, cli.Cmd.ran) }) } } +func TestDuplicateNegatableLong(t *testing.T) { + cli2 := struct { + NoFlag bool + Flag bool `negatable:""` // negation duplicates NoFlag + }{} + _, err := kong.New(&cli2) + assert.EqualError(t, err, ".Flag: duplicate negation flag --no-flag") + + cli3 := struct { + One bool + Two bool `negatable:"one"` // negation duplicates Flag2 + }{} + _, err = kong.New(&cli3) + assert.EqualError(t, err, ".Two: duplicate negation flag --one") +} + func TestExistingNoFlag(t *testing.T) { var cli struct { Cmd struct { diff --git a/negatable.go b/negatable.go new file mode 100644 index 0000000..7d8902a --- /dev/null +++ b/negatable.go @@ -0,0 +1,19 @@ +package kong + +// negatableDefault is a placeholder value for the Negatable tag to indicate +// the negated flag is --no-. This is needed as at the time of +// parsing a tag, the field's flag name is not yet known. +const negatableDefault = "_" + +// negatableFlagName returns the name of the flag for a negatable field, or +// an empty string if the field is not negatable. +func negatableFlagName(name, negation string) string { + switch negation { + case "": + return "" + case negatableDefault: + return "--no-" + name + default: + return "--" + negation + } +} diff --git a/tag.go b/tag.go index 39be2e3..456f7ae 100644 --- a/tag.go +++ b/tag.go @@ -38,7 +38,7 @@ type Tag struct { EnvPrefix string Embed bool Aliases []string - Negatable bool + Negatable string Passthrough bool // Storage for all tag keys for arbitrary lookups. @@ -256,11 +256,16 @@ func hydrateTag(t *Tag, typ reflect.Type) error { //nolint: gocyclo t.Prefix = t.Get("prefix") t.EnvPrefix = t.Get("envprefix") t.Embed = t.Has("embed") - negatable := t.Has("negatable") - if negatable && !isBool && !isBoolPtr { - return fmt.Errorf("negatable can only be set on booleans") + if t.Has("negatable") { + if !isBool && !isBoolPtr { + return fmt.Errorf("negatable can only be set on booleans") + } + negatable := t.Get("negatable") + if negatable == "" { + negatable = negatableDefault // placeholder for default negation of --no- + } + t.Negatable = negatable } - t.Negatable = negatable aliases := t.Get("aliases") if len(aliases) > 0 { t.Aliases = append(t.Aliases, strings.FieldsFunc(aliases, tagSplitFn)...) From 9924ec4461bc4fec32a85c16970c9a2a691d41c9 Mon Sep 17 00:00:00 2001 From: Bob Lail Date: Tue, 10 Sep 2024 17:51:37 -0700 Subject: [PATCH 16/81] fix!: Include `--` in passthrough args (#436) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Given a grammar like this: ```golang var cli struct { Args []string `arg:"" optional:"" passthrough:""` } ``` If Kong parses `cli foo -- bar`, it will populate `Args` with `[]string{"foo", "--", "bar"}` (including "`--`"). However, if Kong parses `cli -- foo bar`, will populate `Args` with `[]string{"foo", "bar"}` (leaving off `"--"`). This differs from the behavior of a passthrough Command, where `"--"` is included with the args in both cases. There are 3 places where `c.endParsing()` is called 1. When `node.Passthrough` is true: https://github.com/alecthomas/kong/blob/5f9c5cc822bdb888a3671c44d4688a6f602ecb90/context.go#L366-L368 2. When `arg.Passthrough` is true: https://github.com/alecthomas/kong/blob/5f9c5cc822bdb888a3671c44d4688a6f602ecb90/context.go#L451-L453 3. When `"--"` is encountered: https://github.com/alecthomas/kong/blob/5f9c5cc822bdb888a3671c44d4688a6f602ecb90/context.go#L384-L387 The first two do not also pop any tokens. The third one does. This commit makes `c.scan.Pop()` conditional, skipping it when the next positional argument is passthrough. I believe this will cause Kong to behave a little more consistently — and from my perspective, `--` is relevant for args intended to be passed through! — but it will change the behavior of existing projects that use `arg:"" passthrough:""`. --- context.go | 6 +++++- kong_test.go | 10 ++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/context.go b/context.go index c97aa84..2ed5d28 100644 --- a/context.go +++ b/context.go @@ -383,9 +383,13 @@ func (c *Context) trace(node *Node) (err error) { //nolint: gocyclo // Indicates end of parsing. All remaining arguments are treated as positional arguments only. case v == "--": - c.scan.Pop() c.endParsing() + // Pop the -- token unless the next positional argument accepts passthrough arguments. + if !(positional < len(node.Positional) && node.Positional[positional].Passthrough) { + c.scan.Pop() + } + // Long flag. case strings.HasPrefix(v, "--"): c.scan.Pop() diff --git a/kong_test.go b/kong_test.go index 7abf0ac..f05fc18 100644 --- a/kong_test.go +++ b/kong_test.go @@ -1743,10 +1743,16 @@ func TestPassthroughArgs(t *testing.T) { []string{"something"}, }, { - "DashDashBeforeRecognizedFlag", + "DashDashBetweenArgs", + []string{"foo", "--", "bar"}, + "", + []string{"foo", "--", "bar"}, + }, + { + "DashDash", []string{"--", "--flag", "foobar"}, "", - []string{"--flag", "foobar"}, + []string{"--", "--flag", "foobar"}, }, { "UnrecognizedFlagAndArgs", From 26c1c9ad052ae46eb77fda47fea6b2b7cfb6a605 Mon Sep 17 00:00:00 2001 From: Alec Thomas Date: Thu, 12 Sep 2024 12:05:09 +1000 Subject: [PATCH 17/81] fix: allow duplicate custom negated flags Fixes #456 --- build.go | 3 +++ kong_test.go | 13 +++++++++++++ 2 files changed, 16 insertions(+) diff --git a/build.go b/build.go index 7791c62..287beeb 100644 --- a/build.go +++ b/build.go @@ -173,6 +173,9 @@ MAIN: if flag.Short != 0 { delete(seenFlags, "-"+string(flag.Short)) } + if negFlag := negatableFlagName(flag.Name, flag.Tag.Negatable); negFlag != "" { + delete(seenFlags, negFlag) + } for _, aflag := range flag.Aliases { delete(seenFlags, "--"+aflag) } diff --git a/kong_test.go b/kong_test.go index f05fc18..4785dd0 100644 --- a/kong_test.go +++ b/kong_test.go @@ -467,6 +467,19 @@ func TestDuplicateNegatableLong(t *testing.T) { assert.EqualError(t, err, ".Two: duplicate negation flag --one") } +func TestDuplicateNegatableFlagsInSubcommands(t *testing.T) { + cli2 := struct { + Sub struct { + Negated bool `negatable:"nope-"` + } `cmd:""` + Sub2 struct { + Negated bool `negatable:"nope-"` + } `cmd:""` + }{} + _, err := kong.New(&cli2) + assert.NoError(t, err) +} + func TestExistingNoFlag(t *testing.T) { var cli struct { Cmd struct { From 3b28c2c83a3ee74b1f7d8087f50f88766755544f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 16 Sep 2024 19:49:21 +1000 Subject: [PATCH 18/81] chore(deps): update module github.com/alecthomas/kong to v1 (#457) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- _examples/server/go.mod | 2 +- _examples/server/go.sum | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/_examples/server/go.mod b/_examples/server/go.mod index b923839..5830322 100644 --- a/_examples/server/go.mod +++ b/_examples/server/go.mod @@ -4,7 +4,7 @@ go 1.13 require ( github.com/alecthomas/colour v0.1.0 - github.com/alecthomas/kong v0.9.0 + github.com/alecthomas/kong v1.2.1 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 diff --git a/_examples/server/go.sum b/_examples/server/go.sum index fd8f6d6..22c3dd5 100644 --- a/_examples/server/go.sum +++ b/_examples/server/go.sum @@ -1,5 +1,6 @@ github.com/alecthomas/assert/v2 v2.1.0/go.mod h1:b/+1DI2Q6NckYi+3mXyH3wFb8qG37K/DuK80n7WefXA= github.com/alecthomas/assert/v2 v2.6.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= +github.com/alecthomas/assert/v2 v2.10.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= 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.1 h1:V1tLBhyQBC4rsbXbcOvm3GBaytJSwRNX69fp1WJxbqQ= @@ -8,6 +9,8 @@ github.com/alecthomas/kong v0.8.1 h1:acZdn3m4lLRobeh3Zi2S2EpnXTd1mOL6U7xVml+vfkY github.com/alecthomas/kong v0.8.1/go.mod h1:n1iCIO2xS46oE8ZfYCNDqdR0b0wZNrXAIAqro/2132U= github.com/alecthomas/kong v0.9.0 h1:G5diXxc85KvoV2f0ZRVuMsi45IrBgx9zDNGNj165aPA= github.com/alecthomas/kong v0.9.0/go.mod h1:Y47y5gKfHp1hDc7CH7OeXgLIpp+Q2m1Ni0L5s3bI8Os= +github.com/alecthomas/kong v1.2.1 h1:E8jH4Tsgv6wCRX2nGrdPyHDUCSG83WH2qE4XLACD33Q= +github.com/alecthomas/kong v1.2.1/go.mod h1:rKTSFhbdp3Ryefn8x5MOEprnRFQ7nlmMC01GKhehhBM= 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= From 31301f527cf21d0095f2dab3d5ae3f41c8e09b1b Mon Sep 17 00:00:00 2001 From: Alec Thomas Date: Thu, 19 Sep 2024 04:08:34 +1000 Subject: [PATCH 19/81] docs: add note about 1.0 to README --- README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3936fde..6c99853 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ +- [Release](#release) - [Introduction](#introduction) - [Help](#help) - [Help as a user of a Kong application](#help-as-a-user-of-a-kong-application) @@ -42,6 +43,12 @@ +## Version 1.0.0 Release + +Kong has been stable for a long time, so it seemed appropriate to cut a 1.0 release. + +There is one breaking change, #436, which should effect relatively few users. + ## Introduction Kong aims to support arbitrarily complex command-line structures with as little developer effort as possible. @@ -554,7 +561,7 @@ Tags can be in two forms: Both can coexist with standard Tag parsing. | 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. | From b34e9952dea92b1c05c04199468064bba4d84d62 Mon Sep 17 00:00:00 2001 From: Alexey Palazhchenko Date: Thu, 19 Sep 2024 12:27:07 +0400 Subject: [PATCH 20/81] Fix Markdown in README.md (#458) --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6c99853..b2c023d 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ -- [Release](#release) +- [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) @@ -47,7 +47,7 @@ Kong has been stable for a long time, so it seemed appropriate to cut a 1.0 release. -There is one breaking change, #436, which should effect relatively few users. +There is one breaking change, [#436](https://github.com/alecthomas/kong/pull/436), which should effect relatively few users. ## Introduction From c9fbc305f4d0629395f729e83f633bf70e77a8fe Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 23 Sep 2024 10:05:14 +1000 Subject: [PATCH 21/81] chore(deps): update module github.com/alecthomas/assert/v2 to v2.11.0 (#459) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index dad659b..a128f6e 100644 --- a/go.mod +++ b/go.mod @@ -1,7 +1,7 @@ module github.com/alecthomas/kong require ( - github.com/alecthomas/assert/v2 v2.10.0 + github.com/alecthomas/assert/v2 v2.11.0 github.com/alecthomas/repr v0.4.0 ) diff --git a/go.sum b/go.sum index c65b342..c03ffd4 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/alecthomas/assert/v2 v2.10.0 h1:jjRCHsj6hBJhkmhznrCzoNpbA3zqy0fYiUcYZP/GkPY= github.com/alecthomas/assert/v2 v2.10.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= +github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= +github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= 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/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= From 56e990fc15ee8b1426a07e14d22013a38d272c67 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 7 Oct 2024 14:25:31 +1100 Subject: [PATCH 22/81] chore(deps): update all non-major dependencies (#462) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- _examples/server/go.mod | 2 +- _examples/server/go.sum | 7 +++++++ bin/{.go-1.23.1.pkg => .go-1.23.2.pkg} | 0 bin/go | 2 +- bin/gofmt | 2 +- 5 files changed, 10 insertions(+), 3 deletions(-) rename bin/{.go-1.23.1.pkg => .go-1.23.2.pkg} (100%) diff --git a/_examples/server/go.mod b/_examples/server/go.mod index 5830322..5f3a811 100644 --- a/_examples/server/go.mod +++ b/_examples/server/go.mod @@ -12,5 +12,5 @@ require ( 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.27.0 + golang.org/x/crypto v0.28.0 ) diff --git a/_examples/server/go.sum b/_examples/server/go.sum index 22c3dd5..dcddb65 100644 --- a/_examples/server/go.sum +++ b/_examples/server/go.sum @@ -79,6 +79,8 @@ golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= 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= @@ -133,6 +135,8 @@ golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -158,6 +162,8 @@ golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM= golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -171,6 +177,7 @@ golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= diff --git a/bin/.go-1.23.1.pkg b/bin/.go-1.23.2.pkg similarity index 100% rename from bin/.go-1.23.1.pkg rename to bin/.go-1.23.2.pkg diff --git a/bin/go b/bin/go index 2cf16c6..4644de3 120000 --- a/bin/go +++ b/bin/go @@ -1 +1 @@ -.go-1.23.1.pkg \ No newline at end of file +.go-1.23.2.pkg \ No newline at end of file diff --git a/bin/gofmt b/bin/gofmt index 2cf16c6..4644de3 120000 --- a/bin/gofmt +++ b/bin/gofmt @@ -1 +1 @@ -.go-1.23.1.pkg \ No newline at end of file +.go-1.23.2.pkg \ No newline at end of file From 07c8821614a71de68c99e73e964c9f03d75e5444 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B8=82=E5=B7=9D=E6=81=AD=E4=BD=91=20=28ebi=29?= <44393661+ebi-yade@users.noreply.github.com> Date: Sun, 27 Oct 2024 05:54:43 +0900 Subject: [PATCH 23/81] Fix broken anchor link in README.md (#464) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b2c023d..e3b9fe9 100644 --- a/README.md +++ b/README.md @@ -748,7 +748,7 @@ The default help output is usually sufficient, but if not there are two solution ### `Bind(...)` - bind values for callback hooks and Run() methods -See the [section on hooks](#hooks-beforeresolve-beforeapply-afterapply-and-the-bind-option) for details. +See the [section on hooks](#hooks-beforereset-beforeresolve-beforeapply-afterapply-and-the-bind-option) for details. ### Other options From 373692af87b177d48898c89ad53b6054f5b339bf Mon Sep 17 00:00:00 2001 From: Alec Thomas Date: Fri, 1 Nov 2024 11:55:27 +1100 Subject: [PATCH 24/81] refactor: reuse callAnyFunction for callFunction Rather than duplicating basically identical calling logic --- callbacks.go | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/callbacks.go b/callbacks.go index 45ef0d2..2d6507a 100644 --- a/callbacks.go +++ b/callbacks.go @@ -78,28 +78,19 @@ 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).Implements(callbackReturnSignature) { return fmt.Errorf("return value of %s must implement \"error\"", t) } - for i := 0; i < t.NumIn(); i++ { - pt := t.In(i) - if argf, ok := bindings[pt]; ok { - argv, err := argf() - if err != nil { - return err - } - in = append(in, argv) - } else { - return fmt.Errorf("couldn't find binding of type %s for parameter %d of %s(), use kong.Bind(%s)", pt, i, t, pt) - } + out, err := callAnyFunction(f, bindings) + if err != nil { + return err } - out := f.Call(in) - if out[0].IsNil() { + ferr := out[0] + if ferrv := reflect.ValueOf(ferr); !ferrv.IsValid() || ferrv.IsNil() { return nil } - return out[0].Interface().(error) //nolint + return ferr.(error) //nolint:forcetypeassert } func callAnyFunction(f reflect.Value, bindings bindings) (out []any, err error) { From 7bbb0b76ada1610f18cf71c54cca74209da88bd8 Mon Sep 17 00:00:00 2001 From: Alec Thomas Date: Fri, 1 Nov 2024 12:23:32 +1100 Subject: [PATCH 25/81] feat: support recursive injection of provider parameters This allows provider functions to accept parameters that are injected by other bindings or binding providers, eg. call the provider function with the root CLI struct (which is automatically bound by Kong): kong.BindToProvider(func(cli *CLI) (*Injected, error) { ... }) --- callbacks.go | 42 ++++++++++++++++++++---------------------- options.go | 6 +++++- options_test.go | 7 ++++++- 3 files changed, 31 insertions(+), 24 deletions(-) diff --git a/callbacks.go b/callbacks.go index 2d6507a..c84f215 100644 --- a/callbacks.go +++ b/callbacks.go @@ -6,7 +6,10 @@ import ( "strings" ) -type bindings map[reflect.Type]func() (reflect.Value, error) +// A map of type to function that returns a value of that type. +// +// The function should have the signature func(...) (T, error). Arguments are recursively resolved. +type bindings map[reflect.Type]any func (b bindings) String() string { out := []string{} @@ -19,32 +22,23 @@ func (b bindings) String() string { func (b bindings) add(values ...interface{}) bindings { for _, v := range values { v := v - b[reflect.TypeOf(v)] = func() (reflect.Value, error) { return reflect.ValueOf(v), nil } + b[reflect.TypeOf(v)] = func() (any, error) { return v, nil } } 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 } + b[reflect.TypeOf(iface).Elem()] = func() (any, error) { return impl, 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) + if t.Kind() != reflect.Func || 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 - } + b[rt] = provider return nil } @@ -101,15 +95,19 @@ func callAnyFunction(f reflect.Value, bindings bindings) (out []any, err error) 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 { + argf, ok := bindings[pt] + if !ok { return nil, fmt.Errorf("couldn't find binding of type %s for parameter %d of %s(), use kong.Bind(%s)", pt, i, t, pt) } + // Recursively resolve binding functions. + argv, err := callAnyFunction(reflect.ValueOf(argf), bindings) + if err != nil { + return nil, fmt.Errorf("%s: %w", pt, err) + } + if ferrv := reflect.ValueOf(argv[len(argv)-1]); ferrv.IsValid() && !ferrv.IsNil() { + return nil, ferrv.Interface().(error) //nolint:forcetypeassert + } + in = append(in, reflect.ValueOf(argv[0])) } outv := f.Call(in) out = make([]any, len(outv)) diff --git a/options.go b/options.go index 3ab383b..3bc991b 100644 --- a/options.go +++ b/options.go @@ -208,7 +208,11 @@ func BindTo(impl, iface interface{}) Option { }) } -// BindToProvider allows binding of provider functions. +// BindToProvider binds an injected value to a provider function. +// +// The provider function must have the signature: +// +// func() (interface{}, error) // // This is useful when the Run() function of different commands require different values that may // not all be initialisable from the main() function. diff --git a/options_test.go b/options_test.go index ca41720..f6971f8 100644 --- a/options_test.go +++ b/options_test.go @@ -89,11 +89,13 @@ func TestCallbackCustomError(t *testing.T) { } type bindToProviderCLI struct { + Filled bool `default:"true"` Called bool Cmd bindToProviderCmd `cmd:""` } type boundThing struct { + Filled bool } type bindToProviderCmd struct{} @@ -105,7 +107,10 @@ func (*bindToProviderCmd) Run(cli *bindToProviderCLI, b *boundThing) error { func TestBindToProvider(t *testing.T) { var cli bindToProviderCLI - app, err := New(&cli, BindToProvider(func() (*boundThing, error) { return &boundThing{}, nil })) + app, err := New(&cli, BindToProvider(func(cli *bindToProviderCLI) (*boundThing, error) { + assert.True(t, cli.Filled, "CLI struct should have already been populated by Kong") + return &boundThing{Filled: cli.Filled}, nil + })) assert.NoError(t, err) ctx, err := app.Parse([]string{"cmd"}) assert.NoError(t, err) From 64229c9fe78f0cfabce5c3713508ca9bb23b1a09 Mon Sep 17 00:00:00 2001 From: Alec Thomas Date: Sun, 3 Nov 2024 14:00:32 +1100 Subject: [PATCH 26/81] fix: format enum value Fixed in #415 --- context.go | 2 +- kong_test.go | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/context.go b/context.go index 2ed5d28..f174e63 100644 --- a/context.go +++ b/context.go @@ -996,7 +996,7 @@ func checkEnum(value *Value, target reflect.Value) error { } enums = append(enums, fmt.Sprintf("%q", enum)) } - return fmt.Errorf("%s must be one of %s but got %q", value.ShortSummary(), strings.Join(enums, ","), target.Interface()) + return fmt.Errorf("%s must be one of %s but got %q", value.ShortSummary(), strings.Join(enums, ","), fmt.Sprintf("%v", target.Interface())) } } diff --git a/kong_test.go b/kong_test.go index 4785dd0..9547dd4 100644 --- a/kong_test.go +++ b/kong_test.go @@ -2291,3 +2291,13 @@ func TestEnumPtrOmittedNoDefault(t *testing.T) { assert.NotZero(t, ctx) assert.Zero(t, cli.X) } + +func TestIntEnum(t *testing.T) { + var cli struct { + Enum int `enum:"1,2,3" default:"1"` + } + k, err := kong.New(&cli) + assert.NoError(t, err) + _, err = k.Parse([]string{"--enum=123"}) + assert.EqualError(t, err, `--enum must be one of "1","2","3" but got "123"`) +} From 1b9d57eec1a2667072589e00d6ad4b63beba9c46 Mon Sep 17 00:00:00 2001 From: Alec Thomas Date: Sun, 3 Nov 2024 14:14:55 +1100 Subject: [PATCH 27/81] feat: support optionally passing kong.Context to Validate() Fixes #340 --- README.md | 9 +++++++-- context.go | 15 +++++++++++++-- kong_test.go | 13 +++++++++++++ 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index e3b9fe9..5dbed02 100644 --- a/README.md +++ b/README.md @@ -663,8 +663,7 @@ func main() { ## Validation Kong does validation on the structure of a command-line, but also supports -extensible validation. Any node in the tree may implement the following -interface: +extensible validation. Any node in the tree may implement either of the following interfaces: ```go type Validatable interface { @@ -672,6 +671,12 @@ type Validatable interface { } ``` +```go +type Validatable interface { + Validate(kctx *kong.Context) error + } +``` + If one of these nodes is in the active command-line it will be called during normal validation. diff --git a/context.go b/context.go index f174e63..38a2f9b 100644 --- a/context.go +++ b/context.go @@ -208,7 +208,7 @@ func (c *Context) Validate() error { //nolint: gocyclo desc = node.Path() } if validate := isValidatable(value); validate != nil { - if err := validate.Validate(); err != nil { + if err := validate.Validate(c); err != nil { if desc != "" { return fmt.Errorf("%s: %w", desc, err) } @@ -1094,12 +1094,23 @@ func findPotentialCandidates(needle string, haystack []string, format string, ar } type validatable interface{ Validate() error } +type extendedValidatable interface { + Validate(kctx *Context) error +} -func isValidatable(v reflect.Value) validatable { +// Proxy a validatable function to the extendedValidatable interface +type validatableFunc func() error + +func (f validatableFunc) Validate(kctx *Context) error { return f() } + +func isValidatable(v reflect.Value) extendedValidatable { if !v.IsValid() || (v.Kind() == reflect.Ptr || v.Kind() == reflect.Slice || v.Kind() == reflect.Map) && v.IsNil() { return nil } if validate, ok := v.Interface().(validatable); ok { + return validatableFunc(validate.Validate) + } + if validate, ok := v.Interface().(extendedValidatable); ok { return validate } if v.CanAddr() { diff --git a/kong_test.go b/kong_test.go index 9547dd4..e0efac8 100644 --- a/kong_test.go +++ b/kong_test.go @@ -1466,6 +1466,19 @@ func TestValidateArg(t *testing.T) { assert.EqualError(t, err, ": flag error") } +type extendedValidateFlag string + +func (v *extendedValidateFlag) Validate(kctx *kong.Context) error { return errors.New("flag error") } + +func TestExtendedValidateFlag(t *testing.T) { + cli := struct { + Flag extendedValidateFlag + }{} + p := mustNew(t, &cli) + _, err := p.Parse([]string{"--flag=one"}) + assert.EqualError(t, err, "--flag: flag error") +} + func TestPointers(t *testing.T) { cli := struct { Mapped *mappedValue From d0beaf7df321c4630d1e6b0460882038c498e0ca Mon Sep 17 00:00:00 2001 From: Alec Thomas Date: Sun, 3 Nov 2024 14:28:27 +1100 Subject: [PATCH 28/81] fix: update enum+default vars after interpolation This doesn't recursively apply interpolation, as discussed in the issue, but that's a much bigger change. Fixes #337 --- kong.go | 10 +++++----- kong_test.go | 13 +++++++++++++ 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/kong.go b/kong.go index 10235d0..45479be 100644 --- a/kong.go +++ b/kong.go @@ -249,19 +249,19 @@ func (k *Kong) interpolateValue(value *Value, vars Vars) (err error) { 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) } + updatedVars := 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 { + if value.Flag.Envs[i], err = interpolate(env, vars, updatedVars); err != nil { return fmt.Errorf("env value for %s: %s", value.Summary(), err) } } diff --git a/kong_test.go b/kong_test.go index e0efac8..61099d4 100644 --- a/kong_test.go +++ b/kong_test.go @@ -2314,3 +2314,16 @@ func TestIntEnum(t *testing.T) { _, err = k.Parse([]string{"--enum=123"}) assert.EqualError(t, err, `--enum must be one of "1","2","3" but got "123"`) } + +func TestRecursiveVariableExpansion(t *testing.T) { + var cli struct { + Config string `type:"path" default:"${config_file}" help:"Default: ${default}"` + } + k := mustNew(t, &cli, kong.Vars{"config_file": "/etc/config"}, kong.Exit(func(int) {})) + w := &strings.Builder{} + k.Stderr = w + k.Stdout = w + _, err := k.Parse([]string{"--help"}) + assert.NoError(t, err) + assert.Contains(t, w.String(), "Default: /etc/config") +} From 2544d3f00855f6857cf121ce04608296b5858e4a Mon Sep 17 00:00:00 2001 From: Alec Thomas Date: Sun, 3 Nov 2024 14:48:36 +1100 Subject: [PATCH 29/81] feat: add AfterRun() hook Fixes #288 --- context.go | 22 +++++++++++++--------- go.mod | 2 +- go.sum | 2 -- hooks.go | 13 ++++++++++--- kong_test.go | 25 +++++++++++++++++++++++++ 5 files changed, 49 insertions(+), 15 deletions(-) diff --git a/context.go b/context.go index 38a2f9b..8a8c2e1 100644 --- a/context.go +++ b/context.go @@ -809,18 +809,22 @@ func (c *Context) RunNode(node *Node, binds ...interface{}) (err error) { func (c *Context) Run(binds ...interface{}) (err error) { 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...) - } + if len(c.Path) == 0 { + return fmt.Errorf("no command selected") + } + selected := c.Path[0].Node() + if selected.Type == ApplicationNode { + method := getMethod(selected.Target, "Run") + if method.IsValid() { + node = selected } + } else { + return fmt.Errorf("no command selected") } - return fmt.Errorf("no command selected") } - return c.RunNode(node, binds...) + runErr := c.RunNode(node, binds...) + err = c.Kong.applyHook(c, "AfterRun") + return errors.Join(runErr, err) } // PrintUsage to Kong's stdout. diff --git a/go.mod b/go.mod index a128f6e..411174d 100644 --- a/go.mod +++ b/go.mod @@ -7,4 +7,4 @@ require ( require github.com/hexops/gotextdiff v1.0.3 // indirect -go 1.18 +go 1.20 diff --git a/go.sum b/go.sum index c03ffd4..f571a34 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,3 @@ -github.com/alecthomas/assert/v2 v2.10.0 h1:jjRCHsj6hBJhkmhznrCzoNpbA3zqy0fYiUcYZP/GkPY= -github.com/alecthomas/assert/v2 v2.10.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= diff --git a/hooks.go b/hooks.go index d166b08..9fdf24c 100644 --- a/hooks.go +++ b/hooks.go @@ -3,17 +3,24 @@ package kong // BeforeResolve is a documentation-only interface describing hooks that run before resolvers are applied. type BeforeResolve interface { // This is not the correct signature - see README for details. - BeforeResolve(args ...interface{}) error + BeforeResolve(args ...any) error } // BeforeApply is a documentation-only interface describing hooks that run before values are set. type BeforeApply interface { // This is not the correct signature - see README for details. - BeforeApply(args ...interface{}) error + BeforeApply(args ...any) error } // AfterApply is a documentation-only interface describing hooks that run after values are set. type AfterApply interface { // This is not the correct signature - see README for details. - AfterApply(args ...interface{}) error + AfterApply(args ...any) error +} + +// AfterRun is a documentation-only interface describing hooks that run after Run() returns. +type AfterRun interface { + // This is not the correct signature - see README for details. + // AfterRun is called after Run() returns. + AfterRun(args ...any) error } diff --git a/kong_test.go b/kong_test.go index 61099d4..36e18e1 100644 --- a/kong_test.go +++ b/kong_test.go @@ -2327,3 +2327,28 @@ func TestRecursiveVariableExpansion(t *testing.T) { assert.NoError(t, err) assert.Contains(t, w.String(), "Default: /etc/config") } + +type afterRunCLI struct { + runCalled bool `kong:"-"` + afterRunCalled bool `kong:"-"` +} + +func (c *afterRunCLI) Run() error { + c.runCalled = true + return nil +} + +func (c *afterRunCLI) AfterRun() error { + c.afterRunCalled = true + return nil +} + +func TestAfterRun(t *testing.T) { + var cli afterRunCLI + k := mustNew(t, &cli) + kctx, err := k.Parse([]string{}) + assert.NoError(t, err) + err = kctx.Run() + assert.NoError(t, err) + assert.Equal(t, afterRunCLI{runCalled: true, afterRunCalled: true}, cli) +} From c90c6732cc4d66d4ee45212f37ca2b19d306941d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 4 Nov 2024 13:51:07 +1100 Subject: [PATCH 30/81] chore(deps): update module github.com/alecthomas/kong to v1.3.0 (#467) --- _examples/server/go.mod | 2 +- _examples/server/go.sum | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/_examples/server/go.mod b/_examples/server/go.mod index 5f3a811..369c16d 100644 --- a/_examples/server/go.mod +++ b/_examples/server/go.mod @@ -4,7 +4,7 @@ go 1.13 require ( github.com/alecthomas/colour v0.1.0 - github.com/alecthomas/kong v1.2.1 + github.com/alecthomas/kong v1.3.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 diff --git a/_examples/server/go.sum b/_examples/server/go.sum index dcddb65..33ed461 100644 --- a/_examples/server/go.sum +++ b/_examples/server/go.sum @@ -1,6 +1,7 @@ github.com/alecthomas/assert/v2 v2.1.0/go.mod h1:b/+1DI2Q6NckYi+3mXyH3wFb8qG37K/DuK80n7WefXA= github.com/alecthomas/assert/v2 v2.6.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= github.com/alecthomas/assert/v2 v2.10.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= +github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= 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.1 h1:V1tLBhyQBC4rsbXbcOvm3GBaytJSwRNX69fp1WJxbqQ= @@ -11,6 +12,8 @@ github.com/alecthomas/kong v0.9.0 h1:G5diXxc85KvoV2f0ZRVuMsi45IrBgx9zDNGNj165aPA github.com/alecthomas/kong v0.9.0/go.mod h1:Y47y5gKfHp1hDc7CH7OeXgLIpp+Q2m1Ni0L5s3bI8Os= github.com/alecthomas/kong v1.2.1 h1:E8jH4Tsgv6wCRX2nGrdPyHDUCSG83WH2qE4XLACD33Q= github.com/alecthomas/kong v1.2.1/go.mod h1:rKTSFhbdp3Ryefn8x5MOEprnRFQ7nlmMC01GKhehhBM= +github.com/alecthomas/kong v1.3.0 h1:YJKuU6/TV2XOBtymafSeuzDvLAFR8cYMZiXVNLhAO6g= +github.com/alecthomas/kong v1.3.0/go.mod h1:IDc8HyiouDdpdiEiY81iaEJM8rSIW6LzX8On4FCO0bE= 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= From f388f6cd39ae7fdbc4ffc75205d16a798c5a56bf Mon Sep 17 00:00:00 2001 From: Alec Thomas Date: Wed, 6 Nov 2024 09:35:14 +1100 Subject: [PATCH 31/81] fix: NPE due to checking if error is nil when it can be a value Fixes #468 --- callbacks.go | 2 +- options_test.go | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/callbacks.go b/callbacks.go index c84f215..1df975d 100644 --- a/callbacks.go +++ b/callbacks.go @@ -81,7 +81,7 @@ func callFunction(f reflect.Value, bindings bindings) error { return err } ferr := out[0] - if ferrv := reflect.ValueOf(ferr); !ferrv.IsValid() || ferrv.IsNil() { + if ferrv := reflect.ValueOf(ferr); !ferrv.IsValid() || ((ferrv.Kind() == reflect.Interface || ferrv.Kind() == reflect.Pointer) && ferrv.IsNil()) { return nil } return ferr.(error) //nolint:forcetypeassert diff --git a/options_test.go b/options_test.go index f6971f8..e549475 100644 --- a/options_test.go +++ b/options_test.go @@ -127,3 +127,22 @@ func TestFlagNamer(t *testing.T) { assert.NoError(t, err) assert.Equal(t, "SOMEFLAG", app.Model.Flags[1].Name) } + +type npError string + +func (e npError) Error() string { + return "ERROR: " + string(e) +} + +func TestCallbackNonPointerError(t *testing.T) { + method := func() error { + return npError("failed") + } + + var cli struct{} + + p, err := New(&cli) + assert.NoError(t, err) + err = callFunction(reflect.ValueOf(method), p.bindings) + assert.EqualError(t, err, "ERROR: failed") +} From 5221ebb8fb83249597a43fa977170e1803890fc8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 15 Nov 2024 17:24:56 +1100 Subject: [PATCH 32/81] chore(deps): update all non-major dependencies (#469) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- _examples/server/go.mod | 4 ++-- _examples/server/go.sum | 10 ++++++++++ bin/{.go-1.23.2.pkg => .go-1.23.3.pkg} | 0 bin/go | 2 +- bin/gofmt | 2 +- 5 files changed, 14 insertions(+), 4 deletions(-) rename bin/{.go-1.23.2.pkg => .go-1.23.3.pkg} (100%) diff --git a/_examples/server/go.mod b/_examples/server/go.mod index 369c16d..5c166f8 100644 --- a/_examples/server/go.mod +++ b/_examples/server/go.mod @@ -4,7 +4,7 @@ go 1.13 require ( github.com/alecthomas/colour v0.1.0 - github.com/alecthomas/kong v1.3.0 + github.com/alecthomas/kong v1.4.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 @@ -12,5 +12,5 @@ require ( 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.28.0 + golang.org/x/crypto v0.29.0 ) diff --git a/_examples/server/go.sum b/_examples/server/go.sum index 33ed461..f5b648b 100644 --- a/_examples/server/go.sum +++ b/_examples/server/go.sum @@ -14,6 +14,8 @@ github.com/alecthomas/kong v1.2.1 h1:E8jH4Tsgv6wCRX2nGrdPyHDUCSG83WH2qE4XLACD33Q github.com/alecthomas/kong v1.2.1/go.mod h1:rKTSFhbdp3Ryefn8x5MOEprnRFQ7nlmMC01GKhehhBM= github.com/alecthomas/kong v1.3.0 h1:YJKuU6/TV2XOBtymafSeuzDvLAFR8cYMZiXVNLhAO6g= github.com/alecthomas/kong v1.3.0/go.mod h1:IDc8HyiouDdpdiEiY81iaEJM8rSIW6LzX8On4FCO0bE= +github.com/alecthomas/kong v1.4.0 h1:UL7tzGMnnY0YRMMvJyITIRX1EpO6RbBRZDNcCevy3HA= +github.com/alecthomas/kong v1.4.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= @@ -84,6 +86,8 @@ golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= +golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= +golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= 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= @@ -107,6 +111,7 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -140,6 +145,8 @@ golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= +golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -167,6 +174,8 @@ golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM= golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= +golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU= +golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -181,6 +190,7 @@ golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= diff --git a/bin/.go-1.23.2.pkg b/bin/.go-1.23.3.pkg similarity index 100% rename from bin/.go-1.23.2.pkg rename to bin/.go-1.23.3.pkg diff --git a/bin/go b/bin/go index 4644de3..a1a9b7e 120000 --- a/bin/go +++ b/bin/go @@ -1 +1 @@ -.go-1.23.2.pkg \ No newline at end of file +.go-1.23.3.pkg \ No newline at end of file diff --git a/bin/gofmt b/bin/gofmt index 4644de3..a1a9b7e 120000 --- a/bin/gofmt +++ b/bin/gofmt @@ -1 +1 @@ -.go-1.23.2.pkg \ No newline at end of file +.go-1.23.3.pkg \ No newline at end of file From 88e13d750a44855f5517ab596191697e01dc94e2 Mon Sep 17 00:00:00 2001 From: Alec Thomas Date: Sun, 1 Dec 2024 11:12:48 +1100 Subject: [PATCH 33/81] chore: fix all golangci-lint warnings --- .golangci.yml | 20 ++++---------------- context.go | 3 ++- kong.go | 2 +- 3 files changed, 7 insertions(+), 18 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index c922a37..844092f 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -7,7 +7,6 @@ output: linters: enable-all: true disable: - - maligned - lll - gochecknoglobals - wsl @@ -17,11 +16,8 @@ linters: - goprintffuncname - paralleltest - nlreturn - - goerr113 - - ifshort - testpackage - wrapcheck - - exhaustivestruct - forbidigo - gci - godot @@ -29,9 +25,6 @@ linters: - cyclop - errorlint - nestif - - golint - - scopelint - - interfacer - tagliatelle - thelper - godox @@ -41,18 +34,17 @@ linters: - 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 - mnd - perfsprint + - err113 + - copyloopvar + - intrange + - execinquery linters-settings: govet: - check-shadowing: true # These govet checks are disabled by default, but they're useful. enable: - niliness @@ -90,7 +82,3 @@ issues: # Duplicate words are okay in tests. - linters: [dupword] path: _test\.go - - - linters: [maintidx] - path: context\.go - text: 'Function name: trace' diff --git a/context.go b/context.go index 8a8c2e1..77643ae 100644 --- a/context.go +++ b/context.go @@ -347,6 +347,7 @@ func (c *Context) endParsing() { } } +//nolint:maintidx func (c *Context) trace(node *Node) (err error) { //nolint: gocyclo positional := 0 node.Active = true @@ -758,7 +759,7 @@ 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) { fv := reflect.ValueOf(fn) - bindings := c.Kong.bindings.clone().add(binds...).add(c).merge(c.bindings) //nolint:govet + bindings := c.Kong.bindings.clone().add(binds...).add(c).merge(c.bindings) return callAnyFunction(fv, bindings) } diff --git a/kong.go b/kong.go index 45479be..764a994 100644 --- a/kong.go +++ b/kong.go @@ -117,7 +117,7 @@ func New(grammar interface{}, options ...Option) (*Kong, error) { // Embed any embedded structs. for _, embed := range k.embedded { - tag, err := parseTagString(strings.Join(embed.tags, " ")) //nolint:govet + tag, err := parseTagString(strings.Join(embed.tags, " ")) if err != nil { return nil, err } From 96647c30af56cdf1a273f3f95de0b73c6d7a0b38 Mon Sep 17 00:00:00 2001 From: Alec Thomas Date: Sun, 1 Dec 2024 19:58:24 +1100 Subject: [PATCH 34/81] feat: add old "passthrough" behaviour back in as an option `passthrough:""` or `passthrough:"all"` (the default) will pass through all further arguments including unrecognised flags. `passthrough:"partial"` will validate flags up until the `--` or the first positional argument, then pass through all subsequent flags and arguments. --- README.md | 6 +++- build.go | 23 +++++++-------- context.go | 4 +-- kong_test.go | 29 +++++++++++++++++++ model.go | 35 ++++++++++++----------- tag.go | 80 +++++++++++++++++++++++++++++++++------------------- 6 files changed, 117 insertions(+), 60 deletions(-) diff --git a/README.md b/README.md index 5dbed02..10b6128 100644 --- a/README.md +++ b/README.md @@ -590,9 +590,13 @@ Both can coexist with standard Tag parsing. | `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. | +| `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. | | `-` | Ignore the field. Useful for adding non-CLI fields to a configuration struct. e.g `` `kong:"-"` `` | +[^1]: `` can be `partial` or `all` (the default). `all` will pass through all arguments including flags, including +flags. `partial` will validate flags until the first positional argument is encountered, then pass through all remaining +positional arguments. + ## Plugins Kong CLI's can be extended by embedding the `kong.Plugin` type and populating it with pointers to Kong annotated structs. For example: diff --git a/build.go b/build.go index 287beeb..42d30f0 100644 --- a/build.go +++ b/build.go @@ -281,17 +281,18 @@ func buildField(k *Kong, node *Node, v reflect.Value, ft reflect.StructField, fv } 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, + 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, + PassthroughMode: tag.PassthroughMode, // Flags are optional by default, and args are required by default. Required: (!tag.Arg && tag.Required) || (tag.Arg && !tag.Optional), diff --git a/context.go b/context.go index 77643ae..b339f6b 100644 --- a/context.go +++ b/context.go @@ -425,7 +425,7 @@ func (c *Context) trace(node *Node) (err error) { //nolint: gocyclo case FlagToken: if err := c.parseFlag(flags, token.String()); err != nil { - if isUnknownFlagError(err) && positional < len(node.Positional) && node.Positional[positional].Passthrough { + if isUnknownFlagError(err) && positional < len(node.Positional) && node.Positional[positional].PassthroughMode == PassThroughModeAll { c.scan.Pop() c.scan.PushTyped(token.String(), PositionalArgumentToken) } else { @@ -435,7 +435,7 @@ func (c *Context) trace(node *Node) (err error) { //nolint: gocyclo case ShortFlagToken: if err := c.parseFlag(flags, token.String()); err != nil { - if isUnknownFlagError(err) && positional < len(node.Positional) && node.Positional[positional].Passthrough { + if isUnknownFlagError(err) && positional < len(node.Positional) && node.Positional[positional].PassthroughMode == PassThroughModeAll { c.scan.Pop() c.scan.PushTyped(token.String(), PositionalArgumentToken) } else { diff --git a/kong_test.go b/kong_test.go index 36e18e1..bd47185 100644 --- a/kong_test.go +++ b/kong_test.go @@ -1803,6 +1803,35 @@ func TestPassthroughArgs(t *testing.T) { } } +func TestPassthroughPartial(t *testing.T) { + var cli struct { + Flag string + Args []string `arg:"" optional:"" passthrough:"partial"` + } + p := mustNew(t, &cli) + _, err := p.Parse([]string{"--flag", "foobar", "something"}) + assert.NoError(t, err) + assert.Equal(t, "foobar", cli.Flag) + assert.Equal(t, []string{"something"}, cli.Args) + _, err = p.Parse([]string{"--invalid", "foobar", "something"}) + assert.EqualError(t, err, "unknown flag --invalid") +} + +func TestPassthroughAll(t *testing.T) { + var cli struct { + Flag string + Args []string `arg:"" optional:"" passthrough:"all"` + } + p := mustNew(t, &cli) + _, err := p.Parse([]string{"--flag", "foobar", "something"}) + assert.NoError(t, err) + assert.Equal(t, "foobar", cli.Flag) + assert.Equal(t, []string{"something"}, cli.Args) + _, err = p.Parse([]string{"--invalid", "foobar", "something"}) + assert.NoError(t, err) + assert.Equal(t, []string{"--invalid", "foobar", "something"}, cli.Args) +} + func TestPassthroughCmd(t *testing.T) { tests := []struct { name string diff --git a/model.go b/model.go index 3190f3e..25ffe96 100644 --- a/model.go +++ b/model.go @@ -239,23 +239,24 @@ func (n *Node) ClosestGroup() *Group { // A Value is either a flag or a variable positional argument. 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 - Mapper Mapper - Tag *Tag - Target reflect.Value - Required bool - 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. + 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 + Mapper Mapper + Tag *Tag + Target reflect.Value + Required bool + 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 // Deprecated: Use PassthroughMode instead. Set to true to stop flag parsing when encountered. + PassthroughMode PassthroughMode // + Active bool // Denotes the value is part of an active branch in the CLI. } // EnumMap returns a map of the enums in this value. diff --git a/tag.go b/tag.go index 456f7ae..00fb7e7 100644 --- a/tag.go +++ b/tag.go @@ -9,37 +9,50 @@ import ( "unicode/utf8" ) +// PassthroughMode indicates how parameters are passed through when "passthrough" is set. +type PassthroughMode int + +const ( + // PassThroughModeNone indicates passthrough mode is disabled. + PassThroughModeNone PassthroughMode = iota + // PassThroughModeAll indicates that all parameters, including flags, are passed through. It is the default. + PassThroughModeAll + // PassThroughModePartial will validate flags until the first positional argument is encountered, then pass through all remaining positional arguments. + PassThroughModePartial +) + // Tag represents the parsed state of Kong tags in a struct field tag. type Tag struct { - Ignored bool // Field is ignored by Kong. ie. kong:"-" - Cmd bool - Arg bool - Required bool - Optional bool - Name string - Help string - Type string - TypeName string - HasDefault bool - Default string - Format string - PlaceHolder string - Envs []string - Short rune - Hidden bool - Sep rune - MapSep rune - Enum string - Group string - Xor []string - And []string - Vars Vars - Prefix string // Optional prefix on anonymous structs. All sub-flags will have this prefix. - EnvPrefix string - Embed bool - Aliases []string - Negatable string - Passthrough bool + Ignored bool // Field is ignored by Kong. ie. kong:"-" + Cmd bool + Arg bool + Required bool + Optional bool + Name string + Help string + Type string + TypeName string + HasDefault bool + Default string + Format string + PlaceHolder string + Envs []string + Short rune + Hidden bool + Sep rune + MapSep rune + Enum string + Group string + Xor []string + And []string + Vars Vars + Prefix string // Optional prefix on anonymous structs. All sub-flags will have this prefix. + EnvPrefix string + Embed bool + Aliases []string + Negatable string + Passthrough bool // Deprecated: use PassthroughMode instead. + PassthroughMode PassthroughMode // Storage for all tag keys for arbitrary lookups. items map[string][]string @@ -289,6 +302,15 @@ func hydrateTag(t *Tag, typ reflect.Type) error { //nolint: gocyclo return fmt.Errorf("passthrough only makes sense for positional arguments or commands") } t.Passthrough = passthrough + passthroughMode := t.Get("passthrough") + switch passthroughMode { + case "partial": + t.PassthroughMode = PassThroughModePartial + case "all", "": + t.PassthroughMode = PassThroughModeAll + default: + return fmt.Errorf("invalid passthrough mode %q, must be one of 'partial' or 'all'", passthroughMode) + } return nil } From 81d0c29303442ba216395c690b07526ede3ca34a Mon Sep 17 00:00:00 2001 From: Alec Thomas Date: Tue, 3 Dec 2024 08:22:10 +1100 Subject: [PATCH 35/81] fix: regression where all args became passthrough Fixes #475 --- tag.go | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/tag.go b/tag.go index 00fb7e7..226171b 100644 --- a/tag.go +++ b/tag.go @@ -302,14 +302,16 @@ func hydrateTag(t *Tag, typ reflect.Type) error { //nolint: gocyclo return fmt.Errorf("passthrough only makes sense for positional arguments or commands") } t.Passthrough = passthrough - passthroughMode := t.Get("passthrough") - switch passthroughMode { - case "partial": - t.PassthroughMode = PassThroughModePartial - case "all", "": - t.PassthroughMode = PassThroughModeAll - default: - return fmt.Errorf("invalid passthrough mode %q, must be one of 'partial' or 'all'", passthroughMode) + if t.Passthrough { + passthroughMode := t.Get("passthrough") + switch passthroughMode { + case "partial": + t.PassthroughMode = PassThroughModePartial + case "all", "": + t.PassthroughMode = PassThroughModeAll + default: + return fmt.Errorf("invalid passthrough mode %q, must be one of 'partial' or 'all'", passthroughMode) + } } return nil } From 87ee7dc3dbfa9f97fa17f5f32e37343dc6f53608 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Dec 2024 10:29:00 +1100 Subject: [PATCH 36/81] chore(deps): update all non-major dependencies (#474) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- _examples/server/go.mod | 4 ++-- _examples/server/go.sum | 10 ++++++++++ bin/{.go-1.23.3.pkg => .go-1.23.4.pkg} | 0 bin/go | 2 +- bin/gofmt | 2 +- 5 files changed, 14 insertions(+), 4 deletions(-) rename bin/{.go-1.23.3.pkg => .go-1.23.4.pkg} (100%) diff --git a/_examples/server/go.mod b/_examples/server/go.mod index 5c166f8..b65caba 100644 --- a/_examples/server/go.mod +++ b/_examples/server/go.mod @@ -4,7 +4,7 @@ go 1.13 require ( github.com/alecthomas/colour v0.1.0 - github.com/alecthomas/kong v1.4.0 + github.com/alecthomas/kong v1.5.1 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 @@ -12,5 +12,5 @@ require ( 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.29.0 + golang.org/x/crypto v0.30.0 ) diff --git a/_examples/server/go.sum b/_examples/server/go.sum index f5b648b..5aea24b 100644 --- a/_examples/server/go.sum +++ b/_examples/server/go.sum @@ -16,6 +16,8 @@ github.com/alecthomas/kong v1.3.0 h1:YJKuU6/TV2XOBtymafSeuzDvLAFR8cYMZiXVNLhAO6g github.com/alecthomas/kong v1.3.0/go.mod h1:IDc8HyiouDdpdiEiY81iaEJM8rSIW6LzX8On4FCO0bE= 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/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= @@ -88,6 +90,8 @@ golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= 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/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= @@ -112,6 +116,7 @@ golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -147,6 +152,8 @@ golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -176,6 +183,8 @@ golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU= golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E= +golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= +golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -191,6 +200,7 @@ golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= diff --git a/bin/.go-1.23.3.pkg b/bin/.go-1.23.4.pkg similarity index 100% rename from bin/.go-1.23.3.pkg rename to bin/.go-1.23.4.pkg diff --git a/bin/go b/bin/go index a1a9b7e..3c91a6c 120000 --- a/bin/go +++ b/bin/go @@ -1 +1 @@ -.go-1.23.3.pkg \ No newline at end of file +.go-1.23.4.pkg \ No newline at end of file diff --git a/bin/gofmt b/bin/gofmt index a1a9b7e..3c91a6c 120000 --- a/bin/gofmt +++ b/bin/gofmt @@ -1 +1 @@ -.go-1.23.3.pkg \ No newline at end of file +.go-1.23.4.pkg \ No newline at end of file From 388ba35f1a32c1cf97108a7777b8a36a84dbc1a4 Mon Sep 17 00:00:00 2001 From: Alec Thomas Date: Sat, 7 Dec 2024 17:08:31 +1100 Subject: [PATCH 37/81] feat: add support for `Provide*() (, error)` methods on commands --- README.md | 80 +++++++++++++++++++++++++++------------------------- context.go | 13 +++++++++ kong_test.go | 25 ++++++++++++++++ 3 files changed, 79 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index 10b6128..ee4befe 100644 --- a/README.md +++ b/README.md @@ -5,43 +5,40 @@ [![](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) - - -- [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) - - [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) - - +- [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 @@ -755,9 +752,14 @@ The default help output is usually sufficient, but if not there are two solution 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 +### Injecting values into `Run()` methods -See the [section on hooks](#hooks-beforereset-beforeresolve-beforeapply-afterapply-and-the-bind-option) for details. +There are several ways to inject values into `Run()` methods: + +1. Use `Bind()` to bind values directly. +2. Use `BindTo()` to bind values to an interface type. +3. Use `BindToProvider()` to bind values to a function that provides the value. +4. Implement `Provide() error` methods on the command structure. ### Other options diff --git a/context.go b/context.go index b339f6b..fd168fa 100644 --- a/context.go +++ b/context.go @@ -782,6 +782,19 @@ func (c *Context) RunNode(node *Node, binds ...interface{}) (err error) { methodBinds = methodBinds.clone() for p := node; p != nil; p = p.Parent { methodBinds = methodBinds.add(p.Target.Addr().Interface()) + // Try value and pointer to value. + for _, p := range []reflect.Value{p.Target, p.Target.Addr()} { + t := p.Type() + for i := 0; i < p.NumMethod(); i++ { + methodt := t.Method(i) + if strings.HasPrefix(methodt.Name, "Provide") { + method := p.Method(i) + if err := methodBinds.addProvider(method.Interface()); err != nil { + return fmt.Errorf("%s.%s: %w", t.Name(), methodt.Name, err) + } + } + } + } } if method.IsValid() { methods = append(methods, targetMethod{node, method, methodBinds}) diff --git a/kong_test.go b/kong_test.go index bd47185..2b52758 100644 --- a/kong_test.go +++ b/kong_test.go @@ -2381,3 +2381,28 @@ func TestAfterRun(t *testing.T) { assert.NoError(t, err) assert.Equal(t, afterRunCLI{runCalled: true, afterRunCalled: true}, cli) } + +type ProvidedString string + +type providerCLI struct { + Sub providerSubCommand `cmd:""` +} + +type providerSubCommand struct{} + +func (p *providerCLI) ProvideFoo() (ProvidedString, error) { + return ProvidedString("foo"), nil +} + +func (p *providerSubCommand) Run(t *testing.T, ps ProvidedString) error { + assert.Equal(t, ProvidedString("foo"), ps) + return nil +} + +func TestProviderMethods(t *testing.T) { + k := mustNew(t, &providerCLI{}) + kctx, err := k.Parse([]string{"sub"}) + assert.NoError(t, err) + err = kctx.Run(t) + assert.NoError(t, err) +} From f2fcd34abfee0386bb50eeea6348b84b35607358 Mon Sep 17 00:00:00 2001 From: Alec Thomas Date: Sat, 7 Dec 2024 19:43:01 +1100 Subject: [PATCH 38/81] docs: fix README TOC --- README.md | 67 +++++++++++++++++++++++++++---------------------------- 1 file changed, 33 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index ee4befe..87de501 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 From 565ae9b740578e038691242cb239aaa1206e698c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 12 Dec 2024 22:32:41 +1100 Subject: [PATCH 39/81] chore(deps): update all non-major dependencies (#476) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- _examples/server/go.mod | 6 +++--- _examples/server/go.sum | 6 ++++++ 2 files changed, 9 insertions(+), 3 deletions(-) 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= From 840220c2ed477d11bf362a32428bc26da7a3051a Mon Sep 17 00:00:00 2001 From: Alec Thomas Date: Fri, 27 Dec 2024 13:41:00 +1100 Subject: [PATCH 40/81] feat: allow hooks to be declared on embedded fields Specifically, on Go embedded fields, not on fields tagged with `embed`. Fixes #90. --- README.md | 38 ++------------------------------------ callbacks.go | 27 +++++++++++++++++++++++++++ kong.go | 30 +++++++++++++----------------- kong_test.go | 33 +++++++++++++++++++++++++++++++++ 4 files changed, 75 insertions(+), 53 deletions(-) diff --git a/README.md b/README.md index 87de501..a3d8511 100644 --- a/README.md +++ b/README.md @@ -307,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. @@ -341,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. diff --git a/callbacks.go b/callbacks.go index 1df975d..9733e91 100644 --- a/callbacks.go +++ b/callbacks.go @@ -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/kong.go b/kong.go index 764a994..4f3bea2 100644 --- a/kong.go +++ b/kong.go @@ -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,13 +390,11 @@ 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) diff --git a/kong_test.go b/kong_test.go index 2b52758..cd3fd66 100644 --- a/kong_test.go +++ b/kong_test.go @@ -2406,3 +2406,36 @@ 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) +} From a14bb2072c00a4aa1fccf1dcad2a16ad230099e8 Mon Sep 17 00:00:00 2001 From: Alec Thomas Date: Sun, 29 Dec 2024 07:42:25 +0900 Subject: [PATCH 41/81] fix: don't call Apply() twice For some reason this was called by `Run()`. All tests pass without it, so I'm not sure why it was there. Fixes #481 --- context.go | 5 ----- kong_test.go | 24 ++++++++++++++++++++++++ 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/context.go b/context.go index fd168fa..b840c68 100644 --- a/context.go +++ b/context.go @@ -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 diff --git a/kong_test.go b/kong_test.go index cd3fd66..42c33da 100644 --- a/kong_test.go +++ b/kong_test.go @@ -2439,3 +2439,27 @@ func TestEmbeddedCallbacks(t *testing.T) { } 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) +} From 36257680f1d0b7bf1e63c751b2b5e7e8aaa78a2c Mon Sep 17 00:00:00 2001 From: Alec Thomas Date: Sun, 29 Dec 2024 08:02:45 +0900 Subject: [PATCH 42/81] refactor(test): replace os.Setenv() with t.Setenv() --- kong_test.go | 11 ++++----- resolver_test.go | 60 +++++++++++++++--------------------------------- 2 files changed, 23 insertions(+), 48 deletions(-) diff --git a/kong_test.go b/kong_test.go index 42c33da..36d4e96 100644 --- a/kong_test.go +++ b/kong_test.go @@ -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) diff --git a/resolver_test.go b/resolver_test.go index c1684b6..5c9eeda 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 interface{}, 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{}) From a32b94b70530e64716c954ff2be62d14f26cc7f6 Mon Sep 17 00:00:00 2001 From: Alec Thomas Date: Sun, 29 Dec 2024 08:10:34 +0900 Subject: [PATCH 43/81] chore: `interface{}` -> `any` --- README.md | 4 ++-- build.go | 4 ++-- callbacks.go | 6 +++--- config_test.go | 2 +- context.go | 18 +++++++++--------- defaults.go | 2 +- global.go | 2 +- help.go | 2 +- kong.go | 16 ++++++++-------- kong_test.go | 8 ++++---- mapper.go | 12 ++++++------ model.go | 2 +- options.go | 14 +++++++------- resolver.go | 12 ++++++------ resolver_test.go | 14 +++++++------- scanner.go | 8 ++++---- 16 files changed, 63 insertions(+), 63 deletions(-) diff --git a/README.md b/README.md index a3d8511..5595a41 100644 --- a/README.md +++ b/README.md @@ -648,7 +648,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). @@ -706,7 +706,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/build.go b/build.go index 42d30f0..166935b 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 { diff --git a/callbacks.go b/callbacks.go index 9733e91..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() { 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 b840c68..4fd2302 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 @@ -815,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 { @@ -1087,7 +1087,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 4f3bea2..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, @@ -401,7 +401,7 @@ func (k *Kong) applyHookToDefaultFlags(ctx *Context, node *Node, name string) er }) } -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 { @@ -417,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 36d4e96..fce739a 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"), @@ -1680,7 +1680,7 @@ func TestOptionReturnsErr(t *testing.T) { func TestEnumValidation(t *testing.T) { tests := []struct { name string - cli interface{} + cli any fail bool }{ { @@ -1954,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 }{ @@ -2004,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 }{ 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..f15e6fb 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") 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 5c9eeda..7ce7ec1 100644 --- a/resolver_test.go +++ b/resolver_test.go @@ -12,7 +12,7 @@ import ( type envMap map[string]string -func newEnvParser(t *testing.T, cli interface{}, env envMap, options ...kong.Option) *kong.Kong { +func newEnvParser(t *testing.T, cli any, env envMap, options ...kong.Option) *kong.Kong { t.Helper() for name, value := range env { t.Setenv(name, value) @@ -275,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 } @@ -294,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 } @@ -318,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 } @@ -336,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 } @@ -355,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 } From cacaace969ab71cfa8815a06590c3e0b03501ee6 Mon Sep 17 00:00:00 2001 From: Alec Thomas Date: Sun, 29 Dec 2024 17:31:07 +0900 Subject: [PATCH 44/81] fix: don't append ... for fields with an explicit type Fixes #346 --- kong_test.go | 15 +++++++++++++++ model.go | 4 ++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/kong_test.go b/kong_test.go index fce739a..66ad847 100644 --- a/kong_test.go +++ b/kong_test.go @@ -2460,3 +2460,18 @@ func TestApplyCalledOnce(t *testing.T) { 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, "...") +} diff --git a/model.go b/model.go index f15e6fb..065fcdd 100644 --- a/model.go +++ b/model.go @@ -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 From 47b090f2f44b4509d00edd0dcf20c0f52f1ff28e Mon Sep 17 00:00:00 2001 From: Alec Thomas Date: Sun, 29 Dec 2024 17:53:41 +0900 Subject: [PATCH 45/81] fix: add an `xorprefix:"..."` option for prefixing xor/and groups Fixes #343 --- README.md | 1 + build.go | 9 ++++++++- kong_test.go | 20 ++++++++++++++++++++ tag.go | 2 ++ 4 files changed, 31 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 5595a41..4a86251 100644 --- a/README.md +++ b/README.md @@ -550,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. | diff --git a/build.go b/build.go index 166935b..228c0e9 100644 --- a/build.go +++ b/build.go @@ -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,12 @@ MAIN: } } + if len(tag.Xor) != 0 { + for i := range tag.Xor { + tag.Xor[i] = tag.XorPrefix + tag.Xor[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/kong_test.go b/kong_test.go index 66ad847..2f3ab47 100644 --- a/kong_test.go +++ b/kong_test.go @@ -2475,3 +2475,23 @@ func TestCustomTypeNoEllipsis(t *testing.T) { 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) +} 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 { From b811e322436d3dfb266ea76b2e2a987bbf204a2f Mon Sep 17 00:00:00 2001 From: Alec Thomas Date: Sun, 29 Dec 2024 18:02:02 +0900 Subject: [PATCH 46/81] fix: whoops, forgot to prefix "and" --- build.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/build.go b/build.go index 228c0e9..5d17f53 100644 --- a/build.go +++ b/build.go @@ -154,6 +154,12 @@ MAIN: } } + 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 From 7ca846736ce870e28be6cbfbfa4e27e2b0609770 Mon Sep 17 00:00:00 2001 From: Abhinav Gupta Date: Fri, 3 Jan 2025 18:13:14 -0800 Subject: [PATCH 47/81] fix(Context.Run): Don't panic on unselected root node (#484) --- context.go | 4 +++- kong_test.go | 13 +++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/context.go b/context.go index 4fd2302..b6a56e3 100644 --- a/context.go +++ b/context.go @@ -827,7 +827,9 @@ func (c *Context) Run(binds ...any) (err error) { if method.IsValid() { node = selected } - } else { + } + + if node == nil { return fmt.Errorf("no command selected") } } diff --git a/kong_test.go b/kong_test.go index 2f3ab47..a7c03b2 100644 --- a/kong_test.go +++ b/kong_test.go @@ -2495,3 +2495,16 @@ func TestPrefixXorIssue343(t *testing.T) { _, 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") +} From 8d238c88faef4e3a6ef331352d81f40397ee46fb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 13 Jan 2025 22:13:14 +1100 Subject: [PATCH 48/81] chore(deps): update all non-major dependencies (#485) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- _examples/server/go.mod | 4 ++-- _examples/server/go.sum | 8 ++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/_examples/server/go.mod b/_examples/server/go.mod index 4a34a65..5f6d7e6 100644 --- a/_examples/server/go.mod +++ b/_examples/server/go.mod @@ -4,7 +4,7 @@ go 1.13 require ( github.com/alecthomas/colour v0.1.0 - github.com/alecthomas/kong v1.6.0 + github.com/alecthomas/kong v1.6.1 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 @@ -12,5 +12,5 @@ require ( 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.31.0 + golang.org/x/crypto v0.32.0 ) diff --git a/_examples/server/go.sum b/_examples/server/go.sum index fe021dc..8fa07ce 100644 --- a/_examples/server/go.sum +++ b/_examples/server/go.sum @@ -20,6 +20,8 @@ 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/kong v1.6.1 h1:/7bVimARU3uxPD0hbryPE8qWrS3Oz3kPQoxA/H2NKG8= +github.com/alecthomas/kong v1.6.1/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= @@ -98,6 +100,8 @@ 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/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= +golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= 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= @@ -160,6 +164,8 @@ golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -191,6 +197,8 @@ golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU= golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E= golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= +golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg= +golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= From 042a3258ec7bf7a1834eaaf86d3565a68c1af253 Mon Sep 17 00:00:00 2001 From: Hritik Vijay Date: Sat, 25 Jan 2025 05:21:46 +0530 Subject: [PATCH 49/81] Document about binds with example (#491) --- README.md | 41 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 4a86251..3b62344 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,8 @@ - [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) +- [Hooks: BeforeReset(), BeforeResolve(), BeforeApply(), AfterApply()](#hooks-beforereset-beforeresolve-beforeapply-afterapply) +- [The Bind() option](#the-bind-option) - [Flags](#flags) - [Commands and sub-commands](#commands-and-sub-commands) - [Branching positional arguments](#branching-positional-arguments) @@ -305,7 +306,7 @@ func main() { ``` -## Hooks: BeforeReset(), BeforeResolve(), BeforeApply(), AfterApply() and the Bind() option +## Hooks: BeforeReset(), BeforeResolve(), BeforeApply(), AfterApply() 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 @@ -314,8 +315,6 @@ and after validation/assignment, respectively. 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()`. - eg. ```go @@ -341,6 +340,40 @@ func main() { } ``` +## The Bind() option + +Arguments to hooks are provided via the `Run(...)` method or `Bind(...)` option. `*Kong`, `*Context`, `*Path` and parent commands are also bound and finally, hooks can also contribute bindings via `kong.Context.Bind()` and `kong.Context.BindTo()`. + +eg: + +```go +type CLI struct { + Debug bool `help:"Enable debug mode."` + + Rm RmCmd `cmd:"" help:"Remove files."` + Ls LsCmd `cmd:"" help:"List paths."` +} + +type AuthorName string + +// ... +func (l *LsCmd) Run(cli *CLI) error { +// use cli.Debug here !! + return nil +} + +func (r *RmCmD) Run(author AuthorName) error{ +// use binded author here + return nil +} + +func main() { + var cli CLI + + ctx := kong.Parse(&cli, Bind(AuthorName("penguin"))) + err := ctx.Run() +``` + ## 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. From 9c08a58eb2064527bc5f6f2e1e5f58e116224ea6 Mon Sep 17 00:00:00 2001 From: Abhinav Gupta Date: Tue, 28 Jan 2025 21:04:52 -0800 Subject: [PATCH 50/81] Support hooks on `embed:""` fields (#493) Relates to 840220c (#90) This change adds support for hooks to be called on fields that are tagged with `embed:""`. ### Use case If a command has several subcommands, many (but not all) of which need the same external resource, this allows defining the flag-level inputs for that resource centrally, and then using `embed:""` in any command that needs that resource. For example, imagine: ```go type githubClientProvider struct { Token string `name:"github-token" env:"GITHUB_TOKEN"` URL string `name:"github-url" env:"GITHUB_URL"` } func (g *githubClientProvider) BeforeApply(kctx *kong.Context) error { return kctx.BindToProvider(func() (*github.Client, error) { return github.NewClient(...), nil }) } ``` Then, any command that needs GitHub client will add this field, any other resource providers it needs, and add parameters to its `Run` method to accept those resources: ```go type listUsersCmd struct { GitHub githubClientProvider `embed:""` S3 s3ClientProvider `embed:""` } func (l *listUsersCmd) Run(gh *github.Client, s3 *s3.Client) error { ... } ``` ### Alternatives It is possible to do the same today if the `*Provider` struct above is actually a Go embed instead of a Kong embed, *and* it is exported. ``` type GitHubClientProvider struct{ ... } type listUsersCmd struct { GithubClientProvider S3ClientProvider } ``` The difference is whether the struct defining the flags is required to be exported or not. --- callbacks.go | 14 +++++++++++++- kong_test.go | 15 ++++++++++++++- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/callbacks.go b/callbacks.go index c1fac81..e541f5a 100644 --- a/callbacks.go +++ b/callbacks.go @@ -80,7 +80,19 @@ func getMethods(value reflect.Value, name string) []reflect.Value { for i := 0; i < value.NumField(); i++ { field := value.Field(i) fieldType := t.Field(i) - if fieldType.IsExported() && fieldType.Anonymous { + if !fieldType.IsExported() { + continue + } + + // Hooks on exported embedded fields should be called. + if fieldType.Anonymous { + receivers = append(receivers, field) + continue + } + + // Hooks on exported fields that are not exported, + // but are tagged with `embed:""` should be called. + if _, ok := fieldType.Tag.Lookup("embed"); ok { receivers = append(receivers, field) } } diff --git a/kong_test.go b/kong_test.go index a7c03b2..874b90b 100644 --- a/kong_test.go +++ b/kong_test.go @@ -2413,9 +2413,19 @@ func (e *EmbeddedCallback) AfterApply() error { return nil } +type taggedEmbeddedCallback struct { + Tagged bool +} + +func (e *taggedEmbeddedCallback) AfterApply() error { + e.Tagged = true + return nil +} + type EmbeddedRoot struct { EmbeddedCallback - Root bool + Tagged taggedEmbeddedCallback `embed:""` + Root bool } func (e *EmbeddedRoot) AfterApply() error { @@ -2432,6 +2442,9 @@ func TestEmbeddedCallbacks(t *testing.T) { EmbeddedCallback: EmbeddedCallback{ Embedded: true, }, + Tagged: taggedEmbeddedCallback{ + Tagged: true, + }, Root: true, } assert.Equal(t, expected, actual) From 4e1757c0e858abc58762bb54014c9bae5d1bbc42 Mon Sep 17 00:00:00 2001 From: Alec Thomas Date: Thu, 30 Jan 2025 13:39:31 +1100 Subject: [PATCH 51/81] feat: allow use of providers that don't return errors --- callbacks.go | 15 ++++++++++++--- context.go | 3 +++ kong_test.go | 20 ++++++++++++++++++++ 3 files changed, 35 insertions(+), 3 deletions(-) diff --git a/callbacks.go b/callbacks.go index e541f5a..b4fe3ca 100644 --- a/callbacks.go +++ b/callbacks.go @@ -34,8 +34,17 @@ func (b bindings) addTo(impl, iface any) { 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() { - return fmt.Errorf("%T must be a function with the signature func(...)(T, error)", provider) + if t.Kind() != reflect.Func { + return fmt.Errorf("%T must be a function", provider) + } + + if t.NumOut() == 0 { + return fmt.Errorf("%T must be a function with the signature func(...)(T, error) or func(...) T", provider) + } + if t.NumOut() == 2 { + if t.Out(1) != reflect.TypeOf((*error)(nil)).Elem() { + return fmt.Errorf("missing error; %T must be a function with the signature func(...)(T, error) or func(...) T", provider) + } } rt := pv.Type().Out(0) b[rt] = provider @@ -143,7 +152,7 @@ func callAnyFunction(f reflect.Value, bindings bindings) (out []any, err error) if err != nil { return nil, fmt.Errorf("%s: %w", pt, err) } - if ferrv := reflect.ValueOf(argv[len(argv)-1]); ferrv.IsValid() && !ferrv.IsNil() { + if ferrv := reflect.ValueOf(argv[len(argv)-1]); ferrv.IsValid() && ferrv.Type().Implements(callbackReturnSignature) && !ferrv.IsNil() { return nil, ferrv.Interface().(error) //nolint:forcetypeassert } in = append(in, reflect.ValueOf(argv[0])) diff --git a/context.go b/context.go index b6a56e3..ebf4c31 100644 --- a/context.go +++ b/context.go @@ -119,6 +119,9 @@ func (c *Context) BindTo(impl, iface any) { // // This is useful when the Run() function of different commands require different values that may // not all be initialisable from the main() function. +// +// "provider" must be a function with the signature func(...) (T, error) or func(...) T, where +// ... will be recursively injected with bound values. func (c *Context) BindToProvider(provider any) error { return c.bindings.addProvider(provider) } diff --git a/kong_test.go b/kong_test.go index 874b90b..2ceb1b1 100644 --- a/kong_test.go +++ b/kong_test.go @@ -2521,3 +2521,23 @@ func TestIssue483EmptyRootNodeNoRun(t *testing.T) { assert.Error(t, err) assert.Contains(t, err.Error(), "no command selected") } + +type providerWithoutErrorCLI struct { +} + +func (p *providerWithoutErrorCLI) Run(name string) error { + if name == "Bob" { + return nil + } + return fmt.Errorf("name %s is not Bob", name) +} + +func TestProviderWithoutError(t *testing.T) { + k := mustNew(t, &providerWithoutErrorCLI{}) + kctx, err := k.Parse(nil) + assert.NoError(t, err) + err = kctx.BindToProvider(func() string { return "Bob" }) + assert.NoError(t, err) + err = kctx.Run() + assert.NoError(t, err) +} From 4be6ae616831001dda40c5c34dc1a364eb8b2c68 Mon Sep 17 00:00:00 2001 From: Abhinav Gupta Date: Wed, 29 Jan 2025 18:43:10 -0800 Subject: [PATCH 52/81] hooks: Recursively search embedded fields for methods (#494) * hooks: Recursively search embedded fields for methods Follow up to #493 and 840220c Kong currently supports hooks on embedded fields of a parsed node, but only at the first level of embedding: ``` type mainCmd struct { FooOptions } type FooOptions struct { BarOptions } func (f *FooOptions) BeforeApply() error { // this will be called } type BarOptions struct { } func (b *BarOptions) BeforeApply() error { // this will not be called } ``` This change adds support for hooks to be defined on embedded fields of embedded fields so that the above example would work as expected. Per #493, the definition of "embedded" field is adjusted to mean: - Any anonymous (Go-embedded) field that is exported - Any non-anonymous field that is tagged with `embed:""` *Testing*: Includes a test case for embedding an anonymous field in an `embed:""` and an `embed:""` field in an anonymous field. * Use recursion to build up the list of receivers The 'receivers' parameter helps avoid constant memory allocation as the backing storage for the slice is reused across recursive calls. --- callbacks.go | 64 +++++++++++++++++++++++++++++++++------------------- kong_test.go | 19 ++++++++++++++++ 2 files changed, 60 insertions(+), 23 deletions(-) diff --git a/callbacks.go b/callbacks.go index b4fe3ca..4644c54 100644 --- a/callbacks.go +++ b/callbacks.go @@ -77,35 +77,53 @@ func getMethod(value reflect.Value, name string) reflect.Value { return method } -// Get methods from the given value and any embedded fields. +// getMethods gets all methods with the given name from the given value +// and any embedded fields. +// +// Returns a slice of bound methods that can be called directly. 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() { - continue - } + // Traverses embedded fields of the struct + // starting from the given value to collect all possible receivers + // for the given method name. + var traverse func(value reflect.Value, receivers []reflect.Value) []reflect.Value + traverse = func(value reflect.Value, receivers []reflect.Value) []reflect.Value { + // Always consider the current value for hooks. + receivers = append(receivers, value) - // Hooks on exported embedded fields should be called. - if fieldType.Anonymous { - receivers = append(receivers, field) - continue - } + if value.Kind() == reflect.Ptr { + value = value.Elem() + } - // Hooks on exported fields that are not exported, - // but are tagged with `embed:""` should be called. - if _, ok := fieldType.Tag.Lookup("embed"); ok { - receivers = append(receivers, field) + // If the current value is a struct, also consider embedded fields. + // Two kinds of embedded fields are considered if they're exported: + // + // - standard Go embedded fields + // - fields tagged with `embed:""` + if value.Kind() == reflect.Struct { + t := value.Type() + for i := 0; i < value.NumField(); i++ { + fieldValue := value.Field(i) + field := t.Field(i) + + if !field.IsExported() { + continue + } + + // Consider a field embedded if it's actually embedded + // or if it's tagged with `embed:""`. + _, isEmbedded := field.Tag.Lookup("embed") + isEmbedded = isEmbedded || field.Anonymous + if isEmbedded { + receivers = traverse(fieldValue, receivers) + } } } + + return receivers } + + receivers := traverse(value, nil /* receivers */) + // Search all receivers for methods var methods []reflect.Value for _, receiver := range receivers { diff --git a/kong_test.go b/kong_test.go index 2ceb1b1..6b5f5d6 100644 --- a/kong_test.go +++ b/kong_test.go @@ -2405,6 +2405,8 @@ func TestProviderMethods(t *testing.T) { } type EmbeddedCallback struct { + Nested NestedCallback `embed:""` + Embedded bool } @@ -2414,6 +2416,8 @@ func (e *EmbeddedCallback) AfterApply() error { } type taggedEmbeddedCallback struct { + NestedCallback + Tagged bool } @@ -2422,6 +2426,15 @@ func (e *taggedEmbeddedCallback) AfterApply() error { return nil } +type NestedCallback struct { + nested bool +} + +func (n *NestedCallback) AfterApply() error { + n.nested = true + return nil +} + type EmbeddedRoot struct { EmbeddedCallback Tagged taggedEmbeddedCallback `embed:""` @@ -2441,9 +2454,15 @@ func TestEmbeddedCallbacks(t *testing.T) { expected := &EmbeddedRoot{ EmbeddedCallback: EmbeddedCallback{ Embedded: true, + Nested: NestedCallback{ + nested: true, + }, }, Tagged: taggedEmbeddedCallback{ Tagged: true, + NestedCallback: NestedCallback{ + nested: true, + }, }, Root: true, } From 5765c1152209cc4f20c3afcffb18ca0d4367e5f2 Mon Sep 17 00:00:00 2001 From: Alec Thomas Date: Thu, 30 Jan 2025 14:08:07 +1100 Subject: [PATCH 53/81] refactor: minor simplification of getMethods --- .golangci.yml | 1 + callbacks.go | 79 +++++++++++++++++++++------------------------------ 2 files changed, 34 insertions(+), 46 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 844092f..3a05633 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -42,6 +42,7 @@ linters: - copyloopvar - intrange - execinquery + - nakedret linters-settings: govet: diff --git a/callbacks.go b/callbacks.go index 4644c54..2a296d0 100644 --- a/callbacks.go +++ b/callbacks.go @@ -81,57 +81,44 @@ func getMethod(value reflect.Value, name string) reflect.Value { // and any embedded fields. // // Returns a slice of bound methods that can be called directly. -func getMethods(value reflect.Value, name string) []reflect.Value { - // Traverses embedded fields of the struct - // starting from the given value to collect all possible receivers - // for the given method name. - var traverse func(value reflect.Value, receivers []reflect.Value) []reflect.Value - traverse = func(value reflect.Value, receivers []reflect.Value) []reflect.Value { - // Always consider the current value for hooks. - receivers = append(receivers, value) - - if value.Kind() == reflect.Ptr { - value = value.Elem() - } - - // If the current value is a struct, also consider embedded fields. - // Two kinds of embedded fields are considered if they're exported: - // - // - standard Go embedded fields - // - fields tagged with `embed:""` - if value.Kind() == reflect.Struct { - t := value.Type() - for i := 0; i < value.NumField(); i++ { - fieldValue := value.Field(i) - field := t.Field(i) - - if !field.IsExported() { - continue - } - - // Consider a field embedded if it's actually embedded - // or if it's tagged with `embed:""`. - _, isEmbedded := field.Tag.Lookup("embed") - isEmbedded = isEmbedded || field.Anonymous - if isEmbedded { - receivers = traverse(fieldValue, receivers) - } - } - } - - return receivers +func getMethods(value reflect.Value, name string) (methods []reflect.Value) { + if value.Kind() == reflect.Ptr { + value = value.Elem() + } + if !value.IsValid() { + return } - receivers := traverse(value, nil /* receivers */) + if method := getMethod(value, name); method.IsValid() { + methods = append(methods, method) + } - // Search all receivers for methods - var methods []reflect.Value - for _, receiver := range receivers { - if method := getMethod(receiver, name); method.IsValid() { - methods = append(methods, method) + if value.Kind() != reflect.Struct { + return + } + // If the current value is a struct, also consider embedded fields. + // Two kinds of embedded fields are considered if they're exported: + // + // - standard Go embedded fields + // - fields tagged with `embed:""` + t := value.Type() + for i := 0; i < value.NumField(); i++ { + fieldValue := value.Field(i) + field := t.Field(i) + + if !field.IsExported() { + continue + } + + // Consider a field embedded if it's actually embedded + // or if it's tagged with `embed:""`. + _, isEmbedded := field.Tag.Lookup("embed") + isEmbedded = isEmbedded || field.Anonymous + if isEmbedded { + methods = append(methods, getMethods(fieldValue, name)...) } } - return methods + return } func callFunction(f reflect.Value, bindings bindings) error { From a6efd403cc72d824c9c948d7c7e29d2c56ed481a Mon Sep 17 00:00:00 2001 From: Alec Thomas Date: Thu, 30 Jan 2025 14:10:48 +1100 Subject: [PATCH 54/81] chore: add lefthook --- bin/.lefthook-1.10.10.pkg | 1 + bin/lefthook | 1 + lefthook.yml | 7 +++++++ 3 files changed, 9 insertions(+) create mode 120000 bin/.lefthook-1.10.10.pkg create mode 120000 bin/lefthook create mode 100644 lefthook.yml diff --git a/bin/.lefthook-1.10.10.pkg b/bin/.lefthook-1.10.10.pkg new file mode 120000 index 0000000..383f451 --- /dev/null +++ b/bin/.lefthook-1.10.10.pkg @@ -0,0 +1 @@ +hermit \ No newline at end of file diff --git a/bin/lefthook b/bin/lefthook new file mode 120000 index 0000000..518e63f --- /dev/null +++ b/bin/lefthook @@ -0,0 +1 @@ +.lefthook-1.10.10.pkg \ No newline at end of file diff --git a/lefthook.yml b/lefthook.yml new file mode 100644 index 0000000..a287e0c --- /dev/null +++ b/lefthook.yml @@ -0,0 +1,7 @@ +pre-push: + jobs: + - name: test + run: go test -v ./... + + - name: lint + run: golangci-lint run From 705e259cd6cb87a0195eceac6f31d9bf7cf01e58 Mon Sep 17 00:00:00 2001 From: Alec Thomas Date: Thu, 30 Jan 2025 14:11:42 +1100 Subject: [PATCH 55/81] chore: reduce lefthook output --- lefthook.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lefthook.yml b/lefthook.yml index a287e0c..28ba9ad 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -1,4 +1,8 @@ +output: + - success + - failure pre-push: + parallel: true jobs: - name: test run: go test -v ./... From 6590294c3d15753cb8ded03ed7bbc24cba5a41f3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 8 Feb 2025 22:24:06 +1100 Subject: [PATCH 56/81] chore(deps): update all non-major dependencies (#486) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- _examples/server/go.mod | 4 ++-- _examples/server/go.sum | 10 ++++++++++ bin/{.go-1.23.4.pkg => .go-1.23.6.pkg} | 0 bin/go | 2 +- bin/gofmt | 2 +- 5 files changed, 14 insertions(+), 4 deletions(-) rename bin/{.go-1.23.4.pkg => .go-1.23.6.pkg} (100%) diff --git a/_examples/server/go.mod b/_examples/server/go.mod index 5f6d7e6..bbdafbc 100644 --- a/_examples/server/go.mod +++ b/_examples/server/go.mod @@ -4,7 +4,7 @@ go 1.13 require ( github.com/alecthomas/colour v0.1.0 - github.com/alecthomas/kong v1.6.1 + github.com/alecthomas/kong v1.7.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 @@ -12,5 +12,5 @@ require ( 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.32.0 + golang.org/x/crypto v0.33.0 ) diff --git a/_examples/server/go.sum b/_examples/server/go.sum index 8fa07ce..23d377f 100644 --- a/_examples/server/go.sum +++ b/_examples/server/go.sum @@ -22,6 +22,8 @@ github.com/alecthomas/kong v1.6.0 h1:mwOzbdMR7uv2vul9J0FU3GYxE7ls/iX1ieMg5WIM6gE github.com/alecthomas/kong v1.6.0/go.mod h1:p2vqieVMeTAnaC83txKtXe8FLke2X07aruPWXyMPQrU= github.com/alecthomas/kong v1.6.1 h1:/7bVimARU3uxPD0hbryPE8qWrS3Oz3kPQoxA/H2NKG8= github.com/alecthomas/kong v1.6.1/go.mod h1:p2vqieVMeTAnaC83txKtXe8FLke2X07aruPWXyMPQrU= +github.com/alecthomas/kong v1.7.0 h1:MnT8+5JxFDCvISeI6vgd/mFbAJwueJ/pqQNzZMsiqZE= +github.com/alecthomas/kong v1.7.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= @@ -102,6 +104,8 @@ 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/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= +golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= +golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= 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= @@ -127,6 +131,7 @@ golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -166,6 +171,8 @@ golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -199,6 +206,8 @@ golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg= golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= +golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= +golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -215,6 +224,7 @@ golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= diff --git a/bin/.go-1.23.4.pkg b/bin/.go-1.23.6.pkg similarity index 100% rename from bin/.go-1.23.4.pkg rename to bin/.go-1.23.6.pkg diff --git a/bin/go b/bin/go index 3c91a6c..8fb1c41 120000 --- a/bin/go +++ b/bin/go @@ -1 +1 @@ -.go-1.23.4.pkg \ No newline at end of file +.go-1.23.6.pkg \ No newline at end of file diff --git a/bin/gofmt b/bin/gofmt index 3c91a6c..8fb1c41 120000 --- a/bin/gofmt +++ b/bin/gofmt @@ -1 +1 @@ -.go-1.23.4.pkg \ No newline at end of file +.go-1.23.6.pkg \ No newline at end of file From 3cedc448212efa54577c7d6c601b079a49b2c2da Mon Sep 17 00:00:00 2001 From: Maxime Vidori Date: Sun, 9 Feb 2025 23:53:27 +0100 Subject: [PATCH 57/81] Load environment variables as a resolver (#480) --- kong.go | 2 +- model.go | 14 ----------- resolver.go | 68 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 69 insertions(+), 15 deletions(-) diff --git a/kong.go b/kong.go index b85e145..3cb2e40 100644 --- a/kong.go +++ b/kong.go @@ -91,7 +91,7 @@ func New(grammar any, options ...Option) (*Kong, error) { }, } - options = append(options, Bind(k)) + options = append(options, Bind(k), Resolvers(EnvResolver())) for _, option := range options { if err := option.Apply(k); err != nil { diff --git a/model.go b/model.go index 065fcdd..3913239 100644 --- a/model.go +++ b/model.go @@ -3,7 +3,6 @@ package kong import ( "fmt" "math" - "os" "reflect" "strconv" "strings" @@ -377,19 +376,6 @@ func (v *Value) ApplyDefault() error { // Does not include resolvers. func (v *Value) Reset() error { v.Target.Set(reflect.Zero(v.Target.Type())) - 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 - } - } - } if v.HasDefault { return v.Parse(ScanFromTokens(Token{Type: FlagValueToken, Value: v.Default}), v.Target) } diff --git a/resolver.go b/resolver.go index 29be1b9..d28158f 100644 --- a/resolver.go +++ b/resolver.go @@ -2,7 +2,9 @@ package kong import ( "encoding/json" + "fmt" "io" + "os" "strings" ) @@ -66,3 +68,69 @@ func snakeCase(name string) string { name = strings.Join(strings.Split(strings.Title(name), "-"), "") return strings.ToLower(name[:1]) + name[1:] } + +// EnvResolver provides a resolver for environment variables tags +func EnvResolver() Resolver { + // Resolvers are typically only invoked for flags, as shown here: + // https://github.com/alecthomas/kong/blob/v1.6.0/context.go#L567 + // However, environment variable annotations can also apply to arguments, + // as demonstrated in this test: + // https://github.com/alecthomas/kong/blob/v1.6.0/kong_test.go#L1226-L1244 + // To handle this, we ensure that arguments are resolved as well. + // Since the resolution only needs to happen once, we use this boolean + // to track whether the resolution process has already been performed. + argsResolved := false + return ResolverFunc(func(context *Context, parent *Path, flag *Flag) (interface{}, error) { + if !argsResolved { + if err := resolveArgs(context.Path); err != nil { + return nil, err + } + // once resolved we do not want to run this anymore + argsResolved = true + } + for _, env := range flag.Tag.Envs { + envar, ok := os.LookupEnv(env) + // Parse the first non-empty ENV in the list + if ok { + return envar, nil + } + } + return nil, nil + }) +} + +func resolveArgs(paths []*Path) error { + for _, path := range paths { + if path.Command == nil { + continue + } + for _, positional := range path.Command.Positional { + if positional.Tag == nil { + continue + } + if err := visitValue(positional); err != nil { + return err + } + } + if path.Command.Argument != nil { + if err := visitValue(path.Command.Argument); err != nil { + return err + } + } + } + return nil +} + +func visitValue(value *Value) error { + for _, env := range value.Tag.Envs { + envar, ok := os.LookupEnv(env) + if !ok { + continue + } + token := Token{Type: FlagValueToken, Value: envar} + if err := value.Parse(ScanFromTokens(token), value.Target); err != nil { + return fmt.Errorf("%s (from envar %s=%q)", err, env, envar) + } + } + return nil +} From 7747b4146b600a1e64fef08b58e5074843179f40 Mon Sep 17 00:00:00 2001 From: Alec Thomas Date: Thu, 13 Feb 2025 11:17:47 -0800 Subject: [PATCH 58/81] Revert "Load environment variables as a resolver (#480)" This reverts commit 3cedc448212efa54577c7d6c601b079a49b2c2da. Fixes #497, #498 --- kong.go | 2 +- model.go | 14 +++++++++++ resolver.go | 68 ----------------------------------------------------- 3 files changed, 15 insertions(+), 69 deletions(-) diff --git a/kong.go b/kong.go index 3cb2e40..b85e145 100644 --- a/kong.go +++ b/kong.go @@ -91,7 +91,7 @@ func New(grammar any, options ...Option) (*Kong, error) { }, } - options = append(options, Bind(k), Resolvers(EnvResolver())) + options = append(options, Bind(k)) for _, option := range options { if err := option.Apply(k); err != nil { diff --git a/model.go b/model.go index 3913239..065fcdd 100644 --- a/model.go +++ b/model.go @@ -3,6 +3,7 @@ package kong import ( "fmt" "math" + "os" "reflect" "strconv" "strings" @@ -376,6 +377,19 @@ func (v *Value) ApplyDefault() error { // Does not include resolvers. func (v *Value) Reset() error { v.Target.Set(reflect.Zero(v.Target.Type())) + 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 + } + } + } if v.HasDefault { return v.Parse(ScanFromTokens(Token{Type: FlagValueToken, Value: v.Default}), v.Target) } diff --git a/resolver.go b/resolver.go index d28158f..29be1b9 100644 --- a/resolver.go +++ b/resolver.go @@ -2,9 +2,7 @@ package kong import ( "encoding/json" - "fmt" "io" - "os" "strings" ) @@ -68,69 +66,3 @@ func snakeCase(name string) string { name = strings.Join(strings.Split(strings.Title(name), "-"), "") return strings.ToLower(name[:1]) + name[1:] } - -// EnvResolver provides a resolver for environment variables tags -func EnvResolver() Resolver { - // Resolvers are typically only invoked for flags, as shown here: - // https://github.com/alecthomas/kong/blob/v1.6.0/context.go#L567 - // However, environment variable annotations can also apply to arguments, - // as demonstrated in this test: - // https://github.com/alecthomas/kong/blob/v1.6.0/kong_test.go#L1226-L1244 - // To handle this, we ensure that arguments are resolved as well. - // Since the resolution only needs to happen once, we use this boolean - // to track whether the resolution process has already been performed. - argsResolved := false - return ResolverFunc(func(context *Context, parent *Path, flag *Flag) (interface{}, error) { - if !argsResolved { - if err := resolveArgs(context.Path); err != nil { - return nil, err - } - // once resolved we do not want to run this anymore - argsResolved = true - } - for _, env := range flag.Tag.Envs { - envar, ok := os.LookupEnv(env) - // Parse the first non-empty ENV in the list - if ok { - return envar, nil - } - } - return nil, nil - }) -} - -func resolveArgs(paths []*Path) error { - for _, path := range paths { - if path.Command == nil { - continue - } - for _, positional := range path.Command.Positional { - if positional.Tag == nil { - continue - } - if err := visitValue(positional); err != nil { - return err - } - } - if path.Command.Argument != nil { - if err := visitValue(path.Command.Argument); err != nil { - return err - } - } - } - return nil -} - -func visitValue(value *Value) error { - for _, env := range value.Tag.Envs { - envar, ok := os.LookupEnv(env) - if !ok { - continue - } - token := Token{Type: FlagValueToken, Value: envar} - if err := value.Parse(ScanFromTokens(token), value.Target); err != nil { - return fmt.Errorf("%s (from envar %s=%q)", err, env, envar) - } - } - return nil -} From cab639ab83b14162986cc0a34700047274514d06 Mon Sep 17 00:00:00 2001 From: Alec Thomas Date: Thu, 13 Feb 2025 11:22:03 -0800 Subject: [PATCH 59/81] chore: add test for decoding --- mapper_test.go | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/mapper_test.go b/mapper_test.go index 113e9f5..a6ddbe3 100644 --- a/mapper_test.go +++ b/mapper_test.go @@ -756,3 +756,31 @@ func TestValuesThatLookLikeFlags(t *testing.T) { assert.NoError(t, err) assert.Equal(t, map[string]string{"-foo": "-bar"}, cli.Map) } + +type DecodeCLI struct { + Foo DecodeFoo `env:"FOO"` +} + +type DecodeFoo struct { + Bar string +} + +func (f DecodeFoo) Decode(ctx *kong.DecodeContext) error { + ctx.Value.Target.Set(reflect.ValueOf(struct { + Bar string + }{"baz"})) + return nil +} + +func TestDecode(t *testing.T) { + c := &DecodeCLI{} + parser, err := kong.New(c) + assert.NoError(t, err) + + t.Setenv("FOO", "foo") + + _, err = parser.Parse([]string{}) + assert.NoError(t, err) + + assert.Equal(t, c.Foo.Bar, "baz") +} From 9f71a497678f07c938979a3a7971ad4ba1b06cd6 Mon Sep 17 00:00:00 2001 From: Abhinav Gupta Date: Sun, 16 Feb 2025 19:10:57 -0800 Subject: [PATCH 60/81] ci: Test with Go 1.23 and 1.24 (#502) In CI, test with Go 1.23 and 1.24, and upgrade the Hermit-managed Go and golangci-lint to latest versions. The new golangci-lint had a number of warnings and minor issues that were either fixed or opted-out of. --- .github/workflows/ci.yml | 4 ++-- .golangci.yml | 4 ++-- bin/{.go-1.23.6.pkg => .go-1.24.0.pkg} | 0 ...-lint-1.60.1.pkg => .golangci-lint-1.64.5.pkg} | 0 bin/go | 2 +- bin/gofmt | 2 +- bin/golangci-lint | 2 +- config_test.go | 15 ++++++--------- help.go | 4 ++-- help_test.go | 14 +++++--------- mapper_test.go | 6 ++---- resolver.go | 2 +- util_test.go | 3 +-- 13 files changed, 24 insertions(+), 34 deletions(-) rename bin/{.go-1.23.6.pkg => .go-1.24.0.pkg} (100%) rename bin/{.golangci-lint-1.60.1.pkg => .golangci-lint-1.64.5.pkg} (100%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3396e46..4483d4b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ jobs: matrix: # These are the release channels. # Hermit will handle installing the right patch. - go: ["1.20", "1.21"] + go: ["1.23", "1.24"] steps: - name: Checkout code uses: actions/checkout@v4 @@ -35,7 +35,7 @@ jobs: matrix: # These are versions for GitHub's setup-go. # '.x' will pick the latest patch release. - go: ["1.20.x", "1.21.x"] + go: ["1.23.x", "1.24.x"] steps: - name: Checkout code uses: actions/checkout@v4 diff --git a/.golangci.yml b/.golangci.yml index 3a05633..1eb0b92 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -12,7 +12,6 @@ linters: - wsl - funlen - gocognit - - gomnd - goprintffuncname - paralleltest - nlreturn @@ -36,13 +35,14 @@ linters: - nilnil - depguard # nothing to guard against yet - tagalign # hurts readability of kong tags + - tenv # deprecated since v1.64, but not removed yet - mnd - perfsprint - err113 - copyloopvar - intrange - - execinquery - nakedret + - recvcheck # value receivers are intentionally used for copies linters-settings: govet: diff --git a/bin/.go-1.23.6.pkg b/bin/.go-1.24.0.pkg similarity index 100% rename from bin/.go-1.23.6.pkg rename to bin/.go-1.24.0.pkg diff --git a/bin/.golangci-lint-1.60.1.pkg b/bin/.golangci-lint-1.64.5.pkg similarity index 100% rename from bin/.golangci-lint-1.60.1.pkg rename to bin/.golangci-lint-1.64.5.pkg diff --git a/bin/go b/bin/go index 8fb1c41..42b4aa3 120000 --- a/bin/go +++ b/bin/go @@ -1 +1 @@ -.go-1.23.6.pkg \ No newline at end of file +.go-1.24.0.pkg \ No newline at end of file diff --git a/bin/gofmt b/bin/gofmt index 8fb1c41..42b4aa3 120000 --- a/bin/gofmt +++ b/bin/gofmt @@ -1 +1 @@ -.go-1.23.6.pkg \ No newline at end of file +.go-1.24.0.pkg \ No newline at end of file diff --git a/bin/golangci-lint b/bin/golangci-lint index 4857716..1bb9797 120000 --- a/bin/golangci-lint +++ b/bin/golangci-lint @@ -1 +1 @@ -.golangci-lint-1.60.1.pkg \ No newline at end of file +.golangci-lint-1.64.5.pkg \ No newline at end of file diff --git a/config_test.go b/config_test.go index 570f142..fd303af 100644 --- a/config_test.go +++ b/config_test.go @@ -15,12 +15,10 @@ func TestMultipleConfigLoading(t *testing.T) { } cli.Flag = "first" - first, cleanFirst := makeConfig(t, &cli) - defer cleanFirst() + first := makeConfig(t, &cli) cli.Flag = "" - second, cleanSecond := makeConfig(t, &cli) - defer cleanSecond() + second := makeConfig(t, &cli) p := mustNew(t, &cli, kong.Configuration(kong.JSON, first, second)) _, err := p.Parse(nil) @@ -34,20 +32,19 @@ func TestConfigValidation(t *testing.T) { } cli.Flag = "invalid" - conf, cleanConf := makeConfig(t, &cli) - defer cleanConf() + conf := makeConfig(t, &cli) p := mustNew(t, &cli, kong.Configuration(kong.JSON, conf)) _, err := p.Parse(nil) assert.Error(t, err) } -func makeConfig(t *testing.T, config any) (path string, cleanup func()) { +func makeConfig(t *testing.T, config any) (path string) { t.Helper() - w, err := os.CreateTemp("", "") + w, err := os.CreateTemp(t.TempDir(), "") assert.NoError(t, err) defer w.Close() err = json.NewEncoder(w).Encode(config) assert.NoError(t, err) - return w.Name(), func() { os.Remove(w.Name()) } + return w.Name() } diff --git a/help.go b/help.go index 6363ea2..28290f6 100644 --- a/help.go +++ b/help.go @@ -415,7 +415,7 @@ func (h *helpWriter) Write(w io.Writer) error { func (h *helpWriter) Wrap(text string) { w := bytes.NewBuffer(nil) - doc.ToText(w, strings.TrimSpace(text), "", " ", h.width) + doc.ToText(w, strings.TrimSpace(text), "", " ", h.width) //nolint:staticcheck // cross-package links not possible for _, line := range strings.Split(strings.TrimSpace(w.String()), "\n") { h.Print(line) } @@ -470,7 +470,7 @@ func writeTwoColumns(w *helpWriter, rows [][2]string) { for _, row := range rows { buf := bytes.NewBuffer(nil) - doc.ToText(buf, row[1], "", strings.Repeat(" ", defaultIndent), w.width-leftSize-defaultColumnPadding) + doc.ToText(buf, row[1], "", strings.Repeat(" ", defaultIndent), w.width-leftSize-defaultColumnPadding) //nolint:staticcheck // cross-package links not possible lines := strings.Split(strings.TrimRight(buf.String(), "\n"), "\n") line := fmt.Sprintf("%-*s", leftSize, row[0]) diff --git a/help_test.go b/help_test.go index e680be2..f8aebab 100644 --- a/help_test.go +++ b/help_test.go @@ -83,8 +83,7 @@ func TestHelp(t *testing.T) { Three threeArg `arg help:"Sub-sub-arg."` - Four struct { - } `cmd help:"Sub-sub-command."` + Four struct{} `cmd help:"Sub-sub-command."` } `cmd help:"Another subcommand."` } @@ -192,8 +191,7 @@ func TestFlagsLast(t *testing.T) { Three threeArg `arg help:"Sub-sub-arg."` - Four struct { - } `cmd help:"Sub-sub-command."` + Four struct{} `cmd help:"Sub-sub-command."` } `cmd help:"Another subcommand."` } @@ -296,8 +294,7 @@ func TestHelpTree(t *testing.T) { Two struct { Three threeArg `arg help:"Sub-sub-arg."` - Four struct { - } `cmd help:"Sub-sub-command." aliases:"for,fore"` + Four struct{} `cmd help:"Sub-sub-command." aliases:"for,fore"` } `cmd help:"Another subcommand."` } @@ -390,8 +387,7 @@ func TestHelpCompactNoExpand(t *testing.T) { Two struct { Three threeArg `arg help:"Sub-sub-arg."` - Four struct { - } `cmd help:"Sub-sub-command." aliases:"for,fore"` + Four struct{} `cmd help:"Sub-sub-command." aliases:"for,fore"` } `cmd help:"Another subcommand."` } @@ -603,7 +599,7 @@ func TestAutoGroup(t *testing.T) { if node, ok := parent.(*kong.Node); ok { return &kong.Group{ Key: node.Name, - Title: strings.Title(node.Name) + " flags:", + Title: strings.Title(node.Name) + " flags:", //nolint:staticcheck // strings.Title in test is okay } } return nil diff --git a/mapper_test.go b/mapper_test.go index a6ddbe3..947f836 100644 --- a/mapper_test.go +++ b/mapper_test.go @@ -268,9 +268,8 @@ func TestFileContentFlag(t *testing.T) { var cli struct { File kong.FileContentFlag } - f, err := os.CreateTemp("", "") + f, err := os.CreateTemp(t.TempDir(), "") assert.NoError(t, err) - defer os.Remove(f.Name()) fmt.Fprint(f, "hello world") f.Close() _, err = mustNew(t, &cli).Parse([]string{"--file", f.Name()}) @@ -282,9 +281,8 @@ func TestNamedFileContentFlag(t *testing.T) { var cli struct { File kong.NamedFileContentFlag } - f, err := os.CreateTemp("", "") + f, err := os.CreateTemp(t.TempDir(), "") assert.NoError(t, err) - defer os.Remove(f.Name()) fmt.Fprint(f, "hello world") f.Close() _, err = mustNew(t, &cli).Parse([]string{"--file", f.Name()}) diff --git a/resolver.go b/resolver.go index 29be1b9..3e37ca7 100644 --- a/resolver.go +++ b/resolver.go @@ -63,6 +63,6 @@ func JSON(r io.Reader) (Resolver, error) { } func snakeCase(name string) string { - name = strings.Join(strings.Split(strings.Title(name), "-"), "") + name = strings.Join(strings.Split(strings.Title(name), "-"), "") //nolint:staticcheck // Unicode punctuation not an issue return strings.ToLower(name[:1]) + name[1:] } diff --git a/util_test.go b/util_test.go index 5ec0131..78302ef 100644 --- a/util_test.go +++ b/util_test.go @@ -16,9 +16,8 @@ func TestConfigFlag(t *testing.T) { Flag string } - w, err := os.CreateTemp("", "") + w, err := os.CreateTemp(t.TempDir(), "") assert.NoError(t, err) - defer os.Remove(w.Name()) w.WriteString(`{"flag": "hello world"}`) //nolint: errcheck w.Close() From 7f94c902b9e2a7e5b84194d1a4c61ce62ec0361d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 17 Feb 2025 15:41:24 +1100 Subject: [PATCH 61/81] chore(deps): update module github.com/alecthomas/kong to v1.8.1 (#503) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- _examples/server/go.mod | 2 +- _examples/server/go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/_examples/server/go.mod b/_examples/server/go.mod index bbdafbc..39fccfe 100644 --- a/_examples/server/go.mod +++ b/_examples/server/go.mod @@ -4,7 +4,7 @@ go 1.13 require ( github.com/alecthomas/colour v0.1.0 - github.com/alecthomas/kong v1.7.0 + github.com/alecthomas/kong v1.8.1 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 diff --git a/_examples/server/go.sum b/_examples/server/go.sum index 23d377f..2449645 100644 --- a/_examples/server/go.sum +++ b/_examples/server/go.sum @@ -24,6 +24,8 @@ github.com/alecthomas/kong v1.6.1 h1:/7bVimARU3uxPD0hbryPE8qWrS3Oz3kPQoxA/H2NKG8 github.com/alecthomas/kong v1.6.1/go.mod h1:p2vqieVMeTAnaC83txKtXe8FLke2X07aruPWXyMPQrU= github.com/alecthomas/kong v1.7.0 h1:MnT8+5JxFDCvISeI6vgd/mFbAJwueJ/pqQNzZMsiqZE= github.com/alecthomas/kong v1.7.0/go.mod h1:p2vqieVMeTAnaC83txKtXe8FLke2X07aruPWXyMPQrU= +github.com/alecthomas/kong v1.8.1 h1:6aamvWBE/REnR/BCq10EcozmcpUPc5aGI1lPAWdB0EE= +github.com/alecthomas/kong v1.8.1/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= From 3b9af5bdced7d12010db4133bf3d0ef0d1f6c3ee Mon Sep 17 00:00:00 2001 From: Abhinav Gupta Date: Sun, 16 Feb 2025 22:44:03 -0800 Subject: [PATCH 62/81] feat: Support singleton providers (#501) * feat: Support singleton providers This change adds support for provider functions that are not reinvoked even if requested by multiple other providers. Instead, their value is cached and reused between invocations. To make this possible, we change how bindings are stored: instead of just a function reference, we now store a binding object which records whether the binding is a singleton, and records the resolved singleton value (if any). Resolves #500 * refac(bindings): hide singleton status Don't require callAnyFunction to be aware of whether a binding is a singleton or not. --- callbacks.go | 78 +++++++++++++++++++++++++++++++++++++++++++------ context.go | 17 ++++++++--- options.go | 24 +++++++++++++-- options_test.go | 37 +++++++++++++++++++++++ 4 files changed, 140 insertions(+), 16 deletions(-) diff --git a/callbacks.go b/callbacks.go index 2a296d0..6096a26 100644 --- a/callbacks.go +++ b/callbacks.go @@ -6,10 +6,59 @@ import ( "strings" ) +// binding is a single binding registered with Kong. +type binding struct { + // fn is a function that returns a value of the target type. + fn reflect.Value + + // val is a value of the target type. + // Must be set if done and singleton are true. + val reflect.Value + + // singleton indicates whether the binding is a singleton. + // If true, the binding will be resolved once and cached. + singleton bool + + // done indicates whether a singleton binding has been resolved. + // If singleton is false, this field is ignored. + done bool +} + +// newValueBinding builds a binding with an already resolved value. +func newValueBinding(v reflect.Value) *binding { + return &binding{val: v, done: true, singleton: true} +} + +// newFunctionBinding builds a binding with a function +// that will return a value of the target type. +// +// The function signature must be func(...) (T, error) or func(...) T +// where parameters are recursively resolved. +func newFunctionBinding(f reflect.Value, singleton bool) *binding { + return &binding{fn: f, singleton: singleton} +} + +// Get returns the pre-resolved value for the binding, +// or false if the binding is not resolved. +func (b *binding) Get() (v reflect.Value, ok bool) { + return b.val, b.done +} + +// Set sets the value of the binding to the given value, +// marking it as resolved. +// +// If the binding is not a singleton, this method does nothing. +func (b *binding) Set(v reflect.Value) { + if b.singleton { + b.val = v + b.done = true + } +} + // A map of type to function that returns a value of that type. // // The function should have the signature func(...) (T, error). Arguments are recursively resolved. -type bindings map[reflect.Type]any +type bindings map[reflect.Type]*binding func (b bindings) String() string { out := []string{} @@ -21,17 +70,18 @@ func (b bindings) String() string { func (b bindings) add(values ...any) bindings { for _, v := range values { - v := v - b[reflect.TypeOf(v)] = func() (any, error) { return v, nil } + val := reflect.ValueOf(v) + b[val.Type()] = newValueBinding(val) } return b } func (b bindings) addTo(impl, iface any) { - b[reflect.TypeOf(iface).Elem()] = func() (any, error) { return impl, nil } + val := reflect.ValueOf(impl) + b[reflect.TypeOf(iface).Elem()] = newValueBinding(val) } -func (b bindings) addProvider(provider any) error { +func (b bindings) addProvider(provider any, singleton bool) error { pv := reflect.ValueOf(provider) t := pv.Type() if t.Kind() != reflect.Func { @@ -47,7 +97,7 @@ func (b bindings) addProvider(provider any) error { } } rt := pv.Type().Out(0) - b[rt] = provider + b[rt] = newFunctionBinding(pv, singleton) return nil } @@ -148,19 +198,29 @@ func callAnyFunction(f reflect.Value, bindings bindings) (out []any, err error) t := f.Type() for i := 0; i < t.NumIn(); i++ { pt := t.In(i) - argf, ok := bindings[pt] + binding, ok := bindings[pt] if !ok { return nil, fmt.Errorf("couldn't find binding of type %s for parameter %d of %s(), use kong.Bind(%s)", pt, i, t, pt) } + + // Don't need to call the function if the value is already resolved. + if val, ok := binding.Get(); ok { + in = append(in, val) + continue + } + // Recursively resolve binding functions. - argv, err := callAnyFunction(reflect.ValueOf(argf), bindings) + argv, err := callAnyFunction(binding.fn, bindings) if err != nil { return nil, fmt.Errorf("%s: %w", pt, err) } if ferrv := reflect.ValueOf(argv[len(argv)-1]); ferrv.IsValid() && ferrv.Type().Implements(callbackReturnSignature) && !ferrv.IsNil() { return nil, ferrv.Interface().(error) //nolint:forcetypeassert } - in = append(in, reflect.ValueOf(argv[0])) + + val := reflect.ValueOf(argv[0]) + binding.Set(val) + in = append(in, val) } outv := f.Call(in) out = make([]any, len(outv)) diff --git a/context.go b/context.go index ebf4c31..7b1d482 100644 --- a/context.go +++ b/context.go @@ -120,10 +120,19 @@ func (c *Context) BindTo(impl, iface any) { // This is useful when the Run() function of different commands require different values that may // not all be initialisable from the main() function. // -// "provider" must be a function with the signature func(...) (T, error) or func(...) T, where -// ... will be recursively injected with bound values. +// "provider" must be a function with the signature func(...) (T, error) or func(...) T, +// where ... will be recursively injected with bound values. func (c *Context) BindToProvider(provider any) error { - return c.bindings.addProvider(provider) + return c.bindings.addProvider(provider, false /* singleton */) +} + +// BindSingletonProvider allows binding of provider functions. +// The provider will be called once and the result cached. +// +// "provider" must be a function with the signature func(...) (T, error) or func(...) T, +// where ... will be recursively injected with bound values. +func (c *Context) BindSingletonProvider(provider any) error { + return c.bindings.addProvider(provider, true /* singleton */) } // Value returns the value for a particular path element. @@ -792,7 +801,7 @@ func (c *Context) RunNode(node *Node, binds ...any) (err error) { methodt := t.Method(i) if strings.HasPrefix(methodt.Name, "Provide") { method := p.Method(i) - if err := methodBinds.addProvider(method.Interface()); err != nil { + if err := methodBinds.addProvider(method.Interface(), false /* singleton */); err != nil { return fmt.Errorf("%s.%s: %w", t.Name(), methodt.Name, err) } } diff --git a/options.go b/options.go index 6263202..d20b2fb 100644 --- a/options.go +++ b/options.go @@ -210,15 +210,33 @@ func BindTo(impl, iface any) Option { // BindToProvider binds an injected value to a provider function. // -// The provider function must have the signature: +// The provider function must have one of the following signatures: // -// func() (any, error) +// func(...) (T, error) +// func(...) T +// +// Where arguments to the function are injected by Kong. // // 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 any) Option { return OptionFunc(func(k *Kong) error { - return k.bindings.addProvider(provider) + return k.bindings.addProvider(provider, false /* singleton */) + }) +} + +// BindSingletonProvider binds an injected value to a provider function. +// The provider function must have the signature: +// +// func(...) (T, error) +// func(...) T +// +// Unlike [BindToProvider], the provider function will only be called +// at most once, and the result will be cached and reused +// across multiple recipients of the injected value. +func BindSingletonProvider(provider any) Option { + return OptionFunc(func(k *Kong) error { + return k.bindings.addProvider(provider, true /* singleton */) }) } diff --git a/options_test.go b/options_test.go index e549475..791cb64 100644 --- a/options_test.go +++ b/options_test.go @@ -119,6 +119,43 @@ func TestBindToProvider(t *testing.T) { assert.True(t, cli.Called) } +func TestBindSingletonProvider(t *testing.T) { + type ( + Connection struct{} + ClientA struct{ conn *Connection } + ClientB struct{ conn *Connection } + ) + + var numConnections int + newConnection := func() *Connection { + numConnections++ + return &Connection{} + } + + var cli struct{} + app, err := New(&cli, + BindSingletonProvider(newConnection), + BindToProvider(func(conn *Connection) *ClientA { + return &ClientA{conn: conn} + }), + BindToProvider(func(conn *Connection) *ClientB { + return &ClientB{conn: conn} + }), + ) + assert.NoError(t, err) + + ctx, err := app.Parse([]string{}) + assert.NoError(t, err) + + _, err = ctx.Call(func(a *ClientA, b *ClientB) { + assert.NotZero(t, a.conn) + assert.NotZero(t, b.conn) + + assert.Equal(t, 1, numConnections, "expected newConnection to be called only once") + }) + assert.NoError(t, err) +} + func TestFlagNamer(t *testing.T) { var cli struct { SomeFlag string From 300cba8c27f2a60a3a65ca7537530bc7ec86a8dd Mon Sep 17 00:00:00 2001 From: Cam Hutchison Date: Mon, 17 Feb 2025 20:06:02 +1100 Subject: [PATCH 63/81] feat: Allow ignoring fields from embedded structs (#499) If a field in a struct is ignored with `kong:"-"`, any embedded fields with the same name are also ignored. This allows an outer struct to remove flags from an embedded struct by redefining it and adding a kong ignore tag. --- build.go | 22 +++++++++++++++++++++- kong_test.go | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/build.go b/build.go index 5d17f53..63afcd4 100644 --- a/build.go +++ b/build.go @@ -54,6 +54,7 @@ func flattenedFields(v reflect.Value, ptag *Tag) (out []flattenedField, err erro if v.Kind() != reflect.Struct { return out, nil } + ignored := map[string]bool{} for i := 0; i < v.NumField(); i++ { ft := v.Type().Field(i) fv := v.Field(i) @@ -61,7 +62,8 @@ func flattenedFields(v reflect.Value, ptag *Tag) (out []flattenedField, err erro if err != nil { return nil, err } - if tag.Ignored { + if tag.Ignored || ignored[ft.Name] { + ignored[ft.Name] = true continue } // Assign group if it's not already set. @@ -106,9 +108,27 @@ func flattenedFields(v reflect.Value, ptag *Tag) (out []flattenedField, err erro } out = append(out, sub...) } + out = removeIgnored(out, ignored) return out, nil } +func removeIgnored(fields []flattenedField, ignored map[string]bool) []flattenedField { + j := 0 + for i := 0; i < len(fields); i++ { + if ignored[fields[i].field.Name] { + continue + } + if i != j { + fields[j] = fields[i] + } + j++ + } + if j != len(fields) { + fields = fields[:j] + } + return fields +} + // Build a Node in the Kong data model. // // "v" is the value to create the node from, "typ" is the output Node type. diff --git a/kong_test.go b/kong_test.go index 6b5f5d6..15d1f51 100644 --- a/kong_test.go +++ b/kong_test.go @@ -856,6 +856,43 @@ func TestExcludedField(t *testing.T) { assert.Error(t, err) } +func TestExcludeEmbeddedField(t *testing.T) { + type Embedded struct { + Flag string + Excluded string + } + type Embedded2 struct { + Flag2 string + Excluded string + } + var cli struct { + Embedded + Excluded string `kong:"-"` + Embedded2 + } + var cli2 struct { + Embedded Embedded `kong:"embed"` + Excluded string `kong:"-"` + Embedded2 Embedded2 `kong:"embed"` + } + + p := mustNew(t, &cli) + _, err := p.Parse([]string{"--flag=foo"}) + assert.NoError(t, err) + _, err = p.Parse([]string{"--flag-2=foo"}) + assert.NoError(t, err) + _, err = p.Parse([]string{"--excluded=foo"}) + assert.Error(t, err) + + p = mustNew(t, &cli2) + _, err = p.Parse([]string{"--flag=foo"}) + assert.NoError(t, err) + _, err = p.Parse([]string{"--flag-2=foo"}) + assert.NoError(t, err) + _, err = p.Parse([]string{"--excluded=foo"}) + assert.Error(t, err) +} + func TestUnnamedFieldEmbeds(t *testing.T) { type Embed struct { Flag string From 0c495e4936b62e3599434bb9938cb5eb4e20ab53 Mon Sep 17 00:00:00 2001 From: Alec Thomas Date: Wed, 26 Feb 2025 14:37:56 +1100 Subject: [PATCH 64/81] feat: add IgnoreDefault optional interface --- context.go | 7 ++++++- help.go | 6 ++++-- kong.go | 2 +- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/context.go b/context.go index 7b1d482..dcaf099 100644 --- a/context.go +++ b/context.go @@ -550,11 +550,16 @@ func (c *Context) trace(node *Node) (err error) { //nolint: gocyclo return c.maybeSelectDefault(flags, node) } +// IgnoreDefault can be implemented by flags that want to be applied before any default commands. +type IgnoreDefault interface { + IgnoreDefault() +} + // End of the line, check for a default command, but only if we're not displaying help, // otherwise we'd only ever display the help for the default command. func (c *Context) maybeSelectDefault(flags []*Flag, node *Node) error { for _, flag := range flags { - if flag.Name == "help" && flag.Set { + if _, ok := flag.Target.Interface().(IgnoreDefault); ok && flag.Set { return nil } } diff --git a/help.go b/help.go index 28290f6..8da1555 100644 --- a/help.go +++ b/help.go @@ -14,9 +14,11 @@ const ( ) // Help flag. -type helpValue bool +type helpFlag bool -func (h helpValue) BeforeReset(ctx *Context) error { +func (h helpFlag) IgnoreDefault() {} + +func (h helpFlag) BeforeReset(ctx *Context) error { options := ctx.Kong.helpOptions options.Summary = false err := ctx.Kong.help(options, ctx) diff --git a/kong.go b/kong.go index b85e145..a5e3d99 100644 --- a/kong.go +++ b/kong.go @@ -283,7 +283,7 @@ func (k *Kong) extraFlags() []*Flag { if k.noDefaultHelp { return nil } - var helpTarget helpValue + var helpTarget helpFlag value := reflect.ValueOf(&helpTarget).Elem() helpFlag := &Flag{ Short: 'h', From 5b36573738d800fbd511e9ed3c5711db6c7b6661 Mon Sep 17 00:00:00 2001 From: Bob Lail Date: Fri, 7 Mar 2025 14:28:09 -0800 Subject: [PATCH 65/81] feat: Allow kong.Path to describe remaining unparsed args (#472) As Kong traces a sequence of command line arguments, it parses them and appends them to the parsed `Path` sequence. For each element in `Path`, these is a corresponding sequence of unparsed arguments. This change enables `Path` to yield these. I have a package that uses Kong's hooks to instrument Kong applications (to monitor usage, reliability, etc of internal tools). I would like to instrument the commandline arguments as well. This change would enable it to work roughly as follows: ```golang func (Foo) BeforeApply(app *kong.Kong, ctx *kong.Context, t *Tracker) error { command := []string{ctx.Model.Name} var args []string for _, path := range ctx.Path { if path.Command != nil { command = append(command, path.Command.Name) args = path.Remainder() } } app.Exit = t.exit(app.Exit) t.WithCommand(strings.Join(command, " ")).WithArgs(args) return nil } ``` --- context.go | 56 ++++++++++++++++++++++++++++++++++++---------------- kong_test.go | 19 ++++++++++++++++++ scanner.go | 5 +++++ 3 files changed, 63 insertions(+), 17 deletions(-) diff --git a/context.go b/context.go index dcaf099..16a7353 100644 --- a/context.go +++ b/context.go @@ -26,6 +26,9 @@ type Path struct { // True if this Path element was created as the result of a resolver. Resolved bool + + // Remaining tokens after this node + remainder []Token } // Node returns the Node associated with this Path, or nil if Path is a non-Node. @@ -64,6 +67,15 @@ func (p *Path) Visitable() Visitable { return nil } +// Remainder returns the remaining unparsed args after this Path element. +func (p *Path) Remainder() []string { + args := []string{} + for _, token := range p.remainder { + args = append(args, token.String()) + } + return args +} + // Context contains the current parse context. type Context struct { *Kong @@ -87,14 +99,15 @@ type Context struct { // This just constructs a new trace. To fully apply the trace you must call Reset(), Resolve(), // Validate() and Apply(). func Trace(k *Kong, args []string) (*Context, error) { + s := Scan(args...) c := &Context{ Kong: k, Args: args, Path: []*Path{ - {App: k.Model, Flags: k.Model.Flags}, + {App: k.Model, Flags: k.Model.Flags, remainder: s.PeekAll()}, }, values: map[*Value]reflect.Value{}, - scan: Scan(args...), + scan: s, bindings: bindings{}, } c.Error = c.trace(c.Model.Node) @@ -477,6 +490,7 @@ func (c *Context) trace(node *Node) (err error) { //nolint: gocyclo c.Path = append(c.Path, &Path{ Parent: node, Positional: arg, + remainder: c.scan.PeekAll(), }) positional++ break @@ -508,9 +522,10 @@ func (c *Context) trace(node *Node) (err error) { //nolint: gocyclo if branch.Type == CommandNode && branch.Name == token.Value { c.scan.Pop() c.Path = append(c.Path, &Path{ - Parent: node, - Command: branch, - Flags: branch.Flags, + Parent: node, + Command: branch, + Flags: branch.Flags, + remainder: c.scan.PeekAll(), }) return c.trace(branch) } @@ -522,9 +537,10 @@ func (c *Context) trace(node *Node) (err error) { //nolint: gocyclo arg := branch.Argument if err := arg.Parse(c.scan, c.getValue(arg)); err == nil { c.Path = append(c.Path, &Path{ - Parent: node, - Argument: branch, - Flags: branch.Flags, + Parent: node, + Argument: branch, + Flags: branch.Flags, + remainder: c.scan.PeekAll(), }) return c.trace(branch) } @@ -535,9 +551,10 @@ func (c *Context) trace(node *Node) (err error) { //nolint: gocyclo // 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, + Parent: node, + Command: node.DefaultCmd, + Flags: node.DefaultCmd.Flags, + remainder: c.scan.PeekAll(), }) return c.trace(node.DefaultCmd) } @@ -565,9 +582,10 @@ func (c *Context) maybeSelectDefault(flags []*Flag, node *Node) error { } if node.DefaultCmd != nil { c.Path = append(c.Path, &Path{ - Parent: node.DefaultCmd, - Command: node.DefaultCmd, - Flags: node.DefaultCmd.Flags, + Parent: node.DefaultCmd, + Command: node.DefaultCmd, + Flags: node.DefaultCmd.Flags, + remainder: c.scan.PeekAll(), }) } return nil @@ -612,8 +630,9 @@ func (c *Context) Resolve() error { return err } inserted = append(inserted, &Path{ - Flag: flag, - Resolved: true, + Flag: flag, + Resolved: true, + remainder: c.scan.PeekAll(), }) } } @@ -757,7 +776,10 @@ func (c *Context) parseFlag(flags []*Flag, match string) (err error) { } flag.Value.Apply(value) } - c.Path = append(c.Path, &Path{Flag: flag}) + c.Path = append(c.Path, &Path{ + Flag: flag, + remainder: c.scan.PeekAll(), + }) return nil } return &unknownFlagError{Cause: findPotentialCandidates(match, candidates, "unknown flag %s", match)} diff --git a/kong_test.go b/kong_test.go index 15d1f51..4edda4f 100644 --- a/kong_test.go +++ b/kong_test.go @@ -48,6 +48,25 @@ func TestPositionalArguments(t *testing.T) { }) } +func TestRemainderReturnsUnparsedArgs(t *testing.T) { + var cli struct { + User struct { + Create struct { + ID int `kong:"arg"` + First string `kong:"arg"` + Last string `kong:"arg"` + } `kong:"cmd"` + } `kong:"cmd"` + } + p := mustNew(t, &cli) + args := []string{"user", "create", "10", "Alec", "Thomas"} + ctx, err := p.Parse(args) + assert.NoError(t, err) + for i, x := range ctx.Path { + assert.Equal(t, strings.Join(args[i:], " "), strings.Join(x.Remainder(), " ")) + } +} + func TestBranchingArgument(t *testing.T) { /* app user create diff --git a/scanner.go b/scanner.go index 262d16f..68f708e 100644 --- a/scanner.go +++ b/scanner.go @@ -203,6 +203,11 @@ func (s *Scanner) Peek() Token { return s.args[0] } +// PeekAll remaining tokens +func (s *Scanner) PeekAll() []Token { + return s.args +} + // Push an untyped Token onto the front of the Scanner. func (s *Scanner) Push(arg any) *Scanner { s.PushToken(Token{Value: arg}) From 73db2e86a5dee444a2089c62beff8703c82467b4 Mon Sep 17 00:00:00 2001 From: Alec Thomas Date: Sat, 8 Mar 2025 21:05:55 +1100 Subject: [PATCH 66/81] fix: ignore --help flag for determining optional flag usage Fixes #508 --- help_test.go | 14 +++++++------- model.go | 3 +++ model_test.go | 20 ++++++++++++++++++++ 3 files changed, 30 insertions(+), 7 deletions(-) diff --git a/help_test.go b/help_test.go index f8aebab..ccbfb63 100644 --- a/help_test.go +++ b/help_test.go @@ -51,7 +51,7 @@ func TestHelpOptionalArgs(t *testing.T) { assert.NoError(t, err) }) assert.True(t, exited) - expected := `Usage: test-app [ []] [flags] + expected := `Usage: test-app [ []] Arguments: [] One optional arg. @@ -320,7 +320,7 @@ func TestHelpTree(t *testing.T) { assert.NoError(t, err) }) assert.True(t, exited) - expected := `Usage: test-app [flags] + expected := `Usage: test-app A test app. @@ -353,7 +353,7 @@ Run "test-app --help" for more information on a command. assert.NoError(t, err) }) assert.True(t, exited) - expected := `Usage: test-app one (un,uno) [flags] + expected := `Usage: test-app one (un,uno) subcommand one @@ -413,7 +413,7 @@ func TestHelpCompactNoExpand(t *testing.T) { assert.NoError(t, err) }) assert.True(t, exited) - expected := `Usage: test-app [flags] + expected := `Usage: test-app A test app. @@ -442,7 +442,7 @@ Run "test-app --help" for more information on a command. assert.NoError(t, err) }) assert.True(t, exited) - expected := `Usage: test-app one (un,uno) [flags] + expected := `Usage: test-app one (un,uno) subcommand one @@ -795,7 +795,7 @@ func TestUsageOnError(t *testing.T) { _, err := p.Parse([]string{}) p.FatalIfErrorf(err) - expected := `Usage: test --flag=STRING [flags] + expected := `Usage: test --flag=STRING Some description. @@ -823,7 +823,7 @@ func TestShortUsageOnError(t *testing.T) { assert.Error(t, err) p.FatalIfErrorf(err) - expected := `Usage: test --flag=STRING [flags] + expected := `Usage: test --flag=STRING Run "test --help" for more information. test: error: missing flags: --flag=STRING diff --git a/model.go b/model.go index 065fcdd..33a6f33 100644 --- a/model.go +++ b/model.go @@ -167,6 +167,9 @@ func (n *Node) Summary() string { allFlags = append(allFlags, n.Parent.Flags...) } for _, flag := range allFlags { + if _, ok := flag.Target.Interface().(helpFlag); ok { + continue + } if !flag.Required { summary += " [flags]" break diff --git a/model_test.go b/model_test.go index 37bd632..2bc83b2 100644 --- a/model_test.go +++ b/model_test.go @@ -1,6 +1,7 @@ package kong_test import ( + "bytes" "testing" "github.com/alecthomas/assert/v2" @@ -70,3 +71,22 @@ func TestFlagString(t *testing.T) { assert.Equal(t, want, flag.String()) } } + +func TestIgnoreHelpInUsage(t *testing.T) { + var cli struct { + One string `required:""` + } + + k := mustNew(t, &cli) + w := &bytes.Buffer{} + k.Stdout = w + k.Exit = func(code int) {} + _, err := k.Parse([]string{"--help"}) + assert.Error(t, err) + assert.Equal(t, `Usage: test --one=STRING + +Flags: + -h, --help Show context-sensitive help. + --one=STRING +`, w.String()) +} From a86adbbb25c02f414c222d42bf4cec9bcb824f96 Mon Sep 17 00:00:00 2001 From: Bob Lail Date: Mon, 10 Mar 2025 23:21:09 -0700 Subject: [PATCH 67/81] feat: Allow Kong to exit with semantic exit codes (#507) * feat: Allow Kong to exit with semantic exit codes At Block, we've instrumented a number of commandline tools and set SLOs on some tools' reliability. To do that effectively, we had to partition usage errors from reliability issues. We looked at [prior art](https://github.com/square/exit?tab=readme-ov-file#reserved-codes-and-prior-art) and, taking inspiration from HTTP, defined [a set of semantic exit codes](https://github.com/square/exit?tab=readme-ov-file#about) in ranges: 80-99 for user errors, 100-119 for system errors. We've been wrapping errors in `exit.Error` at whatever level of the stack can tell which class an error is and unwrapping them at exit (`os.Exit(exit.FromError(err))`). This adds support for semantic exit codes to Kong, to `FatalIfErrorf`, which is used internally by `kong.Parse` and often used in Kong applications. * feat: Exit 80 (Usage Error) when usage is syntactically or semantically invalid * refactor: Always exit 80 (Usage Error) on a `ParseError` but don't wrap errors from hooks in `ParseError` --- error.go | 3 +++ exit.go | 32 ++++++++++++++++++++++++++++++++ help_test.go | 12 +++++++++--- kong.go | 21 ++++++++++++--------- 4 files changed, 56 insertions(+), 12 deletions(-) create mode 100644 exit.go diff --git a/error.go b/error.go index 18225ef..33a4e14 100644 --- a/error.go +++ b/error.go @@ -10,3 +10,6 @@ type ParseError struct { // Unwrap returns the original cause of the error. func (p *ParseError) Unwrap() error { return p.error } + +// ExitCode returns the status that Kong should exit with if it fails with a ParseError. +func (p *ParseError) ExitCode() int { return exitUsageError } diff --git a/exit.go b/exit.go new file mode 100644 index 0000000..4925f48 --- /dev/null +++ b/exit.go @@ -0,0 +1,32 @@ +package kong + +import "errors" + +const ( + exitOk = 0 + exitNotOk = 1 + + // Semantic exit codes from https://github.com/square/exit?tab=readme-ov-file#about + exitUsageError = 80 +) + +// ExitCoder is an interface that may be implemented by an error value to +// provide an integer exit code. The method ExitCode should return an integer +// that is intended to be used as the exit code for the application. +type ExitCoder interface { + ExitCode() int +} + +// exitCodeFromError returns the exit code for the given error. +// If err implements the exitCoder interface, the ExitCode method is called. +// Otherwise, exitCodeFromError returns 0 if err is nil, and 1 if it is not. +func exitCodeFromError(err error) int { + var e ExitCoder + if errors.As(err, &e) { + return e.ExitCode() + } else if err == nil { + return exitOk + } + + return exitNotOk +} diff --git a/help_test.go b/help_test.go index ccbfb63..9162d60 100644 --- a/help_test.go +++ b/help_test.go @@ -786,10 +786,11 @@ func TestUsageOnError(t *testing.T) { Flag string `help:"A required flag." required` } w := &strings.Builder{} + exitCode := -1 p := mustNew(t, &cli, kong.Writers(w, w), kong.Description("Some description."), - kong.Exit(func(int) {}), + kong.Exit(func(code int) { exitCode = code }), kong.UsageOnError(), ) _, err := p.Parse([]string{}) @@ -806,6 +807,7 @@ Flags: test: error: missing flags: --flag=STRING ` assert.Equal(t, expected, w.String()) + assert.Equal(t, 80, exitCode) } func TestShortUsageOnError(t *testing.T) { @@ -813,10 +815,11 @@ func TestShortUsageOnError(t *testing.T) { Flag string `help:"A required flag." required` } w := &strings.Builder{} + exitCode := -1 p := mustNew(t, &cli, kong.Writers(w, w), kong.Description("Some description."), - kong.Exit(func(int) {}), + kong.Exit(func(code int) { exitCode = code }), kong.ShortUsageOnError(), ) _, err := p.Parse([]string{}) @@ -829,6 +832,7 @@ Run "test --help" for more information. test: error: missing flags: --flag=STRING ` assert.Equal(t, expected, w.String()) + assert.Equal(t, 80, exitCode) } func TestCustomShortUsageOnError(t *testing.T) { @@ -840,10 +844,11 @@ func TestCustomShortUsageOnError(t *testing.T) { fmt.Fprintln(ctx.Stdout, "🤷 wish I could help") return nil } + exitCode := -1 p := mustNew(t, &cli, kong.Writers(w, w), kong.Description("Some description."), - kong.Exit(func(int) {}), + kong.Exit(func(code int) { exitCode = code }), kong.ShortHelp(shortHelp), kong.ShortUsageOnError(), ) @@ -856,4 +861,5 @@ func TestCustomShortUsageOnError(t *testing.T) { test: error: missing flags: --flag=STRING ` assert.Equal(t, expected, w.String()) + assert.Equal(t, 80, exitCode) } diff --git a/kong.go b/kong.go index a5e3d99..e000553 100644 --- a/kong.go +++ b/kong.go @@ -311,35 +311,35 @@ func (k *Kong) extraFlags() []*Flag { // invalid one, which will report a normal error). func (k *Kong) Parse(args []string) (ctx *Context, err error) { ctx, err = Trace(k, args) - if err != nil { + if err != nil { // Trace is not expected to return an err return nil, err } 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} + return nil, err } if err = ctx.Reset(); err != nil { return nil, &ParseError{error: err, Context: ctx} } if err = k.applyHook(ctx, "BeforeResolve"); err != nil { - return nil, &ParseError{error: err, Context: ctx} + return nil, err } if err = ctx.Resolve(); err != nil { return nil, &ParseError{error: err, Context: ctx} } if err = k.applyHook(ctx, "BeforeApply"); err != nil { - return nil, &ParseError{error: err, Context: ctx} + return nil, err } - if _, err = ctx.Apply(); err != nil { - return nil, &ParseError{error: err, Context: ctx} + if _, err = ctx.Apply(); err != nil { // Apply is not expected to return an err + return nil, err } if err = ctx.Validate(); err != nil { return nil, &ParseError{error: err, Context: ctx} } if err = k.applyHook(ctx, "AfterApply"); err != nil { - return nil, &ParseError{error: err, Context: ctx} + return nil, err } return ctx, nil } @@ -428,13 +428,15 @@ func (k *Kong) Errorf(format string, args ...any) *Kong { return k } -// Fatalf writes a message to Kong.Stderr with the application name prefixed then exits with a non-zero status. +// Fatalf writes a message to Kong.Stderr with the application name prefixed then exits with status 1. func (k *Kong) Fatalf(format string, args ...any) { k.Errorf(format, args...) k.Exit(1) } // FatalIfErrorf terminates with an error message if err != nil. +// If the error implements the ExitCoder interface, the ExitCode() method is called and +// the application exits with that status. Otherwise, the application exits with status 1. func (k *Kong) FatalIfErrorf(err error, args ...any) { if err == nil { return @@ -455,7 +457,8 @@ func (k *Kong) FatalIfErrorf(err error, args ...any) { fmt.Fprintln(k.Stdout) } } - k.Fatalf("%s", msg) + k.Errorf("%s", msg) + k.Exit(exitCodeFromError(err)) } // LoadConfig from path using the loader configured via Configuration(loader). From 3d03233b16d42450d1deb4b9f2aa18a837cc0f6c Mon Sep 17 00:00:00 2001 From: Alec Thomas Date: Tue, 11 Mar 2025 17:32:24 +1100 Subject: [PATCH 68/81] refactor: ParseError can carry an exit code Reinstated use of ParseError for all error paths so as to retain existing semantics. --- error.go | 10 ++++++++-- kong.go | 22 +++++++++++----------- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/error.go b/error.go index 33a4e14..e79a15d 100644 --- a/error.go +++ b/error.go @@ -5,11 +5,17 @@ package kong // It contains the parse Context that triggered the error. type ParseError struct { error - Context *Context + Context *Context + exitCode int } // Unwrap returns the original cause of the error. func (p *ParseError) Unwrap() error { return p.error } // ExitCode returns the status that Kong should exit with if it fails with a ParseError. -func (p *ParseError) ExitCode() int { return exitUsageError } +func (p *ParseError) ExitCode() int { + if p.exitCode == 0 { + return exitNotOk + } + return p.exitCode +} diff --git a/kong.go b/kong.go index e000553..4f6be87 100644 --- a/kong.go +++ b/kong.go @@ -312,34 +312,34 @@ func (k *Kong) extraFlags() []*Flag { func (k *Kong) Parse(args []string) (ctx *Context, err error) { ctx, err = Trace(k, args) if err != nil { // Trace is not expected to return an err - return nil, err + return nil, &ParseError{error: err, Context: ctx, exitCode: exitUsageError} } if ctx.Error != nil { - return nil, &ParseError{error: ctx.Error, Context: ctx} + return nil, &ParseError{error: ctx.Error, Context: ctx, exitCode: exitUsageError} } if err = k.applyHook(ctx, "BeforeReset"); err != nil { - return nil, err + return nil, &ParseError{error: err, Context: ctx} } if err = ctx.Reset(); err != nil { return nil, &ParseError{error: err, Context: ctx} } if err = k.applyHook(ctx, "BeforeResolve"); err != nil { - return nil, err + return nil, &ParseError{error: err, Context: ctx} } if err = ctx.Resolve(); err != nil { return nil, &ParseError{error: err, Context: ctx} } if err = k.applyHook(ctx, "BeforeApply"); err != nil { - return nil, err - } - if _, err = ctx.Apply(); err != nil { // Apply is not expected to return an err - return nil, err - } - if err = ctx.Validate(); err != nil { return nil, &ParseError{error: err, Context: ctx} } + if _, err = ctx.Apply(); err != nil { // Apply is not expected to return an err + return nil, &ParseError{error: err, Context: ctx} + } + if err = ctx.Validate(); err != nil { + return nil, &ParseError{error: err, Context: ctx, exitCode: exitUsageError} + } if err = k.applyHook(ctx, "AfterApply"); err != nil { - return nil, err + return nil, &ParseError{error: err, Context: ctx} } return ctx, nil } From 44be791798047748290119ce70a4e390a986324a Mon Sep 17 00:00:00 2001 From: Chris Gunn Date: Wed, 12 Mar 2025 17:49:21 -0700 Subject: [PATCH 69/81] feat: Placeholder string interpolation. (#510) Add support string interpolation in placeholder values. --- README.md | 4 ++-- kong.go | 5 +++++ kong_test.go | 6 ++++-- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 3b62344..9e1d6cc 100644 --- a/README.md +++ b/README.md @@ -620,8 +620,8 @@ also supports dynamically adding commands via `kong.DynamicCommand()`. ## Variable interpolation -Kong supports limited variable interpolation into help strings, enum lists and -default values. +Kong supports limited variable interpolation into help strings, placeholder strings, +enum lists and default values. Variables are in the form: diff --git a/kong.go b/kong.go index 4f6be87..a09b711 100644 --- a/kong.go +++ b/kong.go @@ -270,6 +270,11 @@ func (k *Kong) interpolateValue(value *Value, vars Vars) (err error) { if len(value.Flag.Envs) != 0 { updatedVars["env"] = value.Flag.Envs[0] } + + value.Flag.PlaceHolder, err = interpolate(value.Flag.PlaceHolder, vars, updatedVars) + if err != nil { + return fmt.Errorf("placeholder value for %s: %s", value.Summary(), err) + } } value.Help, err = interpolate(value.Help, vars, updatedVars) if err != nil { diff --git a/kong_test.go b/kong_test.go index 4edda4f..3ea6b5f 100644 --- a/kong_test.go +++ b/kong_test.go @@ -766,8 +766,8 @@ func TestPassesThroughOriginalCommandError(t *testing.T) { func TestInterpolationIntoModel(t *testing.T) { var cli struct { - Flag string `default:"${default_value}" help:"Help, I need ${somebody}" enum:"${enum}"` - EnumRef string `enum:"a,b" required:"" help:"One of ${enum}"` + Flag string `default:"${default_value}" help:"Help, I need ${somebody}" enum:"${enum}" placeholder:"${enum}"` + EnumRef string `enum:"a,b" required:"" help:"One of ${enum}" placeholder:"${enum}"` EnvRef string `env:"${env}" help:"God ${env}"` } _, err := kong.New(&cli) @@ -787,7 +787,9 @@ func TestInterpolationIntoModel(t *testing.T) { assert.Equal(t, "Help, I need chickens!", flag.Help) assert.Equal(t, map[string]bool{"a": true, "b": true, "c": true, "d": true}, flag.EnumMap()) assert.Equal(t, []string{"a", "b", "c", "d"}, flag.EnumSlice()) + assert.Equal(t, "a,b,c,d", flag.PlaceHolder) assert.Equal(t, "One of a,b", flag2.Help) + assert.Equal(t, "a,b", flag2.PlaceHolder) assert.Equal(t, []string{"SAVE_THE_QUEEN"}, flag3.Envs) assert.Equal(t, "God SAVE_THE_QUEEN", flag3.Help) } From 1edf069f4a1ad7499f6d4cc34b1e7af0d06d89b1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 17 Mar 2025 08:28:07 +1100 Subject: [PATCH 70/81] chore(deps): update all non-major dependencies (#506) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- _examples/server/go.mod | 32 ++++++++++++++++--- _examples/server/go.sum | 10 ++++++ bin/{.go-1.24.0.pkg => .go-1.24.1.pkg} | 0 ...thook-1.10.10.pkg => .lefthook-1.11.3.pkg} | 0 bin/go | 2 +- bin/gofmt | 2 +- bin/lefthook | 2 +- 7 files changed, 40 insertions(+), 8 deletions(-) rename bin/{.go-1.24.0.pkg => .go-1.24.1.pkg} (100%) rename bin/{.lefthook-1.10.10.pkg => .lefthook-1.11.3.pkg} (100%) diff --git a/_examples/server/go.mod b/_examples/server/go.mod index 39fccfe..20422cd 100644 --- a/_examples/server/go.mod +++ b/_examples/server/go.mod @@ -1,16 +1,38 @@ module kong_server -go 1.13 +go 1.23.0 + +toolchain go1.24.1 require ( github.com/alecthomas/colour v0.1.0 - github.com/alecthomas/kong v1.8.1 + github.com/alecthomas/kong v1.9.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.8 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/kr/pty v1.1.8 + golang.org/x/crypto v0.36.0 +) + +require ( + github.com/alecthomas/assert/v2 v2.11.0 // indirect + github.com/alecthomas/repr v0.4.0 // indirect + github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect + github.com/chzyer/logex v1.2.1 // indirect + github.com/chzyer/test v1.0.0 // indirect + github.com/creack/pty v1.1.7 // indirect + github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/hexops/gotextdiff v1.0.3 // indirect github.com/mattn/go-isatty v0.0.12 // indirect - golang.org/x/crypto v0.33.0 + github.com/yuin/goldmark v1.4.13 // indirect + golang.org/x/mod v0.17.0 // indirect + golang.org/x/net v0.25.0 // indirect + golang.org/x/sync v0.12.0 // indirect + golang.org/x/sys v0.31.0 // indirect + golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2 // indirect + golang.org/x/term v0.30.0 // indirect + golang.org/x/text v0.23.0 // indirect + golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect + golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 // indirect ) diff --git a/_examples/server/go.sum b/_examples/server/go.sum index 2449645..6bb22e5 100644 --- a/_examples/server/go.sum +++ b/_examples/server/go.sum @@ -26,6 +26,8 @@ github.com/alecthomas/kong v1.7.0 h1:MnT8+5JxFDCvISeI6vgd/mFbAJwueJ/pqQNzZMsiqZE github.com/alecthomas/kong v1.7.0/go.mod h1:p2vqieVMeTAnaC83txKtXe8FLke2X07aruPWXyMPQrU= github.com/alecthomas/kong v1.8.1 h1:6aamvWBE/REnR/BCq10EcozmcpUPc5aGI1lPAWdB0EE= github.com/alecthomas/kong v1.8.1/go.mod h1:p2vqieVMeTAnaC83txKtXe8FLke2X07aruPWXyMPQrU= +github.com/alecthomas/kong v1.9.0 h1:Wgg0ll5Ys7xDnpgYBuBn/wPeLGAuK0NvYmEcisJgrIs= +github.com/alecthomas/kong v1.9.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= @@ -108,6 +110,8 @@ golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= 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= @@ -134,6 +138,7 @@ golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -175,6 +180,8 @@ golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -210,6 +217,8 @@ golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg= golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= +golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= +golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -227,6 +236,7 @@ golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= diff --git a/bin/.go-1.24.0.pkg b/bin/.go-1.24.1.pkg similarity index 100% rename from bin/.go-1.24.0.pkg rename to bin/.go-1.24.1.pkg diff --git a/bin/.lefthook-1.10.10.pkg b/bin/.lefthook-1.11.3.pkg similarity index 100% rename from bin/.lefthook-1.10.10.pkg rename to bin/.lefthook-1.11.3.pkg diff --git a/bin/go b/bin/go index 42b4aa3..64209c2 120000 --- a/bin/go +++ b/bin/go @@ -1 +1 @@ -.go-1.24.0.pkg \ No newline at end of file +.go-1.24.1.pkg \ No newline at end of file diff --git a/bin/gofmt b/bin/gofmt index 42b4aa3..64209c2 120000 --- a/bin/gofmt +++ b/bin/gofmt @@ -1 +1 @@ -.go-1.24.0.pkg \ No newline at end of file +.go-1.24.1.pkg \ No newline at end of file diff --git a/bin/lefthook b/bin/lefthook index 518e63f..14a0a83 120000 --- a/bin/lefthook +++ b/bin/lefthook @@ -1 +1 @@ -.lefthook-1.10.10.pkg \ No newline at end of file +.lefthook-1.11.3.pkg \ No newline at end of file From 78d4066dabe5432d604eb28ab5376a3ad1708e29 Mon Sep 17 00:00:00 2001 From: Bob Lail Date: Fri, 21 Mar 2025 20:04:20 -0700 Subject: [PATCH 71/81] feat: Allow configuring global hooks via Kong's functional options (#511) Lets you pass `kong.WithBeforeApply` along with a function that supports dynamic bindings to register a `BeforeApply` hook without tying it directly to a node in the schema. Co-authored-by: Sutina Wipawiwat --- README.md | 18 ++++++++++++---- hooks.go | 6 ++++++ kong.go | 15 ++++++++++++- kong_test.go | 59 ++++++++++++++++++++++++++++++++++++++++++++++++++++ options.go | 34 ++++++++++++++++++++++++++++++ 5 files changed, 127 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 9e1d6cc..9f21cfe 100644 --- a/README.md +++ b/README.md @@ -308,10 +308,16 @@ func main() { ## Hooks: BeforeReset(), BeforeResolve(), BeforeApply(), AfterApply() -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. +If a node in the CLI, or any of its embedded fields, implements a `BeforeReset(...) error`, `BeforeResolve +(...) error`, `BeforeApply(...) error` and/or `AfterApply(...) error` method, those will be called as Kong +resets, resolves, validates, and assigns values to the node. + +| Hook | Description | +| --------------- | ----------------------------------------------------------------------------------------------------------- | +| `BeforeReset` | Invoked before values are reset to their defaults (as defined by the grammar) or to zero values | +| `BeforeResolve` | Invoked before resolvers are applied to a node | +| `BeforeApply` | Invoked before the traced command line arguments are applied to the grammar | +| `AfterApply` | Invoked after command line arguments are applied to the grammar **and validated**` | The `--help` flag is implemented with a `BeforeReset` hook. @@ -340,6 +346,10 @@ func main() { } ``` +It's also possible to register these hooks with the functional options +`kong.WithBeforeReset`, `kong.WithBeforeResolve`, `kong.WithBeforeApply`, and +`kong.WithAfterApply`. + ## The Bind() option Arguments to hooks are provided via the `Run(...)` method or `Bind(...)` option. `*Kong`, `*Context`, `*Path` and parent commands are also bound and finally, hooks can also contribute bindings via `kong.Context.Bind()` and `kong.Context.BindTo()`. diff --git a/hooks.go b/hooks.go index 9fdf24c..e95d21b 100644 --- a/hooks.go +++ b/hooks.go @@ -1,5 +1,11 @@ package kong +// BeforeReset is a documentation-only interface describing hooks that run before defaults values are applied. +type BeforeReset interface { + // This is not the correct signature - see README for details. + BeforeReset(args ...any) error +} + // BeforeResolve is a documentation-only interface describing hooks that run before resolvers are applied. type BeforeResolve interface { // This is not the correct signature - see README for details. diff --git a/kong.go b/kong.go index a09b711..4f3901f 100644 --- a/kong.go +++ b/kong.go @@ -71,6 +71,8 @@ type Kong struct { postBuildOptions []Option embedded []embedded dynamicCommands []*dynamicCommand + + hooks map[string][]reflect.Value } // New creates a new Kong parser on grammar. @@ -84,6 +86,7 @@ func New(grammar any, options ...Option) (*Kong, error) { registry: NewRegistry().RegisterDefaults(), vars: Vars{}, bindings: bindings{}, + hooks: make(map[string][]reflect.Value), helpFormatter: DefaultHelpValueFormatter, ignoreFields: make([]*regexp.Regexp, 0), flagNamer: func(s string) string { @@ -366,7 +369,7 @@ func (k *Kong) applyHook(ctx *Context, name string) error { default: panic("unsupported Path") } - for _, method := range getMethods(value, name) { + for _, method := range k.getMethods(value, name) { binds := k.bindings.clone() binds.add(ctx, trace) binds.add(trace.Node().Vars().CloneWith(k.vars)) @@ -380,6 +383,16 @@ func (k *Kong) applyHook(ctx *Context, name string) error { return k.applyHookToDefaultFlags(ctx, ctx.Path[0].Node(), name) } +func (k *Kong) getMethods(value reflect.Value, name string) []reflect.Value { + return append( + // Identify callbacks by reflecting on value + getMethods(value, name), + + // Identify callbacks that were registered with a kong.Option + k.hooks[name]..., + ) +} + // Call hook on any unset flags with default values. func (k *Kong) applyHookToDefaultFlags(ctx *Context, node *Node, name string) error { if node == nil { diff --git a/kong_test.go b/kong_test.go index 3ea6b5f..f2d315a 100644 --- a/kong_test.go +++ b/kong_test.go @@ -588,6 +588,65 @@ func TestHooks(t *testing.T) { } } +func TestGlobalHooks(t *testing.T) { + var cli struct { + One struct { + Two string `kong:"arg,optional"` + Three string + } `cmd:""` + } + + called := []string{} + log := func(name string) any { + return func(value *kong.Path) error { + switch { + case value.App != nil: + called = append(called, fmt.Sprintf("%s (app)", name)) + + case value.Positional != nil: + called = append(called, fmt.Sprintf("%s (arg) %s", name, value.Positional.Name)) + + case value.Flag != nil: + called = append(called, fmt.Sprintf("%s (flag) %s", name, value.Flag.Name)) + + case value.Argument != nil: + called = append(called, fmt.Sprintf("%s (arg) %s", name, value.Argument.Name)) + + case value.Command != nil: + called = append(called, fmt.Sprintf("%s (cmd) %s", name, value.Command.Name)) + } + return nil + } + } + p := mustNew(t, &cli, + kong.WithBeforeReset(log("BeforeReset")), + kong.WithBeforeResolve(log("BeforeResolve")), + kong.WithBeforeApply(log("BeforeApply")), + kong.WithAfterApply(log("AfterApply")), + ) + + _, err := p.Parse([]string{"one", "two", "--three=THREE"}) + assert.NoError(t, err) + assert.Equal(t, []string{ + "BeforeReset (app)", + "BeforeReset (cmd) one", + "BeforeReset (arg) two", + "BeforeReset (flag) three", + "BeforeResolve (app)", + "BeforeResolve (cmd) one", + "BeforeResolve (arg) two", + "BeforeResolve (flag) three", + "BeforeApply (app)", + "BeforeApply (cmd) one", + "BeforeApply (arg) two", + "BeforeApply (flag) three", + "AfterApply (app)", + "AfterApply (cmd) one", + "AfterApply (arg) two", + "AfterApply (flag) three", + }, called) +} + func TestShort(t *testing.T) { var cli struct { Bool bool `short:"b"` diff --git a/options.go b/options.go index d20b2fb..5fe3532 100644 --- a/options.go +++ b/options.go @@ -123,6 +123,40 @@ func PostBuild(fn func(*Kong) error) Option { }) } +// WithBeforeReset registers a hook to run before fields values are reset to their defaults +// (as specified in the grammar) or to zero values. +func WithBeforeReset(fn any) Option { + return withHook("BeforeReset", fn) +} + +// WithBeforeResolve registers a hook to run before resolvers are applied. +func WithBeforeResolve(fn any) Option { + return withHook("BeforeResolve", fn) +} + +// WithBeforeApply registers a hook to run before command line arguments are applied to the grammar. +func WithBeforeApply(fn any) Option { + return withHook("BeforeApply", fn) +} + +// WithAfterApply registers a hook to run after values are applied to the grammar and validated. +func WithAfterApply(fn any) Option { + return withHook("AfterApply", fn) +} + +// withHook registers a named hook. +func withHook(name string, fn any) Option { + value := reflect.ValueOf(fn) + if value.Kind() != reflect.Func { + panic(fmt.Errorf("expected function, got %s", value.Type())) + } + + return OptionFunc(func(k *Kong) error { + k.hooks[name] = append(k.hooks[name], value) + return nil + }) +} + // Name overrides the application name. func Name(name string) Option { return PostBuild(func(k *Kong) error { From 95c04ac28cf79636f5fd1ba53b8cf5d7944ea57b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 12 Apr 2025 15:49:32 +1000 Subject: [PATCH 72/81] chore(deps): update all non-major dependencies (#515) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- _examples/server/go.mod | 14 +++++++------- _examples/server/go.sum | 10 ++++++++++ bin/{.go-1.24.1.pkg => .go-1.24.2.pkg} | 0 bin/{.lefthook-1.11.3.pkg => .lefthook-1.11.9.pkg} | 0 bin/go | 2 +- bin/gofmt | 2 +- bin/lefthook | 2 +- 7 files changed, 20 insertions(+), 10 deletions(-) rename bin/{.go-1.24.1.pkg => .go-1.24.2.pkg} (100%) rename bin/{.lefthook-1.11.3.pkg => .lefthook-1.11.9.pkg} (100%) diff --git a/_examples/server/go.mod b/_examples/server/go.mod index 20422cd..f0ded0e 100644 --- a/_examples/server/go.mod +++ b/_examples/server/go.mod @@ -2,16 +2,16 @@ module kong_server go 1.23.0 -toolchain go1.24.1 +toolchain go1.24.2 require ( github.com/alecthomas/colour v0.1.0 - github.com/alecthomas/kong v1.9.0 + github.com/alecthomas/kong v1.10.0 github.com/chzyer/readline v1.5.1 github.com/gliderlabs/ssh v0.3.8 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/kr/pty v1.1.8 - golang.org/x/crypto v0.36.0 + golang.org/x/crypto v0.37.0 ) require ( @@ -28,11 +28,11 @@ require ( github.com/yuin/goldmark v1.4.13 // indirect golang.org/x/mod v0.17.0 // indirect golang.org/x/net v0.25.0 // indirect - golang.org/x/sync v0.12.0 // indirect - golang.org/x/sys v0.31.0 // indirect + golang.org/x/sync v0.13.0 // indirect + golang.org/x/sys v0.32.0 // indirect golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2 // indirect - golang.org/x/term v0.30.0 // indirect - golang.org/x/text v0.23.0 // indirect + golang.org/x/term v0.31.0 // indirect + golang.org/x/text v0.24.0 // indirect golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 // indirect ) diff --git a/_examples/server/go.sum b/_examples/server/go.sum index 6bb22e5..1c06140 100644 --- a/_examples/server/go.sum +++ b/_examples/server/go.sum @@ -28,6 +28,8 @@ github.com/alecthomas/kong v1.8.1 h1:6aamvWBE/REnR/BCq10EcozmcpUPc5aGI1lPAWdB0EE github.com/alecthomas/kong v1.8.1/go.mod h1:p2vqieVMeTAnaC83txKtXe8FLke2X07aruPWXyMPQrU= github.com/alecthomas/kong v1.9.0 h1:Wgg0ll5Ys7xDnpgYBuBn/wPeLGAuK0NvYmEcisJgrIs= github.com/alecthomas/kong v1.9.0/go.mod h1:p2vqieVMeTAnaC83txKtXe8FLke2X07aruPWXyMPQrU= +github.com/alecthomas/kong v1.10.0 h1:8K4rGDpT7Iu+jEXCIJUeKqvpwZHbsFRoebLbnzlmrpw= +github.com/alecthomas/kong v1.10.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= @@ -112,6 +114,8 @@ golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= +golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= 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= @@ -139,6 +143,7 @@ golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -182,6 +187,8 @@ golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= +golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -219,6 +226,8 @@ golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= +golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= +golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -237,6 +246,7 @@ golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= diff --git a/bin/.go-1.24.1.pkg b/bin/.go-1.24.2.pkg similarity index 100% rename from bin/.go-1.24.1.pkg rename to bin/.go-1.24.2.pkg diff --git a/bin/.lefthook-1.11.3.pkg b/bin/.lefthook-1.11.9.pkg similarity index 100% rename from bin/.lefthook-1.11.3.pkg rename to bin/.lefthook-1.11.9.pkg diff --git a/bin/go b/bin/go index 64209c2..97af0bd 120000 --- a/bin/go +++ b/bin/go @@ -1 +1 @@ -.go-1.24.1.pkg \ No newline at end of file +.go-1.24.2.pkg \ No newline at end of file diff --git a/bin/gofmt b/bin/gofmt index 64209c2..97af0bd 120000 --- a/bin/gofmt +++ b/bin/gofmt @@ -1 +1 @@ -.go-1.24.1.pkg \ No newline at end of file +.go-1.24.2.pkg \ No newline at end of file diff --git a/bin/lefthook b/bin/lefthook index 14a0a83..cd73351 120000 --- a/bin/lefthook +++ b/bin/lefthook @@ -1 +1 @@ -.lefthook-1.11.3.pkg \ No newline at end of file +.lefthook-1.11.9.pkg \ No newline at end of file From f0b321097eed010e1c58eda710b59dd61e9044dc Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 21 Apr 2025 13:02:03 +1000 Subject: [PATCH 73/81] chore(deps): update dependency lefthook to v1.11.10 (#527) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- bin/{.lefthook-1.11.9.pkg => .lefthook-1.11.10.pkg} | 0 bin/lefthook | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename bin/{.lefthook-1.11.9.pkg => .lefthook-1.11.10.pkg} (100%) diff --git a/bin/.lefthook-1.11.9.pkg b/bin/.lefthook-1.11.10.pkg similarity index 100% rename from bin/.lefthook-1.11.9.pkg rename to bin/.lefthook-1.11.10.pkg diff --git a/bin/lefthook b/bin/lefthook index cd73351..c3e7ea4 120000 --- a/bin/lefthook +++ b/bin/lefthook @@ -1 +1 @@ -.lefthook-1.11.9.pkg \ No newline at end of file +.lefthook-1.11.10.pkg \ No newline at end of file From 66d5762b66ba3ef4afe924a112ec25a4028d8d9f Mon Sep 17 00:00:00 2001 From: Sam Xie Date: Wed, 23 Apr 2025 15:24:47 -0700 Subject: [PATCH 74/81] Support parsing integer literals (#529) --- mapper.go | 4 ++-- mapper_test.go | 25 +++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/mapper.go b/mapper.go index db3f24e..7e97836 100644 --- a/mapper.go +++ b/mapper.go @@ -387,7 +387,7 @@ func intDecoder(bits int) MapperFunc { //nolint: dupl default: return fmt.Errorf("expected an int but got %q (%T)", t, t.Value) } - n, err := strconv.ParseInt(sv, 10, bits) + n, err := strconv.ParseInt(sv, 0, bits) if err != nil { return fmt.Errorf("expected a valid %d bit int but got %q", bits, sv) } @@ -416,7 +416,7 @@ func uintDecoder(bits int) MapperFunc { //nolint: dupl default: return fmt.Errorf("expected an int but got %q (%T)", t, t.Value) } - n, err := strconv.ParseUint(sv, 10, bits) + n, err := strconv.ParseUint(sv, 0, bits) if err != nil { return fmt.Errorf("expected a valid %d bit uint but got %q", bits, sv) } diff --git a/mapper_test.go b/mapper_test.go index 947f836..dd04119 100644 --- a/mapper_test.go +++ b/mapper_test.go @@ -395,6 +395,31 @@ func TestNumbers(t *testing.T) { I64: math.MinInt64, }, cli) }) + t.Run("Integer literals", func(t *testing.T) { + integerLiterals := "10_0" + + _, err := p.Parse([]string{ + fmt.Sprintf("--i-8=%s", integerLiterals), + fmt.Sprintf("--i-16=%s", integerLiterals), + fmt.Sprintf("--i-32=%s", integerLiterals), + fmt.Sprintf("--i-64=%s", integerLiterals), + fmt.Sprintf("--u-8=%s", integerLiterals), + fmt.Sprintf("--u-16=%s", integerLiterals), + fmt.Sprintf("--u-32=%s", integerLiterals), + fmt.Sprintf("--u-64=%s", integerLiterals), + }) + assert.NoError(t, err) + assert.Equal(t, CLI{ + I8: int8(100), + I16: int16(100), + I32: int32(100), + I64: int64(100), + U8: uint8(100), + U16: uint16(100), + U32: uint32(100), + U64: uint64(100), + }, cli) + }) } func TestJSONLargeNumber(t *testing.T) { From e923ecc9fff7ce927c5e38caa48c40fb80cd6051 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 28 Apr 2025 19:55:25 +1000 Subject: [PATCH 75/81] chore(deps): update dependency lefthook to v1.11.11 (#530) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- bin/{.lefthook-1.11.10.pkg => .lefthook-1.11.11.pkg} | 0 bin/lefthook | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename bin/{.lefthook-1.11.10.pkg => .lefthook-1.11.11.pkg} (100%) diff --git a/bin/.lefthook-1.11.10.pkg b/bin/.lefthook-1.11.11.pkg similarity index 100% rename from bin/.lefthook-1.11.10.pkg rename to bin/.lefthook-1.11.11.pkg diff --git a/bin/lefthook b/bin/lefthook index c3e7ea4..88a8747 120000 --- a/bin/lefthook +++ b/bin/lefthook @@ -1 +1 @@ -.lefthook-1.11.10.pkg \ No newline at end of file +.lefthook-1.11.11.pkg \ No newline at end of file From ebf6b70ab99d3acf389725a53bf202d1293adb13 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 9 May 2025 18:01:36 +1000 Subject: [PATCH 76/81] chore(deps): update all non-major dependencies (#531) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- _examples/server/go.mod | 12 ++++++------ _examples/server/go.sum | 8 ++++++++ bin/{.go-1.24.2.pkg => .go-1.24.3.pkg} | 0 bin/{.lefthook-1.11.11.pkg => .lefthook-1.11.12.pkg} | 0 bin/go | 2 +- bin/gofmt | 2 +- bin/lefthook | 2 +- 7 files changed, 17 insertions(+), 9 deletions(-) rename bin/{.go-1.24.2.pkg => .go-1.24.3.pkg} (100%) rename bin/{.lefthook-1.11.11.pkg => .lefthook-1.11.12.pkg} (100%) diff --git a/_examples/server/go.mod b/_examples/server/go.mod index f0ded0e..b26600b 100644 --- a/_examples/server/go.mod +++ b/_examples/server/go.mod @@ -2,7 +2,7 @@ module kong_server go 1.23.0 -toolchain go1.24.2 +toolchain go1.24.3 require ( github.com/alecthomas/colour v0.1.0 @@ -11,7 +11,7 @@ require ( github.com/gliderlabs/ssh v0.3.8 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/kr/pty v1.1.8 - golang.org/x/crypto v0.37.0 + golang.org/x/crypto v0.38.0 ) require ( @@ -28,11 +28,11 @@ require ( github.com/yuin/goldmark v1.4.13 // indirect golang.org/x/mod v0.17.0 // indirect golang.org/x/net v0.25.0 // indirect - golang.org/x/sync v0.13.0 // indirect - golang.org/x/sys v0.32.0 // indirect + golang.org/x/sync v0.14.0 // indirect + golang.org/x/sys v0.33.0 // indirect golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2 // indirect - golang.org/x/term v0.31.0 // indirect - golang.org/x/text v0.24.0 // indirect + golang.org/x/term v0.32.0 // indirect + golang.org/x/text v0.25.0 // indirect golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 // indirect ) diff --git a/_examples/server/go.sum b/_examples/server/go.sum index 1c06140..e23c9d4 100644 --- a/_examples/server/go.sum +++ b/_examples/server/go.sum @@ -116,6 +116,8 @@ golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= +golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= +golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= 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= @@ -144,6 +146,7 @@ golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -189,6 +192,8 @@ golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -228,6 +233,8 @@ golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= +golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= +golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -247,6 +254,7 @@ golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= diff --git a/bin/.go-1.24.2.pkg b/bin/.go-1.24.3.pkg similarity index 100% rename from bin/.go-1.24.2.pkg rename to bin/.go-1.24.3.pkg diff --git a/bin/.lefthook-1.11.11.pkg b/bin/.lefthook-1.11.12.pkg similarity index 100% rename from bin/.lefthook-1.11.11.pkg rename to bin/.lefthook-1.11.12.pkg diff --git a/bin/go b/bin/go index 97af0bd..7f86f93 120000 --- a/bin/go +++ b/bin/go @@ -1 +1 @@ -.go-1.24.2.pkg \ No newline at end of file +.go-1.24.3.pkg \ No newline at end of file diff --git a/bin/gofmt b/bin/gofmt index 97af0bd..7f86f93 120000 --- a/bin/gofmt +++ b/bin/gofmt @@ -1 +1 @@ -.go-1.24.2.pkg \ No newline at end of file +.go-1.24.3.pkg \ No newline at end of file diff --git a/bin/lefthook b/bin/lefthook index 88a8747..d3533ef 120000 --- a/bin/lefthook +++ b/bin/lefthook @@ -1 +1 @@ -.lefthook-1.11.11.pkg \ No newline at end of file +.lefthook-1.11.12.pkg \ No newline at end of file From 8e03dbeaf6a949e5cb184d76b80a760180478496 Mon Sep 17 00:00:00 2001 From: educhastenier Date: Mon, 12 May 2025 11:20:28 +0200 Subject: [PATCH 77/81] fix: broken internal link (#532) Fix of minor broken link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9f21cfe..a976332 100644 --- a/README.md +++ b/README.md @@ -181,7 +181,7 @@ Flags: #### 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)) +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](#branching-positional-arguments)) **Contextual argument help** From 9bc3bf9925397be48270da0e258bfb0a4f6ed96a Mon Sep 17 00:00:00 2001 From: Alec Thomas Date: Thu, 15 May 2025 19:31:29 +1000 Subject: [PATCH 78/81] chore: optionally allow parsing of hyphen-prefixied flag parameters This allows for eg. `foo --number -10`, `foo --flag -bar`. Fixes #478, #315. --- context.go | 2 +- kong.go | 21 +++++++++++---------- kong_test.go | 45 ++++++++++++++++++++++++++++++++++++--------- options.go | 10 ++++++++++ scanner.go | 13 +++++++++++-- 5 files changed, 69 insertions(+), 22 deletions(-) diff --git a/context.go b/context.go index 16a7353..6a4989f 100644 --- a/context.go +++ b/context.go @@ -99,7 +99,7 @@ type Context struct { // This just constructs a new trace. To fully apply the trace you must call Reset(), Resolve(), // Validate() and Apply(). func Trace(k *Kong, args []string) (*Context, error) { - s := Scan(args...) + s := Scan(args...).AllowHyphenPrefixedParameters(k.allowHyphenated) c := &Context{ Kong: k, Args: args, diff --git a/kong.go b/kong.go index 4f3901f..2334a8a 100644 --- a/kong.go +++ b/kong.go @@ -56,16 +56,17 @@ type Kong struct { registry *Registry ignoreFields []*regexp.Regexp - noDefaultHelp bool - usageOnError usageOnError - help HelpPrinter - shortHelp HelpPrinter - helpFormatter HelpValueFormatter - helpOptions HelpOptions - helpFlag *Flag - groups []Group - vars Vars - flagNamer func(string) string + noDefaultHelp bool + allowHyphenated 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 diff --git a/kong_test.go b/kong_test.go index f2d315a..834155d 100644 --- a/kong_test.go +++ b/kong_test.go @@ -1043,15 +1043,6 @@ func TestParentBindings(t *testing.T) { assert.Equal(t, "foo", cli.Command.value) } -func TestNumericParamErrors(t *testing.T) { - var cli struct { - Name string - } - parser := mustNew(t, &cli) - _, err := parser.Parse([]string{"--name", "-10"}) - assert.EqualError(t, err, `--name: expected string value but got "-10" (short flag); perhaps try --name="-10"?`) -} - func TestDefaultValueIsHyphen(t *testing.T) { var cli struct { Flag string `default:"-"` @@ -2677,3 +2668,39 @@ func TestProviderWithoutError(t *testing.T) { err = kctx.Run() assert.NoError(t, err) } + +func TestParseHyphenParameter(t *testing.T) { + type shortFlag struct { + Flag string `short:"f"` + Other string `short:"o"` + Numeric int `short:"n"` + } + + t.Run("ShortFlag", func(t *testing.T) { + actual := &shortFlag{} + _, err := mustNew(t, actual, kong.WithHyphenPrefixedParameters(true)).Parse([]string{"-f", "-foo"}) + assert.NoError(t, err) + assert.Equal(t, &shortFlag{Flag: "-foo"}, actual) + }) + + t.Run("LongFlag", func(t *testing.T) { + actual := &shortFlag{} + _, err := mustNew(t, actual, kong.WithHyphenPrefixedParameters(true)).Parse([]string{"--flag", "-foo"}) + assert.NoError(t, err) + assert.Equal(t, &shortFlag{Flag: "-foo"}, actual) + }) + + t.Run("ParamMatchesFlag", func(t *testing.T) { + actual := &shortFlag{} + _, err := mustNew(t, actual, kong.WithHyphenPrefixedParameters(true)).Parse([]string{"--flag", "-oo"}) + assert.NoError(t, err) + assert.Equal(t, &shortFlag{Flag: "-oo"}, actual) + }) + + t.Run("NegativeNumber", func(t *testing.T) { + actual := &shortFlag{} + _, err := mustNew(t, actual, kong.WithHyphenPrefixedParameters(true)).Parse([]string{"--numeric", "-10"}) + assert.NoError(t, err) + assert.Equal(t, &shortFlag{Numeric: -10}, actual) + }) +} diff --git a/options.go b/options.go index 5fe3532..a1fa242 100644 --- a/options.go +++ b/options.go @@ -55,6 +55,16 @@ func Exit(exit func(int)) Option { }) } +// WithHyphenPrefixedParameters enables or disables hyphen-prefixed parameters. +// +// These are disabled by default. +func WithHyphenPrefixedParameters(enable bool) Option { + return OptionFunc(func(k *Kong) error { + k.allowHyphenated = enable + return nil + }) +} + type embedded struct { strct any tags []string diff --git a/scanner.go b/scanner.go index 68f708e..511bf8f 100644 --- a/scanner.go +++ b/scanner.go @@ -111,7 +111,8 @@ func (t Token) IsValue() bool { // // [{FlagToken, "foo"}, {FlagValueToken, "bar"}] type Scanner struct { - args []Token + allowHyphenated bool + args []Token } // ScanAsType creates a new Scanner from args with the given type. @@ -133,6 +134,14 @@ func ScanFromTokens(tokens ...Token) *Scanner { return &Scanner{args: tokens} } +// AllowHyphenPrefixedParameters enables or disables hyphen-prefixed flag parameters on this Scanner. +// +// Disabled by default. +func (s *Scanner) AllowHyphenPrefixedParameters(enable bool) *Scanner { + s.allowHyphenated = enable + return s +} + // Len returns the number of input arguments. func (s *Scanner) Len() int { return len(s.args) @@ -162,7 +171,7 @@ func (e *expectedError) Error() string { // "context" is used to assist the user if the value can not be popped, eg. "expected value but got " func (s *Scanner) PopValue(context string) (Token, error) { t := s.Pop() - if !t.IsValue() { + if !s.allowHyphenated && !t.IsValue() { return t, &expectedError{context, t} } return t, nil From a521b46eaf1e98a20e4818e89410f93553707979 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 7 Jun 2025 13:22:41 +1000 Subject: [PATCH 79/81] chore(deps): update all non-major dependencies (#533) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- _examples/server/go.mod | 18 +++++++++--------- _examples/server/go.sum | 10 ++++++++++ bin/{.go-1.24.3.pkg => .go-1.24.4.pkg} | 0 ...thook-1.11.12.pkg => .lefthook-1.11.13.pkg} | 0 bin/go | 2 +- bin/gofmt | 2 +- bin/lefthook | 2 +- 7 files changed, 22 insertions(+), 12 deletions(-) rename bin/{.go-1.24.3.pkg => .go-1.24.4.pkg} (100%) rename bin/{.lefthook-1.11.12.pkg => .lefthook-1.11.13.pkg} (100%) diff --git a/_examples/server/go.mod b/_examples/server/go.mod index b26600b..39bdd81 100644 --- a/_examples/server/go.mod +++ b/_examples/server/go.mod @@ -2,16 +2,16 @@ module kong_server go 1.23.0 -toolchain go1.24.3 +toolchain go1.24.4 require ( github.com/alecthomas/colour v0.1.0 - github.com/alecthomas/kong v1.10.0 + github.com/alecthomas/kong v1.11.0 github.com/chzyer/readline v1.5.1 github.com/gliderlabs/ssh v0.3.8 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/kr/pty v1.1.8 - golang.org/x/crypto v0.38.0 + golang.org/x/crypto v0.39.0 ) require ( @@ -26,13 +26,13 @@ require ( github.com/hexops/gotextdiff v1.0.3 // indirect github.com/mattn/go-isatty v0.0.12 // indirect github.com/yuin/goldmark v1.4.13 // indirect - golang.org/x/mod v0.17.0 // indirect - golang.org/x/net v0.25.0 // indirect - golang.org/x/sync v0.14.0 // indirect + golang.org/x/mod v0.25.0 // indirect + golang.org/x/net v0.40.0 // indirect + golang.org/x/sync v0.15.0 // indirect golang.org/x/sys v0.33.0 // indirect - golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2 // indirect + golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457 // indirect golang.org/x/term v0.32.0 // indirect - golang.org/x/text v0.25.0 // indirect - golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect + golang.org/x/text v0.26.0 // indirect + golang.org/x/tools v0.33.0 // indirect golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 // indirect ) diff --git a/_examples/server/go.sum b/_examples/server/go.sum index e23c9d4..503aa3a 100644 --- a/_examples/server/go.sum +++ b/_examples/server/go.sum @@ -30,6 +30,8 @@ github.com/alecthomas/kong v1.9.0 h1:Wgg0ll5Ys7xDnpgYBuBn/wPeLGAuK0NvYmEcisJgrIs github.com/alecthomas/kong v1.9.0/go.mod h1:p2vqieVMeTAnaC83txKtXe8FLke2X07aruPWXyMPQrU= github.com/alecthomas/kong v1.10.0 h1:8K4rGDpT7Iu+jEXCIJUeKqvpwZHbsFRoebLbnzlmrpw= github.com/alecthomas/kong v1.10.0/go.mod h1:p2vqieVMeTAnaC83txKtXe8FLke2X07aruPWXyMPQrU= +github.com/alecthomas/kong v1.11.0 h1:y++1gI7jf8O7G7l4LZo5ASFhrhJvzc+WgF/arranEmM= +github.com/alecthomas/kong v1.11.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= @@ -118,11 +120,14 @@ golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= +golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= +golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= 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= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= @@ -134,6 +139,7 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -147,6 +153,7 @@ golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -195,6 +202,7 @@ golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= +golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -255,10 +263,12 @@ golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= +golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/bin/.go-1.24.3.pkg b/bin/.go-1.24.4.pkg similarity index 100% rename from bin/.go-1.24.3.pkg rename to bin/.go-1.24.4.pkg diff --git a/bin/.lefthook-1.11.12.pkg b/bin/.lefthook-1.11.13.pkg similarity index 100% rename from bin/.lefthook-1.11.12.pkg rename to bin/.lefthook-1.11.13.pkg diff --git a/bin/go b/bin/go index 7f86f93..2625879 120000 --- a/bin/go +++ b/bin/go @@ -1 +1 @@ -.go-1.24.3.pkg \ No newline at end of file +.go-1.24.4.pkg \ No newline at end of file diff --git a/bin/gofmt b/bin/gofmt index 7f86f93..2625879 120000 --- a/bin/gofmt +++ b/bin/gofmt @@ -1 +1 @@ -.go-1.24.3.pkg \ No newline at end of file +.go-1.24.4.pkg \ No newline at end of file diff --git a/bin/lefthook b/bin/lefthook index d3533ef..b6dd74f 120000 --- a/bin/lefthook +++ b/bin/lefthook @@ -1 +1 @@ -.lefthook-1.11.12.pkg \ No newline at end of file +.lefthook-1.11.13.pkg \ No newline at end of file From fcd23468fec8c8072f661adb89f2beada3aee42f Mon Sep 17 00:00:00 2001 From: Sam Xie Date: Tue, 17 Jun 2025 14:45:17 -0700 Subject: [PATCH 80/81] Update license of levenshtein method (#539) --- levenshtein.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/levenshtein.go b/levenshtein.go index 6837d6c..fe11745 100644 --- a/levenshtein.go +++ b/levenshtein.go @@ -2,8 +2,8 @@ package kong import "unicode/utf8" -// https://en.wikibooks.org/wiki/Algorithm_Implementation/Strings/Levenshtein_distance#Go -// License: https://creativecommons.org/licenses/by-sa/3.0/ +// Copied from https://github.com/daviddengcn/go-algs/blob/fe23fabd9d0670e4675326040ba7c285c7117b4c/ed/ed.go#L31 +// License: https://github.com/daviddengcn/go-algs/blob/fe23fabd9d0670e4675326040ba7c285c7117b4c/LICENSE func levenshtein(a, b string) int { f := make([]int, utf8.RuneCountInString(b)+1) From 8469b5cc3c4e51cd6d173718928e221d115fdc8f Mon Sep 17 00:00:00 2001 From: Alec Thomas Date: Mon, 30 Jun 2025 10:56:36 +1000 Subject: [PATCH 81/81] chore: minor tweak for tinygo There are still reflection issues, but it's a step. --- guesswidth.go | 3 +-- guesswidth_unix.go | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/guesswidth.go b/guesswidth.go index dfdc3f5..2c19cac 100644 --- a/guesswidth.go +++ b/guesswidth.go @@ -1,5 +1,4 @@ -//go:build appengine || (!linux && !freebsd && !darwin && !dragonfly && !netbsd && !openbsd) -// +build appengine !linux,!freebsd,!darwin,!dragonfly,!netbsd,!openbsd +//go:build tinygo || appengine || (!linux && !freebsd && !darwin && !dragonfly && !netbsd && !openbsd) package kong diff --git a/guesswidth_unix.go b/guesswidth_unix.go index 0170055..7fc5d02 100644 --- a/guesswidth_unix.go +++ b/guesswidth_unix.go @@ -1,5 +1,4 @@ -//go:build (!appengine && linux) || freebsd || darwin || dragonfly || netbsd || openbsd -// +build !appengine,linux freebsd darwin dragonfly netbsd openbsd +//go:build !tinygo && ((!appengine && linux) || freebsd || darwin || dragonfly || netbsd || openbsd) package kong