Merge remote-tracking branch 'upstream/master'

This commit is contained in:
S.Solodyagin
2025-07-02 20:54:46 +03:00
44 changed files with 1927 additions and 574 deletions
+2 -2
View File
@@ -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
+9 -13
View File
@@ -7,21 +7,16 @@ output:
linters:
enable-all: true
disable:
- maligned
- lll
- gochecknoglobals
- wsl
- funlen
- gocognit
- gomnd
- goprintffuncname
- paralleltest
- nlreturn
- goerr113
- ifshort
- testpackage
- wrapcheck
- exhaustivestruct
- forbidigo
- gci
- godot
@@ -29,9 +24,6 @@ linters:
- cyclop
- errorlint
- nestif
- golint
- scopelint
- interfacer
- tagliatelle
- thelper
- godox
@@ -41,16 +33,19 @@ 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
- tenv # deprecated since v1.64, but not removed yet
- mnd
- perfsprint
- err113
- copyloopvar
- intrange
- nakedret
- recvcheck # value receivers are intentionally used for copies
linters-settings:
govet:
check-shadowing: true
# These govet checks are disabled by default, but they're useful.
enable:
- niliness
@@ -76,6 +71,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.
+110 -80
View File
@@ -5,16 +5,16 @@
[![](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)
<!-- TOC depthfrom:2 depthto:3 -->
- [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)
- [Attach a `Run(...) error` method to each command](#attach-a-run-error-method-to-each-command)
- [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)
@@ -25,22 +25,26 @@
- [Nested data structure](#nested-data-structure)
- [Custom named decoders](#custom-named-decoders)
- [Supported field types](#supported-field-types)
- [Custom decoders mappers](#custom-decoders-mappers)
- [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)
- [`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)
<!-- /TOC -->
## 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](https://github.com/alecthomas/kong/pull/436), which should effect relatively few users.
## Introduction
@@ -177,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**
@@ -302,17 +306,21 @@ func main() {
```
## Hooks: BeforeReset(), BeforeResolve(), BeforeApply(), AfterApply() and the Bind() option
## Hooks: BeforeReset(), BeforeResolve(), BeforeApply(), AfterApply()
If a node in the grammar has a `BeforeReset(...)`, `BeforeResolve
(...)`, `BeforeApply(...) error` and/or `AfterApply(...) error` method, those
methods will be called before values are reset, before validation/assignment,
and after validation/assignment, respectively.
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.
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
@@ -338,38 +346,42 @@ func main() {
}
```
Another example of using hooks is load the env-file:
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()`.
eg:
```go
package main
type CLI struct {
Debug bool `help:"Enable debug mode."`
import (
"fmt"
"git.company.lan/gopkg/kong"
"github.com/joho/godotenv"
)
Rm RmCmd `cmd:"" help:"Remove files."`
Ls LsCmd `cmd:"" help:"List paths."`
}
type EnvFlag string
type AuthorName 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
}
// ...
func (l *LsCmd) Run(cli *CLI) error {
// use cli.Debug here !!
return nil
}
var CLI struct {
EnvFile EnvFlag
Flag `env:"FLAG"`
func (r *RmCmD) Run(author AuthorName) error{
// use binded author here
return nil
}
func main() {
_ = kong.Parse(&CLI)
fmt.Println(CLI.Flag)
}
var cli CLI
ctx := kong.Parse(&cli, Bind(AuthorName("penguin")))
err := ctx.Run()
```
## Flags
@@ -553,36 +565,44 @@ 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. |
| `name:"X"` | Long name, for overriding field name. |
| `help:"X"` | Help text. |
| `type:"X"` | Specify [named types](#custom-named-decoders) to use. |
| `placeholder:"X"` | Placeholder text. |
| `default:"X"` | Default value. |
| `default:"1"` | On a command, make it the default. |
| `default:"withargs"` | On a command, make it the default and allow args/flags from that command |
| `short:"X"` | Short name, if flag. |
| `aliases:"X,Y"` | One or more aliases (for cmd or flag). |
| `required:""` | If present, flag/arg is required. |
| `optional:""` | If present, flag/arg is optional. |
| `hidden:""` | If present, command or flag is hidden. |
| `negatable:""` | If present on a `bool` field, supports prefixing a flag with `--no-` to invert the default value |
| `format:"X"` | Format for parsing input, if supported. |
| `sep:"X"` | Separator for sequences (defaults to ","). May be `none` to disable splitting. |
| `mapsep:"X"` | Separator for maps (defaults to ";"). May be `none` to disable splitting. |
| `enum:"X,Y,..."` | Set of valid values allowed for this flag. An enum field must be `required` or have a valid `default`. |
| `group:"X"` | Logical group for a flag or command. |
| `xor:"X,Y,..."` | Exclusive OR groups for flags. Only one flag in the group can be used which is restricted within the same command. When combined with `required`, at least one of the `xor` group will be required. |
| `prefix:"X"` | Prefix for all sub-flags. |
| `envprefix:"X"` | Envar prefix for all sub-flags. |
| `set:"K=V"` | Set a variable for expansion by child elements. Multiples can occur. |
| `embed:""` | If present, this field's children will be embedded in the parent. Useful for composition. |
| `passthrough:""` | If present on a positional argument, it stops flag parsing when encountered, as if `--` was processed before. Useful for external command wrappers, like `exec`. On a command it requires that the command contains only one argument of type `[]string` which is then filled with everything following the command, unparsed. |
| `-` | Ignore the field. Useful for adding non-CLI fields to a configuration struct. e.g `` `kong:"-"` `` |
| Tag | Description |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `cmd:""` | If present, struct is a command. |
| `arg:""` | If present, field is an argument. Required by default. |
| `env:"X,Y,..."` | Specify envars to use for default value. The envs are resolved in the declared order. The first value found is used. |
| `name:"X"` | Long name, for overriding field name. |
| `help:"X"` | Help text. |
| `type:"X"` | Specify [named types](#custom-named-decoders) to use. |
| `placeholder:"X"` | Placeholder input, if flag. e.g. `` `placeholder:"<the-placeholder>"` `` will show `--flag-name=<the-placeholder>` 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 |
| `short:"X"` | Short name, if flag. |
| `aliases:"X,Y"` | One or more aliases (for cmd or flag). |
| `required:""` | If present, flag/arg is required. |
| `optional:""` | If present, flag/arg is optional. |
| `hidden:""` | If present, command or flag is hidden. |
| `negatable:""` | If present on a `bool` field, supports prefixing a flag with `--no-` to invert the default value |
| `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. |
| `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,..."` | 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:"<mode>"`[^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]:
`<mode>` 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
@@ -611,8 +631,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:
@@ -654,8 +674,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 {
@@ -663,12 +682,18 @@ 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.
## 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).
@@ -726,20 +751,25 @@ 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:"<name>"`.
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
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.
### `Bind(...)` - bind values for callback hooks and Run() methods
### Injecting values into `Run()` methods
See the [section on hooks](#hooks-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<Type>() error` methods on the command structure.
### Other options
+12 -4
View File
@@ -1,17 +1,25 @@
module kong_server
go 1.13
go 1.23.0
toolchain go1.24.4
require (
git.company.lan/gopkg/kong v0.0.0-00010101000000-000000000000
github.com/alecthomas/colour v0.1.0
github.com/chzyer/readline v1.5.1
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
golang.org/x/term v0.32.0
)
require (
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
github.com/creack/pty v1.1.7 // indirect
github.com/mattn/go-isatty v0.0.12 // indirect
golang.org/x/crypto v0.21.0 // indirect
golang.org/x/term v0.18.0
golang.org/x/crypto v0.39.0 // indirect
golang.org/x/sys v0.33.0 // indirect
)
replace git.company.lan/gopkg/kong => ../../
+10 -52
View File
@@ -1,5 +1,5 @@
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.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/colour v0.1.0 h1:nOE9rJm6dsZ66RGWYSFrXw461ZIt9A6+nHgL7FRrDUk=
github.com/alecthomas/colour v0.1.0/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0=
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
@@ -14,8 +14,8 @@ github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
github.com/creack/pty v1.1.7 h1:6pwm8kMQKCmgUg0ZHTm5+/YvRK0s3THD/28+T6/kk4A=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
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/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 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
@@ -24,53 +24,11 @@ github.com/kr/pty v1.1.8 h1:AkaSdXYQOWeaO3neb8EM634ahkXXe3jYbVh/F9lq+GI=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
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/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=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
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.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
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/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
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/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/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.15.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/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.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.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
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/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.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
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.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
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/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
+7 -4
View File
@@ -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 <msg>":
fmt.Println(cli.Echo.Msg)
+1
View File
@@ -0,0 +1 @@
hermit
+1 -1
View File
@@ -1 +1 @@
.go-1.22.1.pkg
.go-1.24.4.pkg
+1 -1
View File
@@ -1 +1 @@
.go-1.22.1.pkg
.go-1.24.4.pkg
+1 -1
View File
@@ -1 +1 @@
.golangci-lint-1.55.2.pkg
.golangci-lint-1.64.5.pkg
+1
View File
@@ -0,0 +1 @@
.lefthook-1.11.13.pkg
+63 -15
View File
@@ -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 {
@@ -51,6 +51,10 @@ 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
}
ignored := map[string]bool{}
for i := 0; i < v.NumField(); i++ {
ft := v.Type().Field(i)
fv := v.Field(i)
@@ -58,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.
@@ -68,6 +73,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.
@@ -102,13 +108,31 @@ 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.
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,
@@ -144,6 +168,18 @@ MAIN:
}
}
if len(tag.Xor) != 0 {
for i := range tag.Xor {
tag.Xor[i] = tag.XorPrefix + tag.Xor[i]
}
}
if len(tag.And) != 0 {
for i := range tag.And {
tag.And[i] = tag.XorPrefix + tag.And[i]
}
}
// Nested structs are either commands or args, unless they implement the Mapper interface.
if field.value.Kind() == reflect.Struct && (tag.Cmd || tag.Arg) && k.registry.ForValue(fv) == nil {
typ := CommandNode
@@ -170,6 +206,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)
}
@@ -275,17 +314,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),
@@ -312,6 +352,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,
@@ -320,6 +367,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
+143 -41
View File
@@ -6,7 +6,59 @@ import (
"strings"
)
type bindings map[reflect.Type]func() (reflect.Value, error)
// 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]*binding
func (b bindings) String() string {
out := []string{}
@@ -16,35 +68,36 @@ 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() (reflect.Value, error) { return reflect.ValueOf(v), nil }
val := reflect.ValueOf(v)
b[val.Type()] = newValueBinding(val)
}
return b
}
func (b bindings) addTo(impl, iface interface{}) {
valueOf := reflect.ValueOf(impl)
b[reflect.TypeOf(iface).Elem()] = func() (reflect.Value, error) { return valueOf, nil }
func (b bindings) addTo(impl, iface any) {
val := reflect.ValueOf(impl)
b[reflect.TypeOf(iface).Elem()] = newValueBinding(val)
}
func (b bindings) addProvider(provider interface{}) error {
func (b bindings) addProvider(provider any, singleton bool) 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 {
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] = 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] = newFunctionBinding(pv, singleton)
return nil
}
@@ -74,32 +127,67 @@ func getMethod(value reflect.Value, name string) reflect.Value {
return method
}
// 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) (methods []reflect.Value) {
if value.Kind() == reflect.Ptr {
value = value.Elem()
}
if !value.IsValid() {
return
}
if method := getMethod(value, 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
}
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.Kind() == reflect.Interface || ferrv.Kind() == reflect.Pointer) && 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) {
@@ -110,15 +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)
if argf, ok := bindings[pt]; ok {
argv, err := argf()
if err != nil {
return nil, err
}
in = append(in, argv)
} else {
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(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
}
val := reflect.ValueOf(argv[0])
binding.Set(val)
in = append(in, val)
}
outv := f.Call(in)
out = make([]any, len(outv))
+6 -9
View File
@@ -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 interface{}) (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()
}
+220 -56
View File
@@ -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...).AllowHyphenPrefixedParameters(k.allowHyphenated)
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)
@@ -102,7 +115,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 +124,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,8 +132,20 @@ 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 {
return c.bindings.addProvider(provider)
//
// "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, 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.
@@ -208,7 +233,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)
}
@@ -259,7 +284,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
}
@@ -306,7 +331,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]
@@ -347,6 +372,7 @@ func (c *Context) endParsing() {
}
}
//nolint:maintidx
func (c *Context) trace(node *Node) (err error) { //nolint: gocyclo
positional := 0
node.Active = true
@@ -383,9 +409,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()
@@ -420,12 +450,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].PassthroughMode == PassThroughModeAll {
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].PassthroughMode == PassThroughModeAll {
c.scan.Pop()
c.scan.PushTyped(token.String(), PositionalArgumentToken)
} else {
return err
}
}
case FlagValueToken:
@@ -450,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
@@ -481,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)
}
@@ -495,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)
}
@@ -508,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)
}
@@ -523,19 +567,25 @@ 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
}
}
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
@@ -557,7 +607,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 {
@@ -580,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(),
})
}
}
@@ -700,13 +751,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))
@@ -725,16 +776,29 @@ 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 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) {
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) //nolint:govet
bindings := c.Kong.bindings.clone().add(binds...).add(c).merge(c.bindings)
return callAnyFunction(fv, bindings)
}
@@ -744,7 +808,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
@@ -757,6 +821,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(), false /* singleton */); err != nil {
return fmt.Errorf("%s.%s: %w", t.Name(), methodt.Name, err)
}
}
}
}
}
if method.IsValid() {
methods = append(methods, targetMethod{node, method, methodBinds})
@@ -765,11 +842,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
@@ -782,21 +854,27 @@ 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 {
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
}
}
return fmt.Errorf("no command selected")
if node == nil {
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.
@@ -811,23 +889,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())
}
@@ -837,6 +927,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
@@ -847,6 +942,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{}
@@ -883,7 +990,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.
@@ -943,7 +1050,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()))
}
}
@@ -957,6 +1064,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 errors.New(strings.Join(errs, ", "))
}
return nil
}
func checkXorDuplicates(paths []*Path) error {
for _, path := range paths {
seen := map[string]*Flag{}
@@ -975,7 +1096,39 @@ func checkXorDuplicates(paths []*Path) error {
return nil
}
func findPotentialCandidates(needle string, haystack []string, format string, args ...interface{}) error {
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 ...any) error {
if len(haystack) == 0 {
return fmt.Errorf(format, args...)
}
@@ -995,12 +1148,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() {
+1 -1
View File
@@ -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
+10 -1
View File
@@ -5,8 +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 {
if p.exitCode == 0 {
return exitNotOk
}
return p.exitCode
}
+32
View File
@@ -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
}
+1 -1
View File
@@ -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)
+2 -2
View File
@@ -1,10 +1,10 @@
module git.company.lan/gopkg/kong
require (
github.com/alecthomas/assert/v2 v2.6.0
github.com/alecthomas/assert/v2 v2.11.0
github.com/alecthomas/repr v0.4.0
)
require github.com/hexops/gotextdiff v1.0.3 // indirect
go 1.18
go 1.20
+2 -2
View File
@@ -1,5 +1,5 @@
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.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=
+1 -2
View File
@@ -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
+1 -2
View File
@@ -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
+21 -24
View File
@@ -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)
@@ -386,7 +388,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...))
}
@@ -415,7 +417,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 +472,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])
@@ -491,27 +493,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())
}
+24 -19
View File
@@ -51,7 +51,7 @@ func TestHelpOptionalArgs(t *testing.T) {
assert.NoError(t, err)
})
assert.True(t, exited)
expected := `Usage: test-app [<one> [<two>]] [flags]
expected := `Usage: test-app [<one> [<two>]]
Arguments:
[<one>] One optional arg.
@@ -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."`
@@ -82,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."`
}
@@ -118,6 +118,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 +160,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
@@ -189,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."`
}
@@ -293,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."`
}
@@ -320,7 +320,7 @@ func TestHelpTree(t *testing.T) {
assert.NoError(t, err)
})
assert.True(t, exited)
expected := `Usage: test-app <command> [flags]
expected := `Usage: test-app <command>
A test app.
@@ -353,7 +353,7 @@ Run "test-app <command> --help" for more information on a command.
assert.NoError(t, err)
})
assert.True(t, exited)
expected := `Usage: test-app one (un,uno) <command> [flags]
expected := `Usage: test-app one (un,uno) <command>
subcommand one
@@ -387,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."`
}
@@ -414,7 +413,7 @@ func TestHelpCompactNoExpand(t *testing.T) {
assert.NoError(t, err)
})
assert.True(t, exited)
expected := `Usage: test-app <command> [flags]
expected := `Usage: test-app <command>
A test app.
@@ -443,7 +442,7 @@ Run "test-app <command> --help" for more information on a command.
assert.NoError(t, err)
})
assert.True(t, exited)
expected := `Usage: test-app one (un,uno) <command> [flags]
expected := `Usage: test-app one (un,uno) <command>
subcommand one
@@ -600,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:", //nolint
Title: strings.Title(node.Name) + " flags:", //nolint:staticcheck // strings.Title in test is okay
}
}
return nil
@@ -787,16 +786,17 @@ 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{})
p.FatalIfErrorf(err)
expected := `Usage: test --flag=STRING [flags]
expected := `Usage: test --flag=STRING
Some description.
@@ -807,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) {
@@ -814,22 +815,24 @@ 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{})
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
`
assert.Equal(t, expected, w.String())
assert.Equal(t, 80, exitCode)
}
func TestCustomShortUsageOnError(t *testing.T) {
@@ -841,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(),
)
@@ -857,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)
}
+16 -3
View File
@@ -1,19 +1,32 @@
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.
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
}
+101 -50
View File
@@ -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 = "<anonymous struct>"
@@ -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)
@@ -56,27 +56,30 @@ 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
embedded []embedded
dynamicCommands []*dynamicCommand
hooks map[string][]reflect.Value
}
// New creates a new Kong parser on grammar.
//
// See the README (https://git.company.lan/gopkg/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,
@@ -84,6 +87,7 @@ func New(grammar interface{}, 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 {
@@ -117,7 +121,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
}
@@ -167,9 +171,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] }
@@ -216,19 +253,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)
}
}
@@ -237,6 +274,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 {
@@ -250,7 +292,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',
@@ -278,11 +320,11 @@ 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 {
return nil, err
if err != nil { // Trace is not expected to return an 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, &ParseError{error: err, Context: ctx}
@@ -299,11 +341,11 @@ func (k *Kong) Parse(args []string) (ctx *Context, err error) {
if err = k.applyHook(ctx, "BeforeApply"); err != nil {
return nil, &ParseError{error: err, Context: ctx}
}
if _, err = ctx.Apply(); err != nil {
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}
return nil, &ParseError{error: err, Context: ctx, exitCode: exitUsageError}
}
if err = k.applyHook(ctx, "AfterApply"); err != nil {
return nil, &ParseError{error: err, Context: ctx}
@@ -328,22 +370,30 @@ 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 k.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.
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 {
@@ -359,21 +409,19 @@ func (k *Kong) applyHookToDefaultFlags(ctx *Context, node *Node, name string) er
if !flag.HasDefault || ctx.values[flag.Value].IsValid() || !flag.Target.IsValid() {
continue
}
method := getMethod(flag.Target, name)
if !method.IsValid() {
continue
}
path := &Path{Flag: flag}
if err := callFunction(method, binds.clone().add(path)); err != nil {
return next(err)
for _, method := range getMethods(flag.Target, name) {
path := &Path{Flag: flag}
if err := callFunction(method, binds.clone().add(path)); err != nil {
return next(err)
}
}
}
return next(nil)
})
}
func formatMultilineMessage(w io.Writer, leaders []string, format string, args ...interface{}) {
lines := strings.Split(fmt.Sprintf(format, args...), "\n")
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 {
if l == "" {
@@ -388,25 +436,27 @@ 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{}) {
// 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.
func (k *Kong) FatalIfErrorf(err error, args ...interface{}) {
// 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
}
@@ -426,7 +476,8 @@ func (k *Kong) FatalIfErrorf(err error, args ...interface{}) {
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).
+727 -50
View File
@@ -4,6 +4,7 @@ import (
"bytes"
"errors"
"fmt"
"sort"
"strings"
"testing"
@@ -13,7 +14,7 @@ import (
"git.company.lan/gopkg/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"),
@@ -47,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 <id> <first> <last>
@@ -356,8 +376,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 {
@@ -367,34 +388,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 {
@@ -407,16 +458,47 @@ 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, "<anonymous struct>.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, "<anonymous struct>.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 {
@@ -506,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"`
@@ -593,11 +734,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("%s", test.text)
assert.Equal(t, test.want, w.String())
})
}
}
type cmdWithRun struct {
@@ -671,8 +825,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)
@@ -692,7 +846,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)
}
@@ -780,6 +936,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
@@ -850,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:"-"`
@@ -879,14 +1063,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\"")
}
@@ -906,6 +1088,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"`
@@ -923,6 +1120,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"`
@@ -939,6 +1153,57 @@ 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 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:""`
@@ -959,6 +1224,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:""`
@@ -978,6 +1263,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"`
@@ -1040,10 +1340,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)
@@ -1272,6 +1571,19 @@ func TestValidateArg(t *testing.T) {
assert.EqualError(t, err, "<arg>: 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
@@ -1297,6 +1609,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 +1622,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 +1638,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())
}
@@ -1460,7 +1788,7 @@ func TestOptionReturnsErr(t *testing.T) {
func TestEnumValidation(t *testing.T) {
tests := []struct {
name string
cli interface{}
cli any
fail bool
}{
{
@@ -1526,6 +1854,89 @@ 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"},
},
{
"DashDashBetweenArgs",
[]string{"foo", "--", "bar"},
"",
[]string{"foo", "--", "bar"},
},
{
"DashDash",
[]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 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
@@ -1651,7 +2062,7 @@ func TestVersionFlagShouldStillWork(t *testing.T) {
func TestSliceDecoderHelpfulErrorMsg(t *testing.T) {
tests := []struct {
name string
cli interface{}
cli any
args []string
err string
}{
@@ -1701,7 +2112,7 @@ func TestSliceDecoderHelpfulErrorMsg(t *testing.T) {
func TestMapDecoderHelpfulErrorMsg(t *testing.T) {
tests := []struct {
name string
cli interface{}
cli any
args []string
expected string
}{
@@ -2027,3 +2438,269 @@ 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"`)
}
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")
}
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)
}
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)
}
type EmbeddedCallback struct {
Nested NestedCallback `embed:""`
Embedded bool
}
func (e *EmbeddedCallback) AfterApply() error {
e.Embedded = true
return nil
}
type taggedEmbeddedCallback struct {
NestedCallback
Tagged bool
}
func (e *taggedEmbeddedCallback) AfterApply() error {
e.Tagged = true
return nil
}
type NestedCallback struct {
nested bool
}
func (n *NestedCallback) AfterApply() error {
n.nested = true
return nil
}
type EmbeddedRoot struct {
EmbeddedCallback
Tagged taggedEmbeddedCallback `embed:""`
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,
Nested: NestedCallback{
nested: true,
},
},
Tagged: taggedEmbeddedCallback{
Tagged: true,
NestedCallback: NestedCallback{
nested: true,
},
},
Root: true,
}
assert.Equal(t, expected, actual)
}
type applyCalledOnce struct {
Dev bool
}
func (c *applyCalledOnce) AfterApply() error {
c.Dev = false
return nil
}
func (c applyCalledOnce) Run() error {
if c.Dev {
return fmt.Errorf("--dev should not be set")
}
return nil
}
func TestApplyCalledOnce(t *testing.T) {
cli := &applyCalledOnce{}
kctx, err := mustNew(t, cli).Parse([]string{"--dev"})
assert.NoError(t, err)
err = kctx.Run()
assert.NoError(t, err)
}
func TestCustomTypeNoEllipsis(t *testing.T) {
type CLI struct {
Flag []byte `type:"existingfile"`
}
var cli CLI
p := mustNew(t, &cli, kong.Exit(func(int) {}))
w := &strings.Builder{}
p.Stderr = w
p.Stdout = w
_, err := p.Parse([]string{"--help"})
assert.NoError(t, err)
help := w.String()
assert.NotContains(t, help, "...")
}
func TestPrefixXorIssue343(t *testing.T) {
type DBConfig struct {
Password string `help:"Password" xor:"password" optional:""`
PasswordFile string `help:"File which content will be used for a password" xor:"password" optional:""`
PasswordCommand string `help:"Command to run to retrieve password" xor:"password" optional:""`
}
type SourceTargetConfig struct {
Source DBConfig `help:"Database config of source to be copied from" prefix:"source-" xorprefix:"source-" embed:""`
Target DBConfig `help:"Database config of source to be copied from" prefix:"target-" xorprefix:"target-" embed:""`
}
cli := SourceTargetConfig{}
kctx := mustNew(t, &cli)
_, err := kctx.Parse([]string{"--source-password=foo", "--target-password=bar"})
assert.NoError(t, err)
_, err = kctx.Parse([]string{"--source-password-file=foo", "--source-password=bar"})
assert.Error(t, err)
}
func TestIssue483EmptyRootNodeNoRun(t *testing.T) {
var emptyCLI struct{}
parser, err := kong.New(&emptyCLI)
assert.NoError(t, err)
kctx, err := parser.Parse([]string{})
assert.NoError(t, err)
err = kctx.Run()
assert.Error(t, err)
assert.Contains(t, err.Error(), "no command selected")
}
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)
}
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)
})
}
+11
View File
@@ -0,0 +1,11 @@
output:
- success
- failure
pre-push:
parallel: true
jobs:
- name: test
run: go test -v ./...
- name: lint
run: golangci-lint run
+3 -3
View File
@@ -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)
@@ -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
}
+8 -8
View File
@@ -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")
@@ -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)
}
@@ -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
+55 -4
View File
@@ -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()})
@@ -397,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) {
@@ -756,3 +779,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")
}
+25 -20
View File
@@ -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")
@@ -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
@@ -239,23 +242,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.
@@ -405,6 +409,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
@@ -431,7 +436,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 != "" {
@@ -444,7 +449,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
+20
View File
@@ -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())
}
+19
View File
@@ -0,0 +1,19 @@
package kong
// negatableDefault is a placeholder value for the Negatable tag to indicate
// the negated flag is --no-<flag-name>. 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
}
}
+78 -8
View File
@@ -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
@@ -79,7 +89,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,8 +97,12 @@ 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 <key>:"<value>".
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)
}
k.dynamicCommands = append(k.dynamicCommands, &dynamicCommand{
name: name,
help: help,
@@ -119,6 +133,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 {
@@ -152,7 +200,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
@@ -187,7 +235,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
@@ -197,20 +245,42 @@ 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
})
}
// BindToProvider allows binding of provider functions.
// BindToProvider binds an injected value to a provider function.
//
// The provider function must have one of the following signatures:
//
// 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 interface{}) Option {
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 */)
})
}
+62 -1
View File
@@ -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)
@@ -114,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
@@ -122,3 +164,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")
}
+7 -7
View File
@@ -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
@@ -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), "-"), "") //nolint:staticcheck // Unicode punctuation not an issue
return strings.ToLower(name[:1]) + name[1:]
}
+25 -47
View File
@@ -2,7 +2,6 @@ package kong_test
import (
"errors"
"os"
"reflect"
"strings"
"testing"
@@ -13,23 +12,13 @@ import (
type envMap map[string]string
func tempEnv(env envMap) func() {
for k, v := range env {
os.Setenv(k, v)
}
return func() {
for k := range env {
os.Unsetenv(k)
}
}
}
func newEnvParser(t *testing.T, cli interface{}, env envMap, options ...kong.Option) (*kong.Kong, func()) {
func newEnvParser(t *testing.T, cli any, env envMap, options ...kong.Option) *kong.Kong {
t.Helper()
restoreEnv := tempEnv(env)
for name, value := range env {
t.Setenv(name, value)
}
parser := mustNew(t, cli, options...)
return parser, restoreEnv
return parser
}
func TestEnvarsFlagBasic(t *testing.T) {
@@ -39,7 +28,7 @@ func TestEnvarsFlagBasic(t *testing.T) {
Interp string `env:"${kongInterp}"`
}
kongInterpEnv := "KONG_INTERP"
parser, unsetEnvs := newEnvParser(t, &cli,
parser := newEnvParser(t, &cli,
envMap{
"KONG_STRING": "bye",
"KONG_SLICE": "5,2,9",
@@ -49,7 +38,6 @@ func TestEnvarsFlagBasic(t *testing.T) {
"kongInterp": kongInterpEnv,
},
)
defer unsetEnvs()
_, err := parser.Parse([]string{})
assert.NoError(t, err)
@@ -63,14 +51,13 @@ func TestEnvarsFlagMultiple(t *testing.T) {
FirstENVPresent string `env:"KONG_TEST1_1,KONG_TEST1_2"`
SecondENVPresent string `env:"KONG_TEST2_1,KONG_TEST2_2"`
}
parser, unsetEnvs := newEnvParser(t, &cli,
parser := newEnvParser(t, &cli,
envMap{
"KONG_TEST1_1": "value1.1",
"KONG_TEST1_2": "value1.2",
"KONG_TEST2_2": "value2.2",
},
)
defer unsetEnvs()
_, err := parser.Parse([]string{})
assert.NoError(t, err)
@@ -82,8 +69,7 @@ func TestEnvarsFlagOverride(t *testing.T) {
var cli struct {
Flag string `env:"KONG_FLAG"`
}
parser, restoreEnv := newEnvParser(t, &cli, envMap{"KONG_FLAG": "bye"})
defer restoreEnv()
parser := newEnvParser(t, &cli, envMap{"KONG_FLAG": "bye"})
_, err := parser.Parse([]string{"--flag=hello"})
assert.NoError(t, err)
@@ -94,8 +80,7 @@ func TestEnvarsTag(t *testing.T) {
var cli struct {
Slice []int `env:"KONG_NUMBERS"`
}
parser, restoreEnv := newEnvParser(t, &cli, envMap{"KONG_NUMBERS": "5,2,9"})
defer restoreEnv()
parser := newEnvParser(t, &cli, envMap{"KONG_NUMBERS": "5,2,9"})
_, err := parser.Parse([]string{})
assert.NoError(t, err)
@@ -109,8 +94,7 @@ func TestEnvarsEnvPrefix(t *testing.T) {
var cli struct {
Anonymous `envprefix:"KONG_"`
}
parser, restoreEnv := newEnvParser(t, &cli, envMap{"KONG_NUMBERS": "1,2,3"})
defer restoreEnv()
parser := newEnvParser(t, &cli, envMap{"KONG_NUMBERS": "1,2,3"})
_, err := parser.Parse([]string{})
assert.NoError(t, err)
@@ -125,8 +109,7 @@ func TestEnvarsEnvPrefixMultiple(t *testing.T) {
var cli struct {
Anonymous `envprefix:"KONG_"`
}
parser, restoreEnv := newEnvParser(t, &cli, envMap{"KONG_NUMBERS1_1": "1,2,3", "KONG_NUMBERS2_2": "5,6,7"})
defer restoreEnv()
parser := newEnvParser(t, &cli, envMap{"KONG_NUMBERS1_1": "1,2,3", "KONG_NUMBERS2_2": "5,6,7"})
_, err := parser.Parse([]string{})
assert.NoError(t, err)
@@ -144,8 +127,7 @@ func TestEnvarsNestedEnvPrefix(t *testing.T) {
var cli struct {
Anonymous `envprefix:"KONG_"`
}
parser, restoreEnv := newEnvParser(t, &cli, envMap{"KONG_ANON_STRING": "abc"})
defer restoreEnv()
parser := newEnvParser(t, &cli, envMap{"KONG_ANON_STRING": "abc"})
_, err := parser.Parse([]string{})
assert.NoError(t, err)
@@ -156,15 +138,13 @@ func TestEnvarsWithDefault(t *testing.T) {
var cli struct {
Flag string `env:"KONG_FLAG" default:"default"`
}
parser, restoreEnv := newEnvParser(t, &cli, envMap{})
defer restoreEnv()
parser := newEnvParser(t, &cli, envMap{})
_, err := parser.Parse(nil)
assert.NoError(t, err)
assert.Equal(t, "default", cli.Flag)
parser, restoreEnv = newEnvParser(t, &cli, envMap{"KONG_FLAG": "moo"})
defer restoreEnv()
parser = newEnvParser(t, &cli, envMap{"KONG_FLAG": "moo"})
_, err = parser.Parse(nil)
assert.NoError(t, err)
assert.Equal(t, "moo", cli.Flag)
@@ -194,7 +174,7 @@ func TestEnv(t *testing.T) {
}
// With the prefix
parser, unsetEnvs := newEnvParser(t, &cli, envMap{
parser := newEnvParser(t, &cli, envMap{
"KONG_ONE_FLAG": "one",
"KONG_TWO_FLAG": "two",
"KONG_THREE_FLAG": "three",
@@ -202,14 +182,13 @@ func TestEnv(t *testing.T) {
"KONG_FIVE": "true",
"KONG_SIX": "true",
}, kong.DefaultEnvars("KONG"))
defer unsetEnvs()
_, err := parser.Parse(nil)
assert.NoError(t, err)
assert.Equal(t, expected, cli)
// Without the prefix
parser, unsetEnvs = newEnvParser(t, &cli, envMap{
parser = newEnvParser(t, &cli, envMap{
"ONE_FLAG": "one",
"TWO_FLAG": "two",
"THREE_FLAG": "three",
@@ -217,7 +196,6 @@ func TestEnv(t *testing.T) {
"FIVE": "true",
"SIX": "true",
}, kong.DefaultEnvars(""))
defer unsetEnvs()
_, err = parser.Parse(nil)
assert.NoError(t, err)
@@ -281,10 +259,10 @@ func TestResolversWithMappers(t *testing.T) {
Flag string `env:"KONG_MOO" type:"upper"`
}
restoreEnv := tempEnv(envMap{"KONG_MOO": "meow"})
defer restoreEnv()
t.Setenv("KONG_MOO", "meow")
parser := mustNew(t, &cli,
parser := newEnvParser(t, &cli,
envMap{"KONG_MOO": "meow"},
kong.NamedMapper("upper", testUppercaseMapper{}),
)
_, err := parser.Parse([]string{})
@@ -297,7 +275,7 @@ func TestResolverWithBool(t *testing.T) {
Bool bool
}
var resolver kong.ResolverFunc = func(context *kong.Context, parent *kong.Path, flag *kong.Flag) (interface{}, error) {
var resolver kong.ResolverFunc = func(context *kong.Context, parent *kong.Path, flag *kong.Flag) (any, error) {
if flag.Name == "bool" {
return true, nil
}
@@ -316,14 +294,14 @@ func TestLastResolverWins(t *testing.T) {
Int []int
}
var first kong.ResolverFunc = func(context *kong.Context, parent *kong.Path, flag *kong.Flag) (interface{}, error) {
var first kong.ResolverFunc = func(context *kong.Context, parent *kong.Path, flag *kong.Flag) (any, error) {
if flag.Name == "int" {
return 1, nil
}
return nil, nil
}
var second kong.ResolverFunc = func(context *kong.Context, parent *kong.Path, flag *kong.Flag) (interface{}, error) {
var second kong.ResolverFunc = func(context *kong.Context, parent *kong.Path, flag *kong.Flag) (any, error) {
if flag.Name == "int" {
return 2, nil
}
@@ -340,7 +318,7 @@ func TestResolverSatisfiesRequired(t *testing.T) {
var cli struct {
Int int `required`
}
var resolver kong.ResolverFunc = func(context *kong.Context, parent *kong.Path, flag *kong.Flag) (interface{}, error) {
var resolver kong.ResolverFunc = func(context *kong.Context, parent *kong.Path, flag *kong.Flag) (any, error) {
if flag.Name == "int" {
return 1, nil
}
@@ -358,7 +336,7 @@ func TestResolverTriggersHooks(t *testing.T) {
Flag hookValue
}
var first kong.ResolverFunc = func(context *kong.Context, parent *kong.Path, flag *kong.Flag) (interface{}, error) {
var first kong.ResolverFunc = func(context *kong.Context, parent *kong.Path, flag *kong.Flag) (any, error) {
if flag.Name == "flag" {
return "one", nil
}
@@ -377,7 +355,7 @@ type validatingResolver struct {
}
func (v *validatingResolver) Validate(app *kong.Application) error { return v.err }
func (v *validatingResolver) Resolve(context *kong.Context, parent *kong.Path, flag *kong.Flag) (interface{}, error) {
func (v *validatingResolver) Resolve(context *kong.Context, parent *kong.Path, flag *kong.Flag) (any, error) {
return nil, nil
}
+20 -6
View File
@@ -41,7 +41,7 @@ func (t TokenType) String() string {
// Token created by Scanner.
type Token struct {
Value interface{}
Value any
Type TokenType
}
@@ -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 <context> value but got <type>"
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
@@ -171,7 +180,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 <context> value but got <type>"
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
@@ -203,14 +212,19 @@ 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 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
}
+67 -32
View File
@@ -9,36 +9,51 @@ 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
Vars Vars
Prefix string // Optional prefix on anonymous structs. All sub-flags will have this prefix.
EnvPrefix string
Embed bool
Aliases []string
Negatable bool
Passthrough bool
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
XorPrefix string // Optional prefix on XOR/AND groups.
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
@@ -249,14 +264,23 @@ 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.XorPrefix = t.Get("xorprefix")
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-<flag>
}
t.Negatable = negatable
}
t.Negatable = negatable
aliases := t.Get("aliases")
if len(aliases) > 0 {
t.Aliases = append(t.Aliases, strings.FieldsFunc(aliases, tagSplitFn)...)
@@ -280,6 +304,17 @@ 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
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
}
+1 -2
View File
@@ -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()