⬆️ Update dependencies

This commit is contained in:
2026-11-01 15:12:52 +01:00
parent fd0d7ddee6
commit 312884dd59
501 changed files with 98424 additions and 35494 deletions
-1
View File
@@ -1 +0,0 @@
bin
+49 -6
View File
@@ -7,25 +7,56 @@ output:
linters:
enable-all: true
disable:
- maligned
- lll
- gochecknoglobals
- wsl
- funlen
- gocognit
- gomnd
- goprintffuncname
- paralleltest
- nlreturn
- testpackage
- wrapcheck
- forbidigo
- gci
- godot
- gofumpt
- cyclop
- errorlint
- nestif
- tagliatelle
- thelper
- godox
- goconst
- varnamelen
- ireturn
- exhaustruct
- nonamedreturns
- nilnil
- depguard # nothing to guard against yet
- tagalign # hurts readability of kong tags
- tenv # deprecated since v1.64, but not removed yet
- mnd
- perfsprint
- err113
- copyloopvar
- intrange
- 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
- sortslice
- unusedwrite
dupl:
threshold: 100
goconst:
min-len: 5
min-occurrences: 3
gocyclo:
min-complexity: 20
exhaustive:
default-signifies-exhaustive: true
issues:
max-per-linter: 0
@@ -40,3 +71,15 @@ 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.
# Parameter names are useful for documentation.
# Replacing them with '_' hides useful information.
- linters: [revive]
text: 'unused-parameter: parameter \S+ seems to be unused, consider removing or renaming it as _'
# Duplicate words are okay in tests.
- linters: [dupword]
path: _test\.go
+343 -104
View File
@@ -2,36 +2,49 @@
<p align="center"><img width="90%" src="kong.png" /></p>
# Kong is a command-line parser for Go
[![](https://godoc.org/github.com/alecthomas/kong?status.svg)](http://godoc.org/github.com/alecthomas/kong) [![CircleCI](https://img.shields.io/circleci/project/github/alecthomas/kong.svg)](https://circleci.com/gh/alecthomas/kong) [![Go Report Card](https://goreportcard.com/badge/github.com/alecthomas/kong)](https://goreportcard.com/report/github.com/alecthomas/kong) [![Slack chat](https://img.shields.io/static/v1?logo=slack&style=flat&label=slack&color=green&message=gophers)](https://gophers.slack.com/messages/CN9DS8YF3)
[TOC levels=2-3 numbered]: # "#### Table of Contents"
- [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()](#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)
- [Positional arguments](#positional-arguments)
- [Slices](#slices)
- [Maps](#maps)
- [Pointers](#pointers)
- [Nested data structure](#nested-data-structure)
- [Custom named decoders](#custom-named-decoders)
- [Supported field types](#supported-field-types)
- [Custom decoders (mappers)](#custom-decoders-mappers)
- [Supported tags](#supported-tags)
- [Plugins](#plugins)
- [Dynamic Commands](#dynamic-commands)
- [Variable interpolation](#variable-interpolation)
- [Validation](#validation)
- [Modifying Kong's behaviour](#modifying-kongs-behaviour)
- [`Name(help)` and `Description(help)` - set the application name description](#namehelp-and-descriptionhelp---set-the-application-name-description)
- [`Configuration(loader, paths...)` - load defaults from configuration files](#configurationloader-paths---load-defaults-from-configuration-files)
- [`Resolver(...)` - support for default values from external sources](#resolver---support-for-default-values-from-external-sources)
- [`*Mapper(...)` - customising how the command-line is mapped to Go values](#mapper---customising-how-the-command-line-is-mapped-to-go-values)
- [`ConfigureHelp(HelpOptions)` and `Help(HelpFunc)` - customising help](#configurehelphelpoptions-and-helphelpfunc---customising-help)
- [Injecting values into `Run()` methods](#injecting-values-into-run-methods)
- [Other options](#other-options)
#### Table of Contents
1. [Introduction](#introduction)
1. [Help](#help)
1. [Command handling](#command-handling)
1. [Switch on the command string](#switch-on-the-command-string)
1. [Attach a `Run(...) error` method to each command](#attach-a-run-error-method-to-each-command)
1. [Hooks: BeforeResolve(), BeforeApply(), AfterApply() and the Bind() option](#hooks-beforeresolve-beforeapply-afterapply-and-the-bind-option)
1. [Flags](#flags)
1. [Commands and sub-commands](#commands-and-sub-commands)
1. [Branching positional arguments](#branching-positional-arguments)
1. [Terminating positional arguments](#terminating-positional-arguments)
1. [Slices](#slices)
1. [Maps](#maps)
1. [Custom named decoders](#custom-named-decoders)
1. [Custom decoders (mappers)](#custom-decoders-mappers)
1. [Supported tags](#supported-tags)
1. [Variable interpolation](#variable-interpolation)
1. [Modifying Kong's behaviour](#modifying-kongs-behaviour)
1. [`Name(help)` and `Description(help)` - set the application name description](#namehelp-and-descriptionhelp---set-the-application-name-description)
1. [`Configuration(loader, paths...)` - load defaults from configuration files](#configurationloader-paths---load-defaults-from-configuration-files)
1. [`Resolver(...)` - support for default values from external sources](#resolver---support-for-default-values-from-external-sources)
1. [`*Mapper(...)` - customising how the command-line is mapped to Go values](#mapper---customising-how-the-command-line-is-mapped-to-go-values)
1. [`ConfigureHelp(HelpOptions)` and `Help(HelpFunc)` - customising help](#configurehelphelpoptions-and-helphelpfunc---customising-help)
1. [`Bind(...)` - bind values for callback hooks and Run() methods](#bind---bind-values-for-callback-hooks-and-run-methods)
1. [Other options](#other-options)
## 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
@@ -56,12 +69,12 @@ var CLI struct {
Force bool `help:"Force removal."`
Recursive bool `help:"Recursively remove files."`
Paths []string `arg name:"path" help:"Paths to remove." type:"path"`
} `cmd help:"Remove files."`
Paths []string `arg:"" name:"path" help:"Paths to remove." type:"path"`
} `cmd:"" help:"Remove files."`
Ls struct {
Paths []string `arg optional name:"path" help:"Paths to list." type:"path"`
} `cmd help:"List paths."`
Paths []string `arg:"" optional:"" name:"path" help:"Paths to list." type:"path"`
} `cmd:"" help:"List paths."`
}
func main() {
@@ -77,7 +90,9 @@ func main() {
## Help
Help is automatically generated. With no other arguments provided, help will display a full summary of all available commands.
### Help as a user of a Kong application
Every Kong application includes a `--help` flag that will display auto-generated help.
eg.
@@ -115,9 +130,73 @@ eg.
-f, --force Force removal.
-r, --recursive Recursively remove files.
For flags with associated environment variables, the variable `${env}` can be
interpolated into the help string. In the absence of this variable in the help,
### Defining help in Kong
Help is automatically generated from the command-line structure itself,
including `help:""` and other tags. [Variables](#variable-interpolation) will
also be interpolated into the help string.
Finally, any command, or argument type implementing the interface
`Help() string` will have this function called to retrieve more detail to
augment the help tag. This allows for much more descriptive text than can
fit in Go tags. [See \_examples/shell/help](./_examples/shell/help)
#### Showing the _command_'s detailed help
A command's additional help text is _not_ shown from top-level help, but can be displayed within contextual help:
**Top level help**
```bash
$ go run ./_examples/shell/help --help
Usage: help <command>
An app demonstrating HelpProviders
Flags:
-h, --help Show context-sensitive help.
--flag Regular flag help
Commands:
echo Regular command help
```
**Contextual**
```bash
$ go run ./_examples/shell/help echo --help
Usage: help echo <msg>
Regular command help
🚀 additional command help
Arguments:
<msg> Regular argument help
Flags:
-h, --help Show context-sensitive help.
--flag Regular flag help
```
#### Showing an _argument_'s detailed help
Custom help will only be shown for _positional arguments with named fields_ ([see the README section on positional arguments for more details on what that means](#branching-positional-arguments))
**Contextual argument help**
```bash
$ go run ./_examples/shell/help msg --help
Usage: help echo <msg>
Regular argument help
📣 additional argument help
Flags:
-h, --help Show context-sensitive help.
--flag Regular flag help
```
## Command handling
@@ -127,7 +206,7 @@ There are two ways to handle commands in Kong.
When you call `kong.Parse()` it will return a unique string representation of the command. Each command branch in the hierarchy will be a bare word and each branching argument or required positional argument will be the name surrounded by angle brackets. Here's an example:
There's an example of this pattern [here](https://github.com/alecthomas/kong/blob/master/_examples/shell/main.go).
There's an example of this pattern [here](https://github.com/alecthomas/kong/blob/master/_examples/shell/commandstring/main.go).
eg.
@@ -141,12 +220,12 @@ var CLI struct {
Force bool `help:"Force removal."`
Recursive bool `help:"Recursively remove files."`
Paths []string `arg name:"path" help:"Paths to remove." type:"path"`
} `cmd help:"Remove files."`
Paths []string `arg:"" name:"path" help:"Paths to remove." type:"path"`
} `cmd:"" help:"Remove files."`
Ls struct {
Paths []string `arg optional name:"path" help:"Paths to list." type:"path"`
} `cmd help:"List paths."`
Paths []string `arg:"" optional:"" name:"path" help:"Paths to list." type:"path"`
} `cmd:"" help:"List paths."`
}
func main() {
@@ -173,7 +252,7 @@ A more robust approach is to break each command out into their own structs:
Once a command node is selected by Kong it will search from that node back to the root. Each
encountered command node with a `Run(...) error` will be called in reverse order. This allows
sub-trees to be re-used fairly conveniently.
sub-trees to be reused fairly conveniently.
In addition to values bound with the `kong.Bind(...)` option, any values
passed through to `kong.Context.Run(...)` are also bindable to the target's
@@ -194,7 +273,7 @@ type RmCmd struct {
Force bool `help:"Force removal."`
Recursive bool `help:"Recursively remove files."`
Paths []string `arg name:"path" help:"Paths to remove." type:"path"`
Paths []string `arg:"" name:"path" help:"Paths to remove." type:"path"`
}
func (r *RmCmd) Run(ctx *Context) error {
@@ -203,7 +282,7 @@ func (r *RmCmd) Run(ctx *Context) error {
}
type LsCmd struct {
Paths []string `arg optional name:"path" help:"Paths to list." type:"path"`
Paths []string `arg:"" optional:"" name:"path" help:"Paths to list." type:"path"`
}
func (l *LsCmd) Run(ctx *Context) error {
@@ -214,8 +293,8 @@ func (l *LsCmd) Run(ctx *Context) error {
var cli struct {
Debug bool `help:"Enable debug mode."`
Rm RmCmd `cmd help:"Remove files."`
Ls LsCmd `cmd help:"List paths."`
Rm RmCmd `cmd:"" help:"Remove files."`
Ls LsCmd `cmd:"" help:"List paths."`
}
func main() {
@@ -227,13 +306,21 @@ func main() {
```
## Hooks: BeforeResolve(), BeforeApply(), AfterApply() and the Bind() option
## Hooks: BeforeReset(), BeforeResolve(), BeforeApply(), AfterApply(), AfterRun()
If a node in the grammar has a `BeforeResolve(...)`, `BeforeApply(...) error` and/or `AfterApply(...) error` method, those methods will be called before validation/assignment and after validation/assignment, respectively.
If a node in the CLI, or any of its embedded fields, implements a `BeforeReset(...) error`, `BeforeResolve
(...) error`, `BeforeApply(...) error`, `AfterApply(...) error`, and/or `AfterRun(...) error` method, those
will be called as Kong resets, resolves, validates, and assigns values to the node.
The `--help` flag is implemented with a `BeforeApply` hook.
| 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** |
| `AfterRun` | Invoked after `Run()` returns. Will not be called if `os.Exit()` is manually called. |
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()`.
The `--help` flag is implemented with a `BeforeReset` hook.
eg.
@@ -252,7 +339,7 @@ var cli struct {
func main() {
// Debug logger going to discard.
logger := log.New(ioutil.Discard, "", log.LstdFlags)
logger := log.New(io.Discard, "", log.LstdFlags)
ctx := kong.Parse(&cli, kong.Bind(logger))
@@ -260,9 +347,47 @@ func main() {
}
```
It's also possible to register these hooks with the functional options
`kong.WithBeforeReset`, `kong.WithBeforeResolve`, `kong.WithBeforeApply`, and
`kong.WithAfterApply`.
## The Bind() option
Arguments to hooks are provided via the `Run(...)` method or `Bind(...)` option. `*Kong`, `*Context`, `*Path` and parent commands are also bound and finally, hooks can also contribute bindings via `kong.Context.Bind()` and `kong.Context.BindTo()`.
eg:
```go
type CLI struct {
Debug bool `help:"Enable debug mode."`
Rm RmCmd `cmd:"" help:"Remove files."`
Ls LsCmd `cmd:"" help:"List paths."`
}
type AuthorName string
// ...
func (l *LsCmd) Run(cli *CLI) error {
// use cli.Debug here !!
return nil
}
func (r *RmCmd) Run(author AuthorName) error{
// use binded author here
return nil
}
func main() {
var cli CLI
ctx := kong.Parse(&cli, Bind(AuthorName("penguin")))
err := ctx.Run()
```
## Flags
Any [mapped](#mapper---customising-how-the-command-line-is-mapped-to-go-values) field in the command structure *not* tagged with `cmd` or `arg` will be a flag. Flags are optional by default.
Any [mapped](#mapper---customising-how-the-command-line-is-mapped-to-go-values) field in the command structure _not_ tagged with `cmd` or `arg` will be a flag. Flags are optional by default.
eg. The command-line `app [--flag="foo"]` can be represented by the following.
@@ -289,7 +414,7 @@ type CLI struct {
}
```
If a sub-command is tagged with `default:"1"` it will be selected if there are no further arguments.
If a sub-command is tagged with `default:"1"` it will be selected if there are no further arguments. If a sub-command is tagged with `default:"withargs"` it will be selected even if there are further arguments or flags and those arguments or flags are valid for the sub-command. This allows the user to omit the sub-command name on the CLI if its arguments/flags are not ambiguous with the sibling commands or flags.
## Branching positional arguments
@@ -318,11 +443,14 @@ var CLI struct {
This looks a little verbose in this contrived example, but typically this will not be the case.
## Terminating positional arguments
## Positional arguments
If a [mapped type](#mapper---customising-how-the-command-line-is-mapped-to-go-values) is tagged with `arg` it will be treated as the final positional values to be parsed on the command line.
If a field is tagged with `arg:""` it will be treated as the final positional
value to be parsed on the command line. By default positional arguments are
required, but specifying `optional:""` will alter this.
If a positional argument is a slice, all remaining arguments will be appended to that slice.
If a positional argument is a slice, all remaining arguments will be appended
to that slice.
## Slices
@@ -337,7 +465,7 @@ You would use the following:
```go
var CLI struct {
Ls struct {
Files []string `arg type:"existingfile"`
Files []string `arg:"" type:"existingfile"`
} `cmd`
}
```
@@ -356,7 +484,7 @@ You would use the following:
var CLI struct {
Config struct {
Set struct {
Config map[string]float64 `arg type:"file:"`
Config map[string]float64 `arg:"" type:"file:"`
} `cmd`
} `cmd`
}
@@ -364,29 +492,66 @@ var CLI struct {
For flags, multiple key+value pairs should be separated by `mapsep:"rune"` tag (defaults to `;`) eg. `--set="key1=value1;key2=value2"`.
## Pointers
Pointers work like the underlying type, except that you can differentiate between the presence of the zero value and no value being supplied.
For example:
```go
var CLI struct {
Foo *int
}
```
Would produce a nil value for `Foo` if no `--foo` argument is supplied, but would have a pointer to the value 0 if the argument `--foo=0` was supplied.
## Nested data structure
Kong support a nested data structure as well with `embed:""`. You can combine `embed:""` with `prefix:""`:
```go
var CLI struct {
Logging struct {
Level string `enum:"debug,info,warn,error" default:"info"`
Type string `enum:"json,console" default:"console"`
} `embed:"" prefix:"logging."`
}
```
This configures Kong to accept flags `--logging.level` and `--logging.type`.
## Custom named decoders
Kong includes a number of builtin custom type mappers. These can be used by
specifying the tag `type:"<type>"`. They are registered with the option
function `NamedMapper(name, mapper)`.
| Name | Description
|-------------------|---------------------------------------------------
| `path` | A path. ~ expansion is applied.
| `existingfile` | An existing file. ~ expansion is applied. `-` is accepted for stdin.
| `existingdir` | An existing directory. ~ expansion is applied.
| `counter` | Increment a numeric field. Useful for `-vvv`. Can accept `-s`, `--long` or `--long=N`.
| Name | Description |
| -------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `path` | A path. ~ expansion is applied. `-` is accepted for stdout, and will be passed unaltered. |
| `existingfile` | An existing file. ~ expansion is applied. `-` is accepted for stdin, and will be passed unaltered. |
| `existingdir` | An existing directory. ~ expansion is applied. |
| `counter` | Increment a numeric field. Useful for `-vvv`. Can accept `-s`, `--long` or `--long=N`. |
| `filecontent` | Read the file at path into the field. ~ expansion is applied. `-` is accepted for stdin, and will be passed unaltered. |
Slices and maps treat type tags specially. For slices, the `type:""` tag
specifies the element type. For maps, the tag has the format
`tag:"[<key>]:[<value>]"` where either may be omitted.
## Supported field types
## Custom decoders (mappers)
Any field implementing `encoding.TextUnmarshaler` or `json.Unmarshaler` will use those interfaces
for decoding values.
for decoding values. Kong also includes builtin support for many common Go types:
| Type | Description |
| --------------- | ----------------------------------------------------------------------------------------------------------- |
| `time.Duration` | Populated using `time.ParseDuration()`. |
| `time.Time` | Populated using `time.Parse()`. Format defaults to RFC3339 but can be overridden with the `format:"X"` tag. |
| `*os.File` | Path to a file that will be opened, or `-` for `os.Stdin`. File must be closed by the user. |
| `*url.URL` | Populated with `url.Parse()`. |
For more fine-grained control, if a field implements the
[MapperValue](https://godoc.org/github.com/alecthomas/kong#MapperValue)
@@ -397,40 +562,76 @@ interface it will be used to decode arguments into the field.
Tags can be in two forms:
1. Standard Go syntax, eg. `kong:"required,name='foo'"`.
2. Bare tags, eg. `required name:"foo"`
2. Bare tags, eg. `required:"" name:"foo"`
Both can coexist with standard Tag parsing.
Tag | Description
-----------------------| -------------------------------------------
`cmd` | If present, struct is a command.
`arg` | If present, field is an argument.
`env:"X"` | Specify envar to use for default value.
`name:"X"` | Long name, for overriding field name.
`help:"X"` | Help text.
`type:"X"` | Specify [named types](#custom-named-decoders) to use.
`placeholder:"X"` | Placeholder text.
`default:"X"` | Default value.
`default:"1"` | On a command, make it the default.
`short:"X"` | Short name, if flag.
`required` | If present, flag/arg is required.
`optional` | If present, flag/arg is optional.
`hidden` | If present, command or flag is hidden.
`format:"X"` | Format for parsing input, if supported.
`sep:"X"` | Separator for sequences (defaults to ","). May be `none` to disable splitting.
`mapsep:"X"` | Separator for maps (defaults to ";"). May be `none` to disable splitting.
`enum:"X,Y,..."` | Set of valid values allowed for this flag.
`group:"X"` | Logical group for a flag or command.
`xor:"X"` | Exclusive OR group for flags. Only one flag in the group can be used which is restricted within the same command.
`prefix:"X"` | Prefix for all sub-flags.
`set:"K=V"` | Set a variable for expansion by child elements. Multiples can occur.
`embed` | If present, this field's children will be embedded in the parent. Useful for composition.
`-` | Ignore the field. Useful for adding non-CLI fields to a configuration struct.
| Tag | Description |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `cmd:""` | If present, struct is a command. |
| `arg:""` | If present, field is an argument. Required by default. |
| `env:"X,Y,..."` | Specify envars to use for default value. The envs are resolved in the declared order. The first value found is used. |
| `name:"X"` | Long name, for overriding field name. |
| `help:"X"` | Help text. |
| `type:"X"` | Specify [named types](#custom-named-decoders) to use. |
| `placeholder:"X"` | Placeholder 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. `partial` will validate flags until the first positional argument is encountered, then pass through all remaining
positional arguments.
## Plugins
Kong CLI's can be extended by embedding the `kong.Plugin` type and populating it with pointers to Kong annotated structs. For example:
```go
var pluginOne struct {
PluginOneFlag string
}
var pluginTwo struct {
PluginTwoFlag string
}
var cli struct {
BaseFlag string
kong.Plugins
}
cli.Plugins = kong.Plugins{&pluginOne, &pluginTwo}
```
Additionally if an interface type is embedded, it can also be populated with a Kong annotated struct.
## Dynamic Commands
While plugins give complete control over extending command-line interfaces, Kong
also supports dynamically adding commands via `kong.DynamicCommand()`.
## Variable interpolation
Kong supports limited variable interpolation into help strings, enum lists and
default values.
Kong supports limited variable interpolation into help strings, placeholder strings,
enum lists and default values.
Variables are in the form:
@@ -450,6 +651,10 @@ are defined from the value itself:
${default}
${enum}
For flags with associated environment variables, the variable `${env}` can be
interpolated into the help string. In the absence of this variable in the
help string, Kong will append `($$${env})` to the help string.
eg.
```go
@@ -465,9 +670,29 @@ 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 either of the following interfaces:
```go
type Validatable interface {
Validate() error
}
```
```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).
@@ -489,7 +714,14 @@ eg.
kong.Parse(&cli, kong.Configuration(kong.JSON, "/etc/myapp.json", "~/.myapp.json"))
```
[See the tests](https://github.com/alecthomas/kong/blob/master/resolver_test.go#L103) for an example of how the JSON file is structured.
[See the tests](https://github.com/alecthomas/kong/blob/master/resolver_test.go#L206) for an example of how the JSON file is structured.
#### List of Configuration Loaders
- [YAML](https://github.com/alecthomas/kong-yaml)
- [HCL](https://github.com/alecthomas/kong-hcl)
- [TOML](https://github.com/alecthomas/kong-toml)
- [JSON](https://github.com/alecthomas/kong)
### `Resolver(...)` - support for default values from external sources
@@ -502,13 +734,14 @@ Example resolvers can be found in [resolver.go](https://github.com/alecthomas/ko
Command-line arguments are mapped to Go values via the Mapper interface:
```go
// A Mapper knows how to map command-line input to Go.
// A Mapper represents how a field is mapped from command-line values to Go.
//
// Mappers can be associated with concrete fields via pointer, reflect.Type, reflect.Kind, or via a "type" tag.
//
// Additionally, if a type implements the MapperValue interface, it will be used.
type Mapper interface {
// Decode scan into target.
//
// "ctx" contains context about the value being decoded that may be useful
// to some mapperss.
Decode(ctx *MapperContext, scan *Scanner, target reflect.Value) error
// Decode ctx.Value with ctx.Scanner into target.
Decode(ctx *DecodeContext, target reflect.Value) error
}
```
@@ -517,19 +750,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.
3. Use `HelpFormatter(HelpValueFormatter)` if you want to just customize the help text that is accompanied by flags and arguments.
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
+285 -74
View File
@@ -4,10 +4,15 @@ import (
"fmt"
"reflect"
"strings"
"unicode/utf8"
)
func build(k *Kong, ast interface{}) (app *Application, err error) {
defer catch(&err)
// Plugins are dynamically embedded command-line structures.
//
// Each element in the Plugins list *must* be a pointer to a structure.
type Plugins []any
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 {
@@ -20,10 +25,17 @@ func build(k *Kong, ast interface{}) (app *Application, err error) {
for _, flag := range extraFlags {
seenFlags[flag.Name] = true
}
node := buildNode(k, iv, ApplicationNode, seenFlags)
node, err := buildNode(k, iv, ApplicationNode, newEmptyTag(), seenFlags)
if err != nil {
return nil, err
}
if len(node.Positional) > 0 && len(node.Children) > 0 {
return nil, fmt.Errorf("can't mix positional arguments and branching arguments on %T", ast)
}
if provider, ok := v.Interface().(HelpProvider); ok {
node.Detail = provider.Help()
}
app.Node = node
app.Node.Flags = append(extraFlags, app.Node.Flags...)
app.Tag = newEmptyTag()
@@ -41,97 +53,216 @@ type flattenedField struct {
tag *Tag
}
func flattenedFields(v reflect.Value) (out []flattenedField) {
func flattenedFields(v reflect.Value, ptag *Tag) (out []flattenedField, err error) {
v = reflect.Indirect(v)
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)
tag := parseTag(fv, ft)
if tag.Ignored {
tag, err := parseTag(v, ft)
if err != nil {
return nil, err
}
if tag.Ignored || ignored[ft.Name] {
ignored[ft.Name] = true
continue
}
if ft.Anonymous || tag.Embed {
if fv.Kind() == reflect.Interface {
fv = fv.Elem()
// Assign group if it's not already set.
if tag.Group == "" {
tag.Group = ptag.Group
}
// Accumulate prefixes.
tag.Prefix = ptag.Prefix + tag.Prefix
tag.EnvPrefix = ptag.EnvPrefix + tag.EnvPrefix
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.
if (tag.Cmd || tag.Embed) && ft.Type.Kind() == reflect.Ptr {
fv = reflect.New(ft.Type.Elem()).Elem()
v.FieldByIndex(ft.Index).Set(fv.Addr())
}
if !ft.Anonymous && !tag.Embed {
if fv.CanSet() {
field := flattenedField{field: ft, value: fv, tag: tag}
out = append(out, field)
}
sub := flattenedFields(fv)
for _, subf := range sub {
// Assign parent if it's not already set.
if subf.tag.Group == "" {
subf.tag.Group = tag.Group
continue
}
// Embedded type.
if fv.Kind() == reflect.Interface {
fv = fv.Elem()
} else if fv.Type() == reflect.TypeOf(Plugins{}) {
for i := 0; i < fv.Len(); i++ {
fields, ferr := flattenedFields(fv.Index(i).Elem(), tag)
if ferr != nil {
return nil, ferr
}
// Accumulate prefixes.
subf.tag.Prefix = tag.Prefix + subf.tag.Prefix
// Combine parent vars.
subf.tag.Vars = tag.Vars.CloneWith(subf.tag.Vars)
out = append(out, fields...)
}
out = append(out, sub...)
continue
}
if !fv.CanSet() {
continue
sub, err := flattenedFields(fv, tag)
if err != nil {
return nil, err
}
out = append(out, flattenedField{field: ft, value: fv, tag: tag})
out = append(out, sub...)
}
return out
out = removeIgnored(out, ignored)
return out, nil
}
func buildNode(k *Kong, v reflect.Value, typ NodeType, seenFlags map[string]bool) *Node {
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) { //nolint:gocyclo
node := &Node{
Type: typ,
Target: v,
Tag: newEmptyTag(),
Tag: tag,
}
for _, field := range flattenedFields(v) {
fields, err := flattenedFields(v, tag)
if err != nil {
return nil, err
}
MAIN:
for _, field := range fields {
for _, r := range k.ignoreFields {
if r.MatchString(v.Type().Name() + "." + field.field.Name) {
continue MAIN
}
}
ft := field.field
fv := field.value
tag := field.tag
name := tag.Name
if name == "" {
name = tag.Prefix + strings.ToLower(dashedString(ft.Name))
name = tag.Prefix + k.flagNamer(ft.Name)
} else {
name = tag.Prefix + name
}
if len(tag.Envs) != 0 {
for i := range tag.Envs {
tag.Envs[i] = tag.EnvPrefix + tag.Envs[i]
}
}
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 ft.Type.Kind() == reflect.Struct && (tag.Cmd || tag.Arg) && k.registry.ForValue(fv) == nil {
if field.value.Kind() == reflect.Struct && (tag.Cmd || tag.Arg) && k.registry.ForValue(fv) == nil {
typ := CommandNode
if tag.Arg {
typ = ArgumentNode
}
buildChild(k, node, typ, v, ft, fv, tag, name, seenFlags)
err = buildChild(k, node, typ, v, ft, fv, tag, name, seenFlags)
} else {
buildField(k, node, v, ft, fv, tag, name, seenFlags)
err = buildField(k, node, v, ft, fv, tag, name, seenFlags)
}
if err != nil {
return nil, err
}
}
// Validate if there are no duplicate names
if err := checkDuplicateNames(node, v); err != nil {
return nil, err
}
// "Unsee" flags.
for _, flag := range node.Flags {
delete(seenFlags, flag.Name)
}
// Scan through argument positionals to ensure optional is never before a required.
last := true
for i, p := range node.Positional {
if !last && p.Required {
fail("argument %q can not be required after an optional", p.Name)
delete(seenFlags, "--"+flag.Name)
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 {
if utf8.RuneCountInString(aflag) == 1 {
delete(seenFlags, "-"+aflag)
} else {
delete(seenFlags, "--"+aflag)
}
}
last = p.Required
p.Position = i
}
return node
if err := validatePositionalArguments(node); err != nil {
return nil, err
}
return node, nil
}
func buildChild(k *Kong, node *Node, typ NodeType, v reflect.Value, ft reflect.StructField, fv reflect.Value, tag *Tag, name string, seenFlags map[string]bool) {
child := buildNode(k, fv, typ, seenFlags)
func validatePositionalArguments(node *Node) error {
var last *Value
for i, curr := range node.Positional {
if last != nil {
// Scan through argument positionals to ensure optional is never before a required.
if !last.Required && curr.Required {
return fmt.Errorf("%s: required %q cannot come after optional %q", node.FullPath(), curr.Name, last.Name)
}
// Cumulative argument needs to be last.
if last.IsCumulative() {
return fmt.Errorf("%s: argument %q cannot come after cumulative %q", node.FullPath(), curr.Name, last.Name)
}
}
last = curr
curr.Position = i
}
return nil
}
func buildChild(k *Kong, node *Node, typ NodeType, v reflect.Value, ft reflect.StructField, fv reflect.Value, tag *Tag, name string, seenFlags map[string]bool) error {
child, err := buildNode(k, fv, typ, newEmptyTag(), seenFlags)
if err != nil {
return err
}
child.Name = name
child.Tag = tag
child.Parent = node
child.Help = tag.Help
child.Hidden = tag.Hidden
child.Group = tag.Group
child.Group = buildGroupForKey(k, tag.Group)
child.Aliases = tag.Aliases
if provider, ok := fv.Addr().Interface().(HelpProvider); ok {
child.Detail = provider.Help()
@@ -141,48 +272,68 @@ func buildChild(k *Kong, node *Node, typ NodeType, v reflect.Value, ft reflect.S
// a positional argument is provided to the child, and move it to the branching argument field.
if tag.Arg {
if len(child.Positional) == 0 {
fail("positional branch %s.%s must have at least one child positional argument named %q",
v.Type().Name(), ft.Name, name)
return failField(v, ft, "positional branch must have at least one child positional argument named %q", name)
}
if child.Positional[0].Name != name {
return failField(v, ft, "first field in positional branch must have the same name as the parent field (%s).", child.Name)
}
value := child.Positional[0]
child.Argument = child.Positional[0]
child.Positional = child.Positional[1:]
if child.Help == "" {
child.Help = value.Help
child.Help = child.Argument.Help
}
child.Name = value.Name
if child.Name != name {
fail("first field in positional branch %s.%s must have the same name as the parent field (%s).",
v.Type().Name(), ft.Name, child.Name)
}
child.Argument = value
} else {
child.Name = name
if tag.HasDefault {
if node.DefaultCmd != nil {
return failField(v, ft, "can't have more than one default command under %s", node.Summary())
}
if tag.Default != "withargs" && (len(child.Children) > 0 || len(child.Positional) > 0) {
return failField(v, ft, "default command %s must not have subcommands or arguments", child.Summary())
}
node.DefaultCmd = child
}
if tag.Passthrough {
if len(child.Children) > 0 || len(child.Flags) > 0 {
return failField(v, ft, "passthrough command %s must not have subcommands or flags", child.Summary())
}
if len(child.Positional) != 1 {
return failField(v, ft, "passthrough command %s must contain exactly one positional argument", child.Summary())
}
if !checkPassthroughArg(child.Positional[0].Target) {
return failField(v, ft, "passthrough command %s must contain exactly one positional argument of []string type", child.Summary())
}
child.Passthrough = true
}
}
node.Children = append(node.Children, child)
if len(child.Positional) > 0 && len(child.Children) > 0 {
fail("can't mix positional arguments and branching arguments on %s.%s", v.Type().Name(), ft.Name)
return failField(v, ft, "can't mix positional arguments and branching arguments")
}
return nil
}
func buildField(k *Kong, node *Node, v reflect.Value, ft reflect.StructField, fv reflect.Value, tag *Tag, name string, seenFlags map[string]bool) {
func buildField(k *Kong, node *Node, v reflect.Value, ft reflect.StructField, fv reflect.Value, tag *Tag, name string, seenFlags map[string]bool) error {
mapper := k.registry.ForNamedValue(tag.Type, fv)
if mapper == nil {
fail("unsupported field type %s.%s (of type %s)", v.Type(), ft.Name, ft.Type)
return failField(v, ft, "unsupported field type %s, perhaps missing a cmd:\"\" tag?", ft.Type)
}
value := &Value{
Name: name,
Help: tag.Help,
Default: tag.Default,
DefaultValue: reflect.New(fv.Type()).Elem(),
Mapper: mapper,
Tag: tag,
Target: fv,
Enum: tag.Enum,
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),
@@ -192,20 +343,80 @@ func buildField(k *Kong, node *Node, v reflect.Value, ft reflect.StructField, fv
if tag.Arg {
node.Positional = append(node.Positional, value)
} else {
if seenFlags[value.Name] {
fail("duplicate flag --%s", value.Name)
if seenFlags["--"+value.Name] {
return failField(v, ft, "duplicate flag --%s", value.Name)
}
seenFlags["--"+value.Name] = true
for _, alias := range tag.Aliases {
aliasFlag := "--" + alias
if utf8.RuneCountInString(alias) == 1 {
aliasFlag = "-" + alias
}
if seenFlags[aliasFlag] {
return failField(v, ft, "duplicate flag %s", aliasFlag)
}
seenFlags[aliasFlag] = true
}
if tag.Short != 0 {
if seenFlags["-"+string(tag.Short)] {
return failField(v, ft, "duplicate short flag -%c", tag.Short)
}
seenFlags["-"+string(tag.Short)] = true
}
if tag.Negatable != "" {
negFlag := negatableFlagName(value.Name, tag.Negatable)
if seenFlags[negFlag] {
return failField(v, ft, "duplicate negation flag %s", negFlag)
}
seenFlags[negFlag] = true
}
seenFlags[value.Name] = true
flag := &Flag{
Value: value,
Aliases: tag.Aliases,
Short: tag.Short,
PlaceHolder: tag.PlaceHolder,
Env: tag.Env,
Group: tag.Group,
Envs: tag.Envs,
Group: buildGroupForKey(k, tag.Group),
Xor: tag.Xor,
And: tag.And,
Hidden: tag.Hidden,
}
value.Flag = flag
node.Flags = append(node.Flags, flag)
}
return nil
}
func buildGroupForKey(k *Kong, key string) *Group {
if key == "" {
return nil
}
for _, group := range k.groups {
if group.Key == key {
return &group
}
}
// No group provided with kong.ExplicitGroups. We create one ad-hoc for this key.
return &Group{
Key: key,
Title: key,
}
}
func checkDuplicateNames(node *Node, v reflect.Value) error {
seenNames := make(map[string]struct{})
for _, node := range node.Children {
if _, ok := seenNames[node.Name]; ok {
name := v.Type().Name()
if name == "" {
name = "<anonymous struct>"
}
return fmt.Errorf("duplicate command name %q in command %q", node.Name, name)
}
seenNames[node.Name] = struct{}{}
}
return nil
}
+208 -22
View File
@@ -3,10 +3,63 @@ package kong
import (
"fmt"
"reflect"
"runtime"
"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,14 +69,39 @@ 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 any) {
val := reflect.ValueOf(impl)
b[reflect.TypeOf(iface).Elem()] = newValueBinding(val)
}
func (b bindings) addProvider(provider any, singleton bool) error {
pv := reflect.ValueOf(provider)
t := pv.Type()
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] = newFunctionBinding(pv, singleton)
return nil
}
// Clone and add values.
func (b bindings) clone() bindings {
out := make(bindings, len(b))
@@ -50,27 +128,135 @@ func getMethod(value reflect.Value, name string) reflect.Value {
return method
}
func callMethod(name string, v, f reflect.Value, bindings bindings) error {
in := []reflect.Value{}
t := f.Type()
if t.NumOut() != 1 || t.Out(0) != callbackReturnSignature {
return fmt.Errorf("return value of %T.%s() must be exactly \"error\"", v.Type(), name)
func getExplicitMethod(value reflect.Value, name string) reflect.Value {
if isExplicitMethod(value.Type(), name) {
return value.MethodByName(name)
}
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.%s(), use kong.Bind(%s)", pt, i, v.Type(), name, pt)
if value.CanAddr() && isExplicitMethod(value.Addr().Type(), name) {
return value.Addr().MethodByName(name)
}
return reflect.Value{}
}
func isExplicitMethod(t reflect.Type, name string) bool {
method, ok := t.MethodByName(name)
if !ok {
return false
}
// Promoted embedded methods are compiler-generated wrappers. The embedded
// value itself is visited separately, so skip those wrappers here.
fn := runtime.FuncForPC(method.Func.Pointer())
if fn == nil {
return true
}
file, _ := fn.FileLine(method.Func.Pointer())
return file != "<autogenerated>"
}
// 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) {
walkEmbedded(value, func(v reflect.Value) {
if method := getExplicitMethod(v, name); method.IsValid() {
methods = append(methods, method)
}
})
return
}
// walkEmbedded calls visit on v and recursively on every exported field
// of v that is either a standard Go anonymous field or tagged `embed:""`.
// Pointer values are dereferenced before traversal; nil/invalid pointers
// are skipped. [Plugins] are descended into element-by-element, matching how
// [flattenedFields] treats them at build time.
func walkEmbedded(value reflect.Value, visit func(reflect.Value)) {
if value.Kind() == reflect.Pointer {
value = value.Elem()
}
out := f.Call(in)
if out[0].IsNil() {
if !value.IsValid() {
return
}
visit(value)
if value.Type() == reflect.TypeOf(Plugins{}) {
for i := 0; i < value.Len(); i++ {
walkEmbedded(value.Index(i).Elem(), visit)
}
return
}
if value.Kind() != reflect.Struct {
return
}
t := value.Type()
for i := 0; i < value.NumField(); i++ {
field := t.Field(i)
if !field.IsExported() {
continue
}
_, isEmbedded := field.Tag.Lookup("embed")
if !isEmbedded && !field.Anonymous {
continue
}
walkEmbedded(value.Field(i), visit)
}
}
func callFunction(f reflect.Value, bindings bindings) error {
if f.Kind() != reflect.Func {
return fmt.Errorf("expected function, got %s", f.Type())
}
t := f.Type()
if t.NumOut() != 1 || !t.Out(0).Implements(callbackReturnSignature) {
return fmt.Errorf("return value of %s must implement \"error\"", t)
}
out, err := callAnyFunction(f, bindings)
if err != nil {
return err
}
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)
return ferr.(error) //nolint:forcetypeassert
}
func callAnyFunction(f reflect.Value, bindings bindings) (out []any, err error) {
if f.Kind() != reflect.Func {
return nil, fmt.Errorf("expected function, got %s", f.Type())
}
in := []reflect.Value{}
t := f.Type()
for i := 0; i < t.NumIn(); i++ {
pt := t.In(i)
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))
for i, v := range outv {
out[i] = v.Interface()
}
return out, nil
}
+24 -24
View File
@@ -13,37 +13,37 @@ import (
//
// Examples
//
// "" => [""]
// "lowercase" => ["lowercase"]
// "Class" => ["Class"]
// "MyClass" => ["My", "Class"]
// "MyC" => ["My", "C"]
// "HTML" => ["HTML"]
// "PDFLoader" => ["PDF", "Loader"]
// "AString" => ["A", "String"]
// "SimpleXMLParser" => ["Simple", "XML", "Parser"]
// "vimRPCPlugin" => ["vim", "RPC", "Plugin"]
// "GL11Version" => ["GL", "11", "Version"]
// "99Bottles" => ["99", "Bottles"]
// "May5" => ["May", "5"]
// "BFG9000" => ["BFG", "9000"]
// "BöseÜberraschung" => ["Böse", "Überraschung"]
// "Two spaces" => ["Two", " ", "spaces"]
// "BadUTF8\xe2\xe2\xa1" => ["BadUTF8\xe2\xe2\xa1"]
// "" => [""]
// "lowercase" => ["lowercase"]
// "Class" => ["Class"]
// "MyClass" => ["My", "Class"]
// "MyC" => ["My", "C"]
// "HTML" => ["HTML"]
// "PDFLoader" => ["PDF", "Loader"]
// "AString" => ["A", "String"]
// "SimpleXMLParser" => ["Simple", "XML", "Parser"]
// "vimRPCPlugin" => ["vim", "RPC", "Plugin"]
// "GL11Version" => ["GL", "11", "Version"]
// "99Bottles" => ["99", "Bottles"]
// "May5" => ["May", "5"]
// "BFG9000" => ["BFG", "9000"]
// "BöseÜberraschung" => ["Böse", "Überraschung"]
// "Two spaces" => ["Two", " ", "spaces"]
// "BadUTF8\xe2\xe2\xa1" => ["BadUTF8\xe2\xe2\xa1"]
//
// Splitting rules
//
// 1) If string is not valid UTF-8, return it without splitting as
// 1. If string is not valid UTF-8, return it without splitting as
// single item array.
// 2) Assign all unicode characters into one of 4 sets: lower case
// 2. Assign all unicode characters into one of 4 sets: lower case
// letters, upper case letters, numbers, and all other characters.
// 3) Iterate through characters of string, introducing splits
// 3. Iterate through characters of string, introducing splits
// between adjacent characters that belong to different sets.
// 4) Iterate through array of split strings, and if a given string
// 4. Iterate through array of split strings, and if a given string
// is upper case:
// if subsequent string is lower case:
// move last character of upper case string to beginning of
// lower case string
// if subsequent string is lower case:
// move last character of upper case string to beginning of
// lower case string
func camelCase(src string) (entries []string) {
// don't split invalid utf8
if !utf8.ValidString(src) {
+552 -116
View File
@@ -1,13 +1,14 @@
package kong
import (
"errors"
"fmt"
"os"
"reflect"
"sort"
"strconv"
"strings"
"github.com/pkg/errors"
"unicode/utf8"
)
// Path records the nodes and parsed values from the current command-line.
@@ -26,6 +27,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.
@@ -43,6 +47,36 @@ func (p *Path) Node() *Node {
return nil
}
// Visitable returns the Visitable for this path element.
func (p *Path) Visitable() Visitable {
switch {
case p.App != nil:
return p.App
case p.Argument != nil:
return p.Argument
case p.Command != nil:
return p.Command
case p.Flag != nil:
return p.Flag
case p.Positional != nil:
return p.Positional
}
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
@@ -66,14 +100,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)
@@ -81,7 +116,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...)
}
@@ -89,10 +124,29 @@ 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{}) {
valueOf := reflect.ValueOf(impl)
c.bindings[reflect.TypeOf(iface).Elem()] = func() (reflect.Value, error) { return valueOf, nil }
// BindTo(impl, (*MyInterface)(nil))
func (c *Context) BindTo(impl, iface any) {
c.bindings.addTo(impl, iface)
}
// BindToProvider allows binding of provider functions.
//
// This is useful when the Run() function of different commands require different values that may
// not all be initialisable from the main() function.
//
// "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.
@@ -133,19 +187,53 @@ func (c *Context) Empty() bool {
}
// Validate the current context.
func (c *Context) Validate() error { // nolint: gocyclo
err := Visit(c.Model, func(node Visitable, next Next) error {
if value, ok := node.(*Value); ok {
if value.Enum != "" && (!value.Required || value.Default != "") {
func (c *Context) Validate() error { //nolint: gocyclo
// Only check nodes on the selected command path: an envar shared with
// another command may hold a value that is invalid there.
for _, path := range c.Path {
node := path.Node()
if node == nil {
continue
}
for _, value := range node.Values() {
ok := atLeastOneEnvSet(value.Tag.Envs)
if value.Enum != "" && (!value.Required || value.HasDefault || (len(value.Tag.Envs) != 0 && ok)) {
if err := checkEnum(value, value.Target); err != nil {
return err
}
}
}
return next(nil)
})
if err != nil {
return err
}
for _, el := range c.Path {
var (
value reflect.Value
desc string
)
switch node := el.Visitable().(type) {
case *Value:
value = node.Target
desc = node.ShortSummary()
case *Flag:
value = node.Target
desc = node.ShortSummary()
case *Application:
value = node.Target
desc = ""
case *Node:
value = node.Target
desc = node.Path()
}
for _, validate := range getValidators(value) {
if err := validate.Validate(c); err != nil {
if desc != "" {
return fmt.Errorf("%s: %w", desc, err)
}
return err
}
}
}
for _, resolver := range c.combineResolvers() {
if err := resolver.Validate(c.Model); err != nil {
@@ -190,7 +278,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
}
@@ -237,7 +325,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]
@@ -255,22 +343,73 @@ func (c *Context) FlagValue(flag *Flag) interface{} {
// Reset recursively resets values to defaults (as specified in the grammar) or the zero value.
func (c *Context) Reset() error {
selected := c.selectedValues()
return Visit(c.Model.Node, func(node Visitable, next Next) error {
if value, ok := node.(*Value); ok {
return next(value.Reset())
value, ok := node.(*Value)
if !ok {
return next(nil)
}
return next(nil)
err := value.Reset()
if err != nil && !selected[value] {
// An envar shared with a node outside the selected command path
// may not parse there; that must not fail this parse.
value.Target.Set(reflect.Zero(value.Target.Type()))
err = nil
}
return next(err)
})
}
func (c *Context) trace(node *Node) (err error) { // nolint: gocyclo
// selectedValues returns the set of values attached to nodes on the traced
// command path.
func (c *Context) selectedValues() map[*Value]bool {
selected := map[*Value]bool{}
for _, path := range c.Path {
if node := path.Node(); node != nil {
for _, value := range node.Values() {
selected[value] = true
}
}
}
return selected
}
func (c *Context) endParsing() {
args := []string{}
for {
token := c.scan.Pop()
if token.Type == EOLToken {
break
}
args = append(args, token.String())
}
// Note: tokens must be pushed in reverse order.
for i := range args {
c.scan.PushTyped(args[len(args)-1-i], PositionalArgumentToken)
}
}
//nolint:maintidx
func (c *Context) trace(node *Node) (err error) { //nolint: gocyclo
positional := 0
node.Active = true
flags := []*Flag{}
for _, group := range node.AllFlags(false) {
flagNode := node
if node.DefaultCmd != nil && node.DefaultCmd.Tag.Default == "withargs" {
// Add flags of the default command if the current node has one
// and that default command allows args / flags without explicitly
// naming the command on the CLI.
flagNode = node.DefaultCmd
}
for _, group := range flagNode.AllFlags(false) {
flags = append(flags, group...)
}
if node.Passthrough {
c.endParsing()
}
for !c.scan.Peek().IsEOL() {
token := c.scan.Peek()
switch token.Type {
@@ -281,24 +420,17 @@ func (c *Context) trace(node *Node) (err error) { // nolint: gocyclo
switch {
case v == "-":
fallthrough
default: // nolint
default: //nolint
c.scan.Pop()
c.scan.PushTyped(token.Value, PositionalArgumentToken)
// Indicates end of parsing. All remaining arguments are treated as positional arguments only.
case v == "--":
c.scan.Pop()
args := []string{}
for {
token = c.scan.Pop()
if token.Type == EOLToken {
break
}
args = append(args, token.String())
}
// Note: tokens must be pushed in reverse order.
for i := range args {
c.scan.PushTyped(args[len(args)-1-i], PositionalArgumentToken)
c.endParsing()
// 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.
@@ -335,12 +467,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:
@@ -352,6 +494,12 @@ func (c *Context) trace(node *Node) (err error) { // nolint: gocyclo
// Ensure we've consumed all positional arguments.
if positional < len(node.Positional) {
arg := node.Positional[positional]
if arg.Passthrough {
c.endParsing()
}
arg.Active = true
err := arg.Parse(c.scan, c.getValue(arg))
if err != nil {
return err
@@ -359,11 +507,30 @@ 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
}
// Assign token value to a branch name if tagged as an alias
// An alias will be ignored in the case of an existing command
cmds := make(map[string]bool)
for _, branch := range node.Children {
if branch.Type == CommandNode {
cmds[branch.Name] = true
}
}
for _, branch := range node.Children {
for _, a := range branch.Aliases {
_, ok := cmds[a]
if token.Value == a && !ok {
token.Value = branch.Name
break
}
}
}
// After positional arguments have been consumed, check commands next...
for _, branch := range node.Children {
if branch.Type == CommandNode && !branch.Hidden {
@@ -372,9 +539,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)
}
@@ -386,15 +554,28 @@ 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)
}
}
}
// If there is a default command that allows args and nothing else
// matches, take the branch of the default command
if node.DefaultCmd != nil && node.DefaultCmd.Tag.Default == "withargs" {
c.Path = append(c.Path, &Path{
Parent: node,
Command: node.DefaultCmd,
Flags: node.DefaultCmd.Flags,
remainder: c.scan.PeekAll(),
})
return c.trace(node.DefaultCmd)
}
return findPotentialCandidates(token.String(), candidates, "unexpected argument %s", token)
default:
return fmt.Errorf("unexpected token %s", token)
@@ -403,29 +584,26 @@ 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
}
}
var defaultNode *Path
for _, child := range node.Children {
if child.Type == CommandNode && child.Tag.Default != "" {
if defaultNode != nil {
return fmt.Errorf("can't have more than one default command under %s", node.Summary())
}
defaultNode = &Path{
Parent: child,
Command: child,
Flags: child.Flags,
}
}
}
if defaultNode != nil {
c.Path = append(c.Path, defaultNode)
if node.DefaultCmd != nil {
c.Path = append(c.Path, &Path{
Parent: node.DefaultCmd,
Command: node.DefaultCmd,
Flags: node.DefaultCmd.Flags,
remainder: c.scan.PeekAll(),
})
}
return nil
}
@@ -446,11 +624,11 @@ 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 {
return errors.Wrap(err, flag.ShortSummary())
return fmt.Errorf("%s: %w", flag.ShortSummary(), err)
}
if s == nil {
continue
@@ -469,12 +647,13 @@ func (c *Context) Resolve() error {
return err
}
inserted = append(inserted, &Path{
Flag: flag,
Resolved: true,
Flag: flag,
Resolved: true,
remainder: c.scan.PeekAll(),
})
}
}
c.Path = append(inserted, c.Path...)
c.Path = append(c.Path, inserted...)
return nil
}
@@ -490,6 +669,15 @@ func (c *Context) getValue(value *Value) reflect.Value {
v, ok := c.values[value]
if !ok {
v = reflect.New(value.Target.Type()).Elem()
switch v.Kind() {
case reflect.Ptr:
v.Set(reflect.New(v.Type().Elem()))
case reflect.Slice:
v.Set(reflect.MakeSlice(v.Type(), 0, 0))
case reflect.Map:
v.Set(reflect.MakeMap(v.Type()))
default:
}
c.values[value] = v
}
return v
@@ -546,32 +734,92 @@ func (c *Context) Apply() (string, error) {
return strings.Join(path, " "), nil
}
func flipBoolValue(value reflect.Value) error {
if value.Kind() == reflect.Bool {
value.SetBool(!value.Bool())
return nil
}
if value.Kind() == reflect.Ptr {
if !value.IsNil() {
return flipBoolValue(value.Elem())
}
return nil
}
return fmt.Errorf("cannot negate a value of %s", value.Type().String())
}
func (c *Context) parseFlag(flags []*Flag, match string) (err error) {
defer catch(&err)
candidates := []string{}
for _, flag := range flags {
long := "--" + flag.Name
short := "-" + string(flag.Short)
matched := long == match
candidates = append(candidates, long)
if flag.Short != 0 {
short := "-" + string(flag.Short)
matched = matched || (short == match)
candidates = append(candidates, short)
}
if short != match && long != match {
for _, alias := range flag.Aliases {
aliasFlag := "--" + alias
if utf8.RuneCountInString(alias) == 1 {
aliasFlag = "-" + alias
}
matched = matched || (aliasFlag == match)
candidates = append(candidates, aliasFlag)
}
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 != "" {
flag.Negated = true
}
err := flag.Parse(c.scan, c.getValue(flag.Value))
if err != nil {
if e, ok := errors.Cause(err).(*expectedError); ok && e.token.InferredType().IsAny(FlagToken, ShortFlagToken) {
return errors.Errorf("%s; perhaps try %s=%q?", err, flag.ShortSummary(), e.token)
var expected *expectedError
if errors.As(err, &expected) && expected.token.InferredType().IsAny(FlagToken, ShortFlagToken) {
return fmt.Errorf("%s; perhaps try %s=%q?", err.Error(), flag.ShortSummary(), expected.token)
}
return err
}
c.Path = append(c.Path, &Path{Flag: flag})
if flag.Negated {
value := c.getValue(flag.Value)
err := flipBoolValue(value)
if err != nil {
return err
}
flag.Value.Apply(value)
}
c.Path = append(c.Path, &Path{
Flag: flag,
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 ...any) (out []any, err error) {
fv := reflect.ValueOf(fn)
bindings := c.Kong.bindings.clone().add(binds...).add(c).merge(c.bindings)
return callAnyFunction(fv, bindings)
}
// RunNode calls the Run() method on an arbitrary node.
@@ -580,7 +828,7 @@ func (c *Context) parseFlag(flags []*Flag, match string) (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) 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
@@ -593,6 +841,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})
@@ -601,13 +862,8 @@ 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 = callMethod("Run", method.node.Target, method.method, method.binds); err != nil {
if err = callFunction(method.method, method.binds); err != nil {
return err
}
}
@@ -618,13 +874,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) {
defer catch(&err)
func (c *Context) Run(binds ...any) (err error) {
node := c.Selected()
if node == nil {
return fmt.Errorf("no command selected")
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
}
}
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.
@@ -633,25 +903,82 @@ func (c *Context) Run(binds ...interface{}) (err error) {
func (c *Context) PrintUsage(summary bool) error {
options := c.helpOptions
options.Summary = summary
_ = c.help(options, c)
return nil
return c.printHelp(options)
}
func (c *Context) printHelp(options HelpOptions) error {
options.ValueFormatter = c.Kong.helpFormatter
return c.help(options, c)
}
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
}
missing = append(missing, flag.Summary())
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())
}
}
for xor, flags := range xorGroup {
if !xorGroupSet[xor] && len(flags) > 1 {
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
}
sort.Strings(missing)
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{}
@@ -665,7 +992,10 @@ func checkMissingChildren(node *Node) error {
missing = append(missing, strconv.Quote(strings.Join(missingArgs, " ")))
}
haveDefault := 0
// A node with a Run() method may run on its own, so it does not require one
// of its subcommands to be selected.
runnable := node.Target.IsValid() && getMethod(node.Target, "Run").IsValid()
for _, child := range node.Children {
if child.Hidden {
continue
@@ -675,20 +1005,11 @@ func checkMissingChildren(node *Node) error {
continue
}
missing = append(missing, strconv.Quote(child.Summary()))
} else {
if child.Tag.Default != "" {
if len(child.Children) > 0 {
return fmt.Errorf("default command %s must not have subcommands or arguments", child.Summary())
}
haveDefault++
}
} else if !runnable {
missing = append(missing, strconv.Quote(child.Name))
}
}
if haveDefault > 1 {
return fmt.Errorf("more than one default command found under %s", node.Summary())
}
if len(missing) == 0 || haveDefault > 0 {
if len(missing) == 0 {
return nil
}
@@ -698,7 +1019,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.
@@ -715,7 +1036,17 @@ func checkMissingPositionals(positional int, values []*Value) error {
missing := []string{}
for ; positional < len(values); positional++ {
missing = append(missing, "<"+values[positional].Name+">")
arg := values[positional]
// TODO(aat): Fix hardcoding of these env checks all over the place :\
if len(arg.Tag.Envs) != 0 {
if atLeastOneEnvSet(arg.Tag.Envs) {
continue
}
}
missing = append(missing, "<"+arg.Name+">")
}
if len(missing) == 0 {
return nil
}
return fmt.Errorf("missing positional arguments %s", strings.Join(missing, " "))
}
@@ -731,23 +1062,51 @@ func checkEnum(value *Value, target reflect.Value) error {
return nil
case reflect.Map, reflect.Struct:
return errors.Errorf("enum can only be applied to a slice or value")
return errors.New("enum can only be applied to a slice or value")
default:
enumMap := value.EnumMap()
v := fmt.Sprintf("%v", target)
if enumMap[v] {
case reflect.Ptr:
if target.IsNil() {
return nil
}
return checkEnum(value, target.Elem())
default:
enumSlice := value.EnumSlice()
v := fmt.Sprintf("%v", target)
enums := []string{}
for enum := range enumMap {
for _, enum := range enumSlice {
if enum == v {
return nil
}
enums = append(enums, fmt.Sprintf("%q", enum))
}
sort.Strings(enums)
return fmt.Errorf("%s must be one of %s but got %q", value.ShortSummary(), strings.Join(enums, ","), target.Interface())
return fmt.Errorf("%s must be one of %s but got %q", value.ShortSummary(), strings.Join(enums, ","), fmt.Sprintf("%v", target.Interface()))
}
}
func checkPassthroughArg(target reflect.Value) bool {
typ := target.Type()
switch typ.Kind() {
case reflect.Slice:
return typ.Elem().Kind() == reflect.String
default:
return false
}
}
func 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{}
@@ -755,19 +1114,50 @@ func checkXorDuplicates(paths []*Path) error {
if !flag.Set {
continue
}
if flag.Xor == "" {
continue
for _, xor := range flag.Xor {
if seen[xor] != nil {
return fmt.Errorf("--%s and --%s can't be used together", seen[xor].Name, flag.Name)
}
seen[xor] = flag
}
if seen[flag.Xor] != nil {
return fmt.Errorf("--%s and --%s can't be used together", seen[flag.Xor].Name, flag.Name)
}
seen[flag.Xor] = flag
}
}
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...)
}
@@ -785,3 +1175,49 @@ func findPotentialCandidates(needle string, haystack []string, format string, ar
}
return fmt.Errorf("%s", prefix)
}
type validatable interface{ Validate() error }
type extendedValidatable interface {
Validate(kctx *Context) error
}
// 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() {
return isValidatable(v.Addr())
}
return nil
}
// getValidators returns validators implemented by v and by any embedded fields,
// matching how hooks are discovered (see getMethods).
func getValidators(v reflect.Value) (validators []extendedValidatable) {
walkEmbedded(v, func(v reflect.Value) {
if validate := isValidatable(v); validate != nil {
validators = append(validators, validate)
}
})
return
}
func atLeastOneEnvSet(envs []string) bool {
for _, env := range envs {
if _, ok := os.LookupEnv(env); ok {
return true
}
}
return false
}
+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
+17 -17
View File
@@ -2,31 +2,31 @@
//
// Here's an example:
//
// shell rm [-f] [-r] <paths> ...
// shell ls [<paths> ...]
// shell rm [-f] [-r] <paths> ...
// shell ls [<paths> ...]
//
// This can be represented by the following command-line structure:
//
// package main
// package main
//
// import "github.com/alecthomas/kong"
// import "github.com/alecthomas/kong"
//
// var CLI struct {
// Rm struct {
// Force bool `short:"f" help:"Force removal."`
// Recursive bool `short:"r" help:"Recursively remove files."`
// var CLI struct {
// Rm struct {
// Force bool `short:"f" help:"Force removal."`
// Recursive bool `short:"r" help:"Recursively remove files."`
//
// Paths []string `arg help:"Paths to remove." type:"path"`
// } `cmd help:"Remove files."`
// Paths []string `arg help:"Paths to remove." type:"path"`
// } `cmd help:"Remove files."`
//
// Ls struct {
// Paths []string `arg optional help:"Paths to list." type:"path"`
// } `cmd help:"List paths."`
// }
// Ls struct {
// Paths []string `arg optional help:"Paths to list." type:"path"`
// } `cmd help:"List paths."`
// }
//
// func main() {
// kong.Parse(&CLI)
// }
// func main() {
// kong.Parse(&CLI)
// }
//
// See https://github.com/alecthomas/kong for details.
package kong
+12 -3
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
}
// Cause returns the original cause of the error.
func (p *ParseError) Cause() error { return p.error }
// Unwrap returns the original cause of the error.
func (p *ParseError) Unwrap() error { return p.error }
// 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)
-10
View File
@@ -1,10 +0,0 @@
module github.com/alecthomas/kong
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pkg/errors v0.8.1
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/stretchr/testify v1.2.2
)
go 1.13
-8
View File
@@ -1,8 +0,0 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+1 -1
View File
@@ -1,4 +1,4 @@
// +build appengine !linux,!freebsd,!darwin,!dragonfly,!netbsd,!openbsd
//go:build tinygo || appengine || (!linux && !freebsd && !darwin && !dragonfly && !netbsd && !openbsd)
package kong
+3 -3
View File
@@ -1,4 +1,4 @@
// +build !appengine,linux freebsd darwin dragonfly netbsd openbsd
//go:build !tinygo && ((!appengine && linux) || freebsd || darwin || dragonfly || netbsd || openbsd)
package kong
@@ -26,9 +26,9 @@ func guessWidth(w io.Writer) int {
if _, _, err := syscall.Syscall6(
syscall.SYS_IOCTL,
uintptr(fd), // nolint: unconvert
uintptr(fd), //nolint: unconvert
uintptr(syscall.TIOCGWINSZ),
uintptr(unsafe.Pointer(&dimensions)), // nolint: gas
uintptr(unsafe.Pointer(&dimensions)), //nolint: gas
0, 0, 0,
); err == 0 {
if dimensions[1] == 0 {
+231 -60
View File
@@ -14,12 +14,14 @@ const (
)
// Help flag.
type helpValue bool
type helpFlag bool
func (h helpValue) BeforeApply(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)
err := ctx.printHelp(options)
if err != nil {
return err
}
@@ -41,10 +43,28 @@ type HelpOptions struct {
// Tree writes command chains in a tree structure instead of listing them separately.
Tree bool
// Place the flags after the commands listing.
FlagsLast bool
// Indenter modulates the given prefix for the next layer in the tree view.
// The following exported templates can be used: kong.SpaceIndenter, kong.LineIndenter, kong.TreeIndenter
// The kong.SpaceIndenter will be used by default.
Indenter HelpIndenter
// Don't show the help associated with subcommands
NoExpandSubcommands bool
// Clamp the help wrap width to a value smaller than the terminal width.
// If this is set to a non-positive number, the terminal width is used; otherwise,
// the min of this value or the terminal width is used.
WrapUpperBound int
// ValueFormatter is used to format the help text of flags and positional arguments.
ValueFormatter HelpValueFormatter
// NoAppDescFormat skips all formatting of app description text (wrapping, paragraph
// reflow, newline merging, etc).
NoAppDescFormat bool
}
// Apply options to Kong as a configuration option.
@@ -59,6 +79,11 @@ type HelpProvider interface {
Help() string
}
// PlaceHolderProvider can be implemented by mappers to provide custom placeholder text.
type PlaceHolderProvider interface {
PlaceHolder(flag *Flag) string
}
// HelpIndenter is used to indent new layers in the help tree.
type HelpIndenter func(prefix string) string
@@ -70,10 +95,10 @@ type HelpValueFormatter func(value *Value) string
// DefaultHelpValueFormatter is the default HelpValueFormatter.
func DefaultHelpValueFormatter(value *Value) string {
if value.Tag.Env == "" {
if len(value.Tag.Envs) == 0 || HasInterpolatedVar(value.OrigHelp, "env") {
return value.Help
}
suffix := "($" + value.Tag.Env + ")"
suffix := "(" + formatEnvs(value.Tag.Envs) + ")"
switch {
case strings.HasSuffix(value.Help, "."):
return value.Help[:len(value.Help)-1] + " " + suffix + "."
@@ -84,6 +109,21 @@ func DefaultHelpValueFormatter(value *Value) string {
}
}
// DefaultShortHelpPrinter is the default HelpPrinter for short help on error.
func DefaultShortHelpPrinter(options HelpOptions, ctx *Context) error {
w := newHelpWriter(ctx, options)
cmd := ctx.Selected()
app := ctx.Model
if cmd == nil {
w.Printf("Usage: %s%s", app.Name, app.Summary())
w.Printf(`Run "%s --help" for more information.`, app.Name)
} else {
w.Printf("Usage: %s %s", app.Name, cmd.Summary())
w.Printf(`Run "%s --help" for more information.`, cmd.FullPath())
}
return w.Write(ctx.Stdout)
}
// DefaultHelpPrinter is the default HelpPrinter.
func DefaultHelpPrinter(options HelpOptions, ctx *Context) error {
if ctx.Empty() {
@@ -126,10 +166,16 @@ func printCommand(w *helpWriter, app *Application, cmd *Command) {
}
}
func printNodeDetail(w *helpWriter, node *Node, hide bool) {
func printNodeDetail(w *helpWriter, node *Node, hide bool) { //nolint:gocyclo
if node.Help != "" {
w.Print("")
w.Wrap(node.Help)
if w.NoAppDescFormat && node.Parent == nil {
for _, line := range strings.Split(node.Help, "\n") {
w.Print(line)
}
} else {
w.Wrap(node.Help)
}
}
if w.Summary {
return
@@ -143,26 +189,60 @@ func printNodeDetail(w *helpWriter, node *Node, hide bool) {
w.Print("Arguments:")
writePositionals(w.Indent(), node.Positional)
}
if flags := node.AllFlags(true); len(flags) > 0 {
w.Print("")
w.Print("Flags:")
writeFlags(w.Indent(), flags)
}
cmds := node.Leaves(hide)
if len(cmds) > 0 {
w.Print("")
w.Print("Commands:")
if w.Tree {
writeCommandTree(w, node)
} else {
iw := w.Indent()
if w.Compact {
writeCompactCommandList(cmds, iw)
} else {
writeCommandList(cmds, iw)
printFlags := func() {
if flags := node.AllFlags(true); len(flags) > 0 {
groupedFlags := collectFlagGroups(flags)
for _, group := range groupedFlags {
w.Print("")
if group.Metadata.Title != "" {
w.Wrap(group.Metadata.Title)
}
if group.Metadata.Description != "" {
w.Indent().Wrap(group.Metadata.Description)
w.Print("")
}
writeFlags(w.Indent(), group.Flags)
}
}
}
if !w.FlagsLast {
printFlags()
}
var cmds []*Node
if w.NoExpandSubcommands {
cmds = node.Children
} else {
cmds = node.Leaves(hide)
}
if len(cmds) > 0 {
iw := w.Indent()
if w.Tree {
w.Print("")
w.Print("Commands:")
writeCommandTree(iw, node)
} else {
groupedCmds := collectCommandGroups(cmds)
for _, group := range groupedCmds {
w.Print("")
if group.Metadata.Title != "" {
w.Wrap(group.Metadata.Title)
}
if group.Metadata.Description != "" {
w.Indent().Wrap(group.Metadata.Description)
w.Print("")
}
if w.Compact {
writeCompactCommandList(group.Commands, iw)
} else {
writeCommandList(group.Commands, iw)
}
}
}
}
if w.FlagsLast {
printFlags()
}
}
func writeCommandList(cmds []*Node, iw *helpWriter) {
@@ -189,7 +269,6 @@ func writeCompactCommandList(cmds []*Node, iw *helpWriter) {
}
func writeCommandTree(w *helpWriter, node *Node) {
iw := w.Indent()
rows := make([][2]string, 0, len(node.Children)*2)
for i, cmd := range node.Children {
if cmd.Hidden {
@@ -200,27 +279,93 @@ func writeCommandTree(w *helpWriter, node *Node) {
rows = append(rows, [2]string{"", ""})
}
}
writeTwoColumns(iw, rows)
writeTwoColumns(w, rows)
}
type helpFlagGroup struct {
Metadata *Group
Flags [][]*Flag
}
func collectFlagGroups(flags [][]*Flag) []helpFlagGroup {
// Group keys in order of appearance.
groups := []*Group{}
// Flags grouped by their group key.
flagsByGroup := map[string][][]*Flag{}
for _, levelFlags := range flags {
levelFlagsByGroup := map[string][]*Flag{}
for _, flag := range levelFlags {
key := ""
if flag.Group != nil {
key = flag.Group.Key
groupAlreadySeen := false
for _, group := range groups {
if key == group.Key {
groupAlreadySeen = true
break
}
}
if !groupAlreadySeen {
groups = append(groups, flag.Group)
}
}
levelFlagsByGroup[key] = append(levelFlagsByGroup[key], flag)
}
for key, flags := range levelFlagsByGroup {
flagsByGroup[key] = append(flagsByGroup[key], flags)
}
}
out := []helpFlagGroup{}
// Ungrouped flags are always displayed first.
if ungroupedFlags, ok := flagsByGroup[""]; ok {
out = append(out, helpFlagGroup{
Metadata: &Group{Title: "Flags:"},
Flags: ungroupedFlags,
})
}
for _, group := range groups {
out = append(out, helpFlagGroup{Metadata: group, Flags: flagsByGroup[group.Key]})
}
return out
}
// nolint: unused
type helpCommandGroup struct {
Name string
Metadata *Group
Commands []*Node
}
// nolint: unused, deadcode
func collectCommandGroups(nodes []*Node) []helpCommandGroup {
groups := map[string][]*Node{}
// Groups in order of appearance.
groups := []*Group{}
// Nodes grouped by their group key.
nodesByGroup := map[string][]*Node{}
for _, node := range nodes {
groups[node.Group] = append(groups[node.Group], node)
}
out := []helpCommandGroup{}
for name, nodes := range groups {
if name == "" {
name = "Commands"
key := ""
if group := node.ClosestGroup(); group != nil {
key = group.Key
if _, ok := nodesByGroup[key]; !ok {
groups = append(groups, group)
}
}
out = append(out, helpCommandGroup{Name: name, Commands: nodes})
nodesByGroup[key] = append(nodesByGroup[key], node)
}
out := []helpCommandGroup{}
// Ungrouped nodes are always displayed first.
if ungroupedNodes, ok := nodesByGroup[""]; ok {
out = append(out, helpCommandGroup{
Metadata: &Group{Title: "Commands:"},
Commands: ungroupedNodes,
})
}
for _, group := range groups {
out = append(out, helpCommandGroup{Metadata: group, Commands: nodesByGroup[group.Key]})
}
return out
}
@@ -233,26 +378,28 @@ func printCommandSummary(w *helpWriter, cmd *Command) {
}
type helpWriter struct {
indent string
width int
lines *[]string
helpFormatter HelpValueFormatter
indent string
width int
lines *[]string
HelpOptions
}
func newHelpWriter(ctx *Context, options HelpOptions) *helpWriter {
lines := []string{}
wrapWidth := guessWidth(ctx.Stdout)
if options.WrapUpperBound > 0 && wrapWidth > options.WrapUpperBound {
wrapWidth = options.WrapUpperBound
}
w := &helpWriter{
indent: "",
width: guessWidth(ctx.Stdout),
lines: &lines,
helpFormatter: ctx.Kong.helpFormatter,
HelpOptions: options,
indent: "",
width: wrapWidth,
lines: &lines,
HelpOptions: options,
}
return w
}
func (h *helpWriter) Printf(format string, args ...interface{}) {
func (h *helpWriter) Printf(format string, args ...any) {
h.Print(fmt.Sprintf(format, args...))
}
@@ -260,8 +407,9 @@ func (h *helpWriter) Print(text string) {
*h.lines = append(*h.lines, strings.TrimRight(h.indent+text, " "))
}
// Indent returns a new helpWriter indented by two characters.
func (h *helpWriter) Indent() *helpWriter {
return &helpWriter{indent: h.indent + " ", lines: h.lines, width: h.width - 2, HelpOptions: h.HelpOptions, helpFormatter: h.helpFormatter}
return &helpWriter{indent: h.indent + " ", lines: h.lines, width: h.width - 2, HelpOptions: h.HelpOptions}
}
func (h *helpWriter) String() string {
@@ -280,7 +428,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)
}
@@ -289,7 +437,7 @@ func (h *helpWriter) Wrap(text string) {
func writePositionals(w *helpWriter, args []*Positional) {
rows := [][2]string{}
for _, arg := range args {
rows = append(rows, [2]string{arg.Summary(), w.helpFormatter(arg)})
rows = append(rows, [2]string{arg.Summary(), w.HelpOptions.ValueFormatter(arg)})
}
writeTwoColumns(w, rows)
}
@@ -311,7 +459,7 @@ func writeFlags(w *helpWriter, groups [][]*Flag) {
}
for _, flag := range group {
if !flag.Hidden {
rows = append(rows, [2]string{formatFlag(haveShort, flag), w.helpFormatter(flag.Value)})
rows = append(rows, [2]string{formatFlag(haveShort, flag), w.HelpOptions.ValueFormatter(flag.Value)})
}
}
}
@@ -335,7 +483,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])
@@ -355,16 +503,24 @@ func formatFlag(haveShort bool, flag *Flag) string {
flagString := ""
name := flag.Name
isBool := flag.IsBool()
isCounter := flag.IsCounter()
short := ""
if flag.Short != 0 {
flagString += fmt.Sprintf("-%c, --%s", flag.Short, 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 {
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())
}
return flagString
@@ -376,6 +532,9 @@ func (h *HelpOptions) CommandTree(node *Node, prefix string) (rows [][2]string)
switch node.Type {
default:
nodeName += prefix + node.Name
if len(node.Aliases) != 0 {
nodeName += fmt.Sprintf(" (%s)", strings.Join(node.Aliases, ","))
}
case ArgumentNode:
nodeName += prefix + "<" + node.Name + ">"
}
@@ -389,6 +548,9 @@ func (h *HelpOptions) CommandTree(node *Node, prefix string) (rows [][2]string)
rows = append(rows, [2]string{prefix + arg.Summary(), arg.Help})
}
for _, subCmd := range node.Children {
if subCmd.Hidden {
continue
}
rows = append(rows, h.CommandTree(subCmd, prefix)...)
}
return
@@ -414,3 +576,12 @@ func TreeIndenter(prefix string) string {
}
return "|" + strings.Repeat(" ", defaultIndent) + prefix
}
func formatEnvs(envs []string) string {
formatted := make([]string, len(envs))
for i := range envs {
formatted[i] = "$" + envs[i]
}
return strings.Join(formatted, ", ")
}
+17 -4
View File
@@ -1,19 +1,32 @@
package kong
// BeforeResolve is a documentation-only interface describing hooks that run before values are set.
// 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
}
+17 -4
View File
@@ -5,7 +5,18 @@ import (
"regexp"
)
var interpolationRegex = regexp.MustCompile(`((?:\${([[:alpha:]_][[:word:]]*))(?:=([^}]+))?})|(\$)|([^$]+)`)
var interpolationRegex = regexp.MustCompile(`(\$\$)|((?:\${([[:alpha:]_][[:word:]]*))(?:=([^}]+))?})|(\$)|([^$]+)`)
// HasInterpolatedVar returns true if the variable "v" is interpolated in "s".
func HasInterpolatedVar(s string, v string) bool {
matches := interpolationRegex.FindAllStringSubmatch(s, -1)
for _, match := range matches {
if name := match[3]; name == v {
return true
}
}
return false
}
// Interpolate variables from vars into s for substrings in the form ${var} or ${var=default}.
func interpolate(s string, vars Vars, updatedVars map[string]string) (string, error) {
@@ -21,14 +32,16 @@ func interpolate(s string, vars Vars, updatedVars map[string]string) (string, er
}
}
for _, match := range matches {
if name := match[2]; name != "" {
if dollar := match[1]; dollar != "" {
out += "$"
} else if name := match[3]; name != "" {
value, ok := vars[name]
if !ok {
// No default value.
if match[3] == "" {
if match[4] == "" {
return "", fmt.Errorf("undefined variable ${%s}", name)
}
value = match[3]
value = match[4]
}
out += value
} else {
+211 -69
View File
@@ -1,11 +1,13 @@
package kong
import (
"errors"
"fmt"
"io"
"os"
"path/filepath"
"reflect"
"regexp"
"strings"
)
@@ -13,17 +15,16 @@ var (
callbackReturnSignature = reflect.TypeOf((*error)(nil)).Elem()
)
// Error reported by Kong.
type Error struct{ msg string }
func (e Error) Error() string { return e.msg }
func fail(format string, args ...interface{}) {
panic(Error{msg: fmt.Sprintf(format, args...)})
func failField(parent reflect.Value, field reflect.StructField, format string, args ...any) error {
name := parent.Type().Name()
if name == "" {
name = "<anonymous struct>"
}
return fmt.Errorf("%s.%s: %s", name, field.Name, fmt.Sprintf(format, args...))
}
// Must creates a new Parser or panics if there is an error.
func Must(ast interface{}, options ...Option) *Kong {
func Must(ast any, options ...Option) *Kong {
k, err := New(ast, options...)
if err != nil {
panic(err)
@@ -31,6 +32,13 @@ func Must(ast interface{}, options ...Option) *Kong {
return k
}
type usageOnError int
const (
shortUsage usageOnError = iota + 1
fullUsage
)
// Kong is the main parser type.
type Kong struct {
// Grammar model.
@@ -42,27 +50,36 @@ type Kong struct {
Stdout io.Writer
Stderr io.Writer
bindings bindings
loader ConfigurationLoader
resolvers []Resolver
registry *Registry
bindings bindings
loader ConfigurationLoader
resolvers []Resolver
registry *Registry
ignoreFields []*regexp.Regexp
noDefaultHelp bool
usageOnError bool
help HelpPrinter
helpFormatter HelpValueFormatter
helpOptions HelpOptions
helpFlag *Flag
vars Vars
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://github.com/alecthomas/kong) for usage instructions.
func New(grammar interface{}, options ...Option) (*Kong, error) {
func New(grammar any, options ...Option) (*Kong, error) {
k := &Kong{
Exit: os.Exit,
Stdout: os.Stdout,
@@ -70,7 +87,12 @@ 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 {
return strings.ToLower(dashedString(s))
},
}
options = append(options, Bind(k))
@@ -85,6 +107,10 @@ func New(grammar interface{}, options ...Option) (*Kong, error) {
k.help = DefaultHelpPrinter
}
if k.shortHelp == nil {
k.shortHelp = DefaultShortHelpPrinter
}
model, err := build(k, grammar)
if err != nil {
return k, err
@@ -93,6 +119,45 @@ func New(grammar interface{}, options ...Option) (*Kong, error) {
k.Model = model
k.Model.HelpFlag = k.helpFlag
// Embed any embedded structs.
for _, embed := range k.embedded {
tag, err := parseTagString(strings.Join(embed.tags, " "))
if err != nil {
return nil, err
}
tag.Embed = true
v := reflect.Indirect(reflect.ValueOf(embed.strct))
node, err := buildNode(k, v, CommandNode, tag, map[string]bool{})
if err != nil {
return nil, err
}
for _, child := range node.Children {
child.Parent = k.Model.Node
k.Model.Children = append(k.Model.Children, child)
}
k.Model.Flags = append(k.Model.Flags, node.Flags...)
}
// Synthesise command nodes.
for _, dcmd := range k.dynamicCommands {
tag, terr := parseTagString(strings.Join(dcmd.tags, " "))
if terr != nil {
return nil, terr
}
tag.Name = dcmd.name
tag.Help = dcmd.help
tag.Group = dcmd.group
tag.Cmd = true
v := reflect.Indirect(reflect.ValueOf(dcmd.cmd))
err = buildChild(k, k.Model.Node, CommandNode, reflect.Value{}, reflect.StructField{
Name: dcmd.name,
Type: v.Type(),
}, v, tag, dcmd.name, map[string]bool{})
if err != nil {
return nil, err
}
}
for _, option := range k.postBuildOptions {
if err = option.Apply(k); err != nil {
return nil, err
@@ -106,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] }
@@ -147,16 +245,44 @@ func (k *Kong) interpolateValue(value *Value, vars Vars) (err error) {
if len(value.Tag.Vars) > 0 {
vars = vars.CloneWith(value.Tag.Vars)
}
if varsContributor, ok := value.Mapper.(VarsContributor); ok {
vars = vars.CloneWith(varsContributor.Vars(value))
}
initialVars := vars.CloneWith(nil)
for n, v := range initialVars {
if vars[n], err = interpolate(v, initialVars, nil); err != nil {
return fmt.Errorf("variable %s for %s: %s", n, value.Summary(), err)
}
}
if value.Default, err = interpolate(value.Default, vars, nil); err != nil {
return fmt.Errorf("default value for %s: %s", value.Summary(), err)
}
if value.Enum, err = interpolate(value.Enum, vars, nil); err != nil {
return fmt.Errorf("enum value for %s: %s", value.Summary(), err)
}
value.Help, err = interpolate(value.Help, vars, map[string]string{
updatedVars := map[string]string{
"default": value.Default,
"enum": value.Enum,
})
}
for i, env := range value.Tag.Envs {
if value.Tag.Envs[i], err = interpolate(env, vars, updatedVars); err != nil {
return fmt.Errorf("env value for %s: %s", value.Summary(), err)
}
}
updatedVars["env"] = ""
if len(value.Tag.Envs) != 0 {
updatedVars["env"] = value.Tag.Envs[0]
}
if value.Flag != nil {
value.Flag.Envs = value.Tag.Envs
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 {
return fmt.Errorf("help for %s: %s", value.Summary(), err)
}
@@ -168,13 +294,14 @@ 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',
Value: &Value{
Name: "help",
Help: "Show context-sensitive help.",
OrigHelp: "Show context-sensitive help.",
Target: value,
Tag: &Tag{},
Mapper: k.registry.ForValue(value),
@@ -194,13 +321,15 @@ func (k *Kong) extraFlags() []*Flag {
// Will return a ParseError if a *semantically* invalid command-line is encountered (as opposed to a syntactically
// invalid one, which will report a normal error).
func (k *Kong) Parse(args []string) (ctx *Context, err error) {
defer catch(&err)
ctx, err = Trace(k, args)
if err != nil {
return nil, err
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}
}
if err = ctx.Reset(); err != nil {
return nil, &ParseError{error: err, Context: ctx}
@@ -214,11 +343,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}
@@ -243,23 +372,31 @@ 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 := callMethod(name, value, 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)
}
// Call hook on any unset flags with default values.
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 or values supplied via env.
func (k *Kong) applyHookToDefaultFlags(ctx *Context, node *Node, name string) error {
if node == nil {
return nil
@@ -271,24 +408,30 @@ func (k *Kong) applyHookToDefaultFlags(ctx *Context, node *Node, name string) er
}
binds := k.bindings.clone().add(ctx).add(node.Vars().CloneWith(k.vars))
for _, flag := range node.Flags {
if flag.Default == "" || ctx.values[flag.Value].IsValid() || !flag.Target.IsValid() {
// Flags handled here are the ones that won't show up in ctx.Path:
// they got their value from the default tag or an env var, both of
// which Reset() applies straight to the target without touching the
// parse path. Anything actually parsed off argv or set by a resolver
// shows up in ctx.values and is covered by the main hook loop.
if ctx.values[flag.Value].IsValid() || !flag.Target.IsValid() {
continue
}
method := getMethod(flag.Target, name)
if !method.IsValid() {
if !flag.HasDefault && !atLeastOneEnvSet(flag.Tag.Envs) {
continue
}
path := &Path{Flag: flag}
if err := callMethod(name, flag.Target, 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 == "" {
@@ -303,40 +446,48 @@ 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
}
msg := err.Error()
if len(args) > 0 {
msg = fmt.Sprintf(args[0].(string), args[1:]...) + ": " + err.Error()
msg = fmt.Sprintf(args[0].(string), args[1:]...) + ": " + err.Error() //nolint
}
// Maybe display usage information.
if err, ok := err.(*ParseError); ok && k.usageOnError {
options := k.helpOptions
_ = k.help(options, err.Context)
fmt.Fprintln(k.Stdout)
var parseErr *ParseError
if errors.As(err, &parseErr) {
switch k.usageOnError {
case fullUsage:
_ = parseErr.Context.printHelp(k.helpOptions)
fmt.Fprintln(k.Stdout)
case shortUsage:
_ = k.shortHelp(k.helpOptions, parseErr.Context)
fmt.Fprintln(k.Stdout)
}
}
k.Errorf("%s", msg)
k.Exit(1)
k.Exit(exitCodeFromError(err))
}
// LoadConfig from path using the loader configured via Configuration(loader).
@@ -349,7 +500,7 @@ func (k *Kong) LoadConfig(path string) (Resolver, error) {
if err != nil {
return nil, err
}
r, err := os.Open(path) // nolint: gas
r, err := os.Open(path) //nolint: gas
if err != nil {
return nil, err
}
@@ -357,12 +508,3 @@ func (k *Kong) LoadConfig(path string) (Resolver, error) {
return k.loader(r)
}
func catch(err *error) {
msg := recover()
if test, ok := msg.(Error); ok {
*err = test
} else if msg != nil {
panic(msg)
}
}
+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
}
+328 -90
View File
@@ -3,8 +3,9 @@ package kong
import (
"encoding"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"io"
"math/bits"
"net/url"
"os"
@@ -12,13 +13,12 @@ import (
"strconv"
"strings"
"time"
"github.com/pkg/errors"
)
var (
mapperValueType = reflect.TypeOf((*MapperValue)(nil)).Elem()
boolMapperType = reflect.TypeOf((*BoolMapper)(nil)).Elem()
boolMapperValueType = reflect.TypeOf((*BoolMapperValue)(nil)).Elem()
jsonUnmarshalerType = reflect.TypeOf((*json.Unmarshaler)(nil)).Elem()
textUnmarshalerType = reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem()
binaryUnmarshalerType = reflect.TypeOf((*encoding.BinaryUnmarshaler)(nil)).Elem()
)
@@ -42,19 +42,26 @@ func (r *DecodeContext) WithScanner(scan *Scanner) *DecodeContext {
}
// MapperValue may be implemented by fields in order to provide custom mapping.
// Mappers may additionally implement PlaceHolderProvider to provide custom placeholder text.
type MapperValue interface {
Decode(ctx *DecodeContext) error
}
// BoolMapperValue may be implemented by fields in order to provide custom mappings for boolean values.
type BoolMapperValue interface {
MapperValue
IsBool() bool
}
type mapperValueAdapter struct {
isBool bool
}
func (m *mapperValueAdapter) Decode(ctx *DecodeContext, target reflect.Value) error {
if target.Type().Implements(mapperValueType) {
return target.Interface().(MapperValue).Decode(ctx)
return target.Interface().(MapperValue).Decode(ctx) //nolint
}
return target.Addr().Interface().(MapperValue).Decode(ctx)
return target.Addr().Interface().(MapperValue).Decode(ctx) //nolint
}
func (m *mapperValueAdapter) IsBool() bool {
@@ -70,9 +77,9 @@ func (m *textUnmarshalerAdapter) Decode(ctx *DecodeContext, target reflect.Value
return err
}
if target.Type().Implements(textUnmarshalerType) {
return target.Interface().(encoding.TextUnmarshaler).UnmarshalText([]byte(value))
return target.Interface().(encoding.TextUnmarshaler).UnmarshalText([]byte(value)) //nolint
}
return target.Addr().Interface().(encoding.TextUnmarshaler).UnmarshalText([]byte(value))
return target.Addr().Interface().(encoding.TextUnmarshaler).UnmarshalText([]byte(value)) //nolint
}
type binaryUnmarshalerAdapter struct{}
@@ -83,33 +90,59 @@ func (m *binaryUnmarshalerAdapter) Decode(ctx *DecodeContext, target reflect.Val
if err != nil {
return err
}
if target.Type().Implements(textUnmarshalerType) {
return target.Interface().(encoding.BinaryUnmarshaler).UnmarshalBinary([]byte(value))
if target.Type().Implements(binaryUnmarshalerType) {
return target.Interface().(encoding.BinaryUnmarshaler).UnmarshalBinary([]byte(value)) //nolint
}
return target.Addr().Interface().(encoding.BinaryUnmarshaler).UnmarshalBinary([]byte(value))
return target.Addr().Interface().(encoding.BinaryUnmarshaler).UnmarshalBinary([]byte(value)) //nolint
}
type jsonUnmarshalerAdapter struct{}
func (j *jsonUnmarshalerAdapter) Decode(ctx *DecodeContext, target reflect.Value) error {
var value string
err := ctx.Scan.PopValueInto("value", &value)
if err != nil {
return err
}
if target.Type().Implements(jsonUnmarshalerType) {
return target.Interface().(json.Unmarshaler).UnmarshalJSON([]byte(value)) //nolint
}
return target.Addr().Interface().(json.Unmarshaler).UnmarshalJSON([]byte(value)) //nolint
}
// A Mapper represents how a field is mapped from command-line values to Go.
//
// Mappers can be associated with concrete fields via pointer, reflect.Type, reflect.Kind, or via a "type" tag.
//
// Additionally, if a type implements the MappverValue interface, it will be used.
// Additionally, if a type implements the MapperValue interface, it will be used.
type Mapper interface {
// Decode ctx.Value with ctx.Scanner into target.
Decode(ctx *DecodeContext, target reflect.Value) error
}
// VarsContributor can be implemented by a Mapper to contribute Vars during interpolation.
type VarsContributor interface {
Vars(ctx *Value) Vars
}
// A BoolMapper is a Mapper to a value that is a boolean.
//
// This is used solely for formatting help.
type BoolMapper interface {
Mapper
IsBool() bool
}
// BoolMapperExt allows a Mapper to dynamically determine if a value is a boolean.
type BoolMapperExt interface {
Mapper
IsBoolFromValue(v reflect.Value) bool
}
// A MapperFunc is a single function that complies with the Mapper interface.
type MapperFunc func(ctx *DecodeContext, target reflect.Value) error
func (m MapperFunc) Decode(ctx *DecodeContext, target reflect.Value) error { // nolint: golint
func (m MapperFunc) Decode(ctx *DecodeContext, target reflect.Value) error { //nolint: revive
return m(ctx, target)
}
@@ -166,7 +199,8 @@ func (r *Registry) ForType(typ reflect.Type) Mapper {
// Check if the type implements MapperValue.
for _, impl := range []reflect.Type{typ, reflect.PtrTo(typ)} {
if impl.Implements(mapperValueType) {
return &mapperValueAdapter{impl.Implements(boolMapperType)}
// FIXME: This should pass in the bool mapper.
return &mapperValueAdapter{impl.Implements(boolMapperValueType)}
}
}
// Next, try explicitly registered types.
@@ -177,10 +211,13 @@ func (r *Registry) ForType(typ reflect.Type) Mapper {
}
// Next try stdlib unmarshaler interfaces.
for _, impl := range []reflect.Type{typ, reflect.PtrTo(typ)} {
if impl.Implements(textUnmarshalerType) {
switch {
case impl.Implements(textUnmarshalerType):
return &textUnmarshalerAdapter{}
} else if impl.Implements(binaryUnmarshalerType) {
case impl.Implements(binaryUnmarshalerType):
return &binaryUnmarshalerAdapter{}
case impl.Implements(jsonUnmarshalerType):
return &jsonUnmarshalerAdapter{}
}
}
// Finally try registered kinds.
@@ -200,8 +237,8 @@ func (r *Registry) RegisterKind(kind reflect.Kind, mapper Mapper) *Registry {
//
// eg.
//
// Mapper string `kong:"type='colour'`
// registry.RegisterName("colour", ...)
// Mapper string `kong:"type='colour'`
// registry.RegisterName("colour", ...)
func (r *Registry) RegisterName(name string, mapper Mapper) *Registry {
r.names[name] = mapper
return r
@@ -214,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")
@@ -231,16 +268,15 @@ func (r *Registry) RegisterDefaults() *Registry {
RegisterKind(reflect.Int16, intDecoder(16)).
RegisterKind(reflect.Int32, intDecoder(32)).
RegisterKind(reflect.Int64, intDecoder(64)).
RegisterKind(reflect.Uint, uintDecoder(64)).
RegisterKind(reflect.Uint8, uintDecoder(bits.UintSize)).
RegisterKind(reflect.Uint, uintDecoder(bits.UintSize)).
RegisterKind(reflect.Uint8, uintDecoder(8)).
RegisterKind(reflect.Uint16, uintDecoder(16)).
RegisterKind(reflect.Uint32, uintDecoder(32)).
RegisterKind(reflect.Uint64, uintDecoder(64)).
RegisterKind(reflect.Float32, floatDecoder(32)).
RegisterKind(reflect.Float64, floatDecoder(64)).
RegisterKind(reflect.String, MapperFunc(func(ctx *DecodeContext, target reflect.Value) error {
err := ctx.Scan.PopValueInto("string", target.Addr().Interface())
return err
return ctx.Scan.PopValueInto("string", target.Addr().Interface())
})).
RegisterKind(reflect.Bool, boolMapper{}).
RegisterKind(reflect.Slice, sliceDecoder(r)).
@@ -248,10 +284,13 @@ func (r *Registry) RegisterDefaults() *Registry {
RegisterType(reflect.TypeOf(time.Time{}), timeDecoder()).
RegisterType(reflect.TypeOf(time.Duration(0)), durationDecoder()).
RegisterType(reflect.TypeOf(&url.URL{}), urlMapper()).
RegisterType(reflect.TypeOf(&os.File{}), fileMapper(r)).
RegisterName("path", pathMapper(r)).
RegisterName("existingfile", existingFileMapper(r)).
RegisterName("existingdir", existingDirMapper(r)).
RegisterName("counter", counterMapper())
RegisterName("counter", counterMapper()).
RegisterName("filecontent", fileContentMapper(r)).
RegisterKind(reflect.Ptr, ptrMapper{r})
}
type boolMapper struct{}
@@ -270,14 +309,14 @@ func (boolMapper) Decode(ctx *DecodeContext, target reflect.Value) error {
target.SetBool(false)
default:
return errors.Errorf("bool value must be true, 1, yes, false, 0 or no but got %q", v)
return fmt.Errorf("bool value must be true, 1, yes, false, 0 or no but got %q", v)
}
case bool:
target.SetBool(v)
default:
return errors.Errorf("expected bool but got %q (%T)", token.Value, token.Value)
return fmt.Errorf("expected bool but got %q (%T)", token.Value, token.Value)
}
} else {
target.SetBool(true)
@@ -288,15 +327,23 @@ func (boolMapper) IsBool() bool { return true }
func durationDecoder() MapperFunc {
return func(ctx *DecodeContext, target reflect.Value) error {
var value string
if err := ctx.Scan.PopValueInto("duration", &value); err != nil {
t, err := ctx.Scan.PopValue("duration")
if err != nil {
return err
}
r, err := time.ParseDuration(value)
if err != nil {
return errors.Errorf("expected duration but got %q: %s", value, err)
var d time.Duration
switch v := t.Value.(type) {
case string:
d, err = time.ParseDuration(v)
if err != nil {
return fmt.Errorf("expected duration but got %q: %v", v, err)
}
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64:
d = reflect.ValueOf(v).Convert(reflect.TypeOf(time.Duration(0))).Interface().(time.Duration) //nolint: forcetypeassert
default:
return fmt.Errorf("expected duration but got %q", v)
}
target.Set(reflect.ValueOf(r))
target.Set(reflect.ValueOf(d))
return nil
}
}
@@ -320,7 +367,7 @@ func timeDecoder() MapperFunc {
}
}
func intDecoder(bits int) MapperFunc { // nolint: dupl
func intDecoder(bits int) MapperFunc { //nolint: dupl
return func(ctx *DecodeContext, target reflect.Value) error {
t, err := ctx.Scan.PopValue("int")
if err != nil {
@@ -331,22 +378,25 @@ func intDecoder(bits int) MapperFunc { // nolint: dupl
case string:
sv = v
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64:
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
sv = fmt.Sprintf("%v", v)
case float32, float64:
sv = fmt.Sprintf("%0.f", v)
default:
return errors.Errorf("expected an int but got %q (%T)", t, t.Value)
return fmt.Errorf("expected an int but got %q (%T)", t, t.Value)
}
n, err := strconv.ParseInt(sv, 10, bits)
n, err := strconv.ParseInt(sv, 0, bits)
if err != nil {
return errors.Errorf("expected a valid %d bit int but got %q", bits, sv)
return fmt.Errorf("expected a valid %d bit int but got %q", bits, sv)
}
target.SetInt(n)
return nil
}
}
func uintDecoder(bits int) MapperFunc { // nolint: dupl
func uintDecoder(bits int) MapperFunc { //nolint: dupl
return func(ctx *DecodeContext, target reflect.Value) error {
t, err := ctx.Scan.PopValue("uint")
if err != nil {
@@ -357,15 +407,18 @@ func uintDecoder(bits int) MapperFunc { // nolint: dupl
case string:
sv = v
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64:
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
sv = fmt.Sprintf("%v", v)
case float32, float64:
sv = fmt.Sprintf("%0.f", v)
default:
return errors.Errorf("expected an int but got %q (%T)", t, t.Value)
return fmt.Errorf("expected an int but got %q (%T)", t, t.Value)
}
n, err := strconv.ParseUint(sv, 10, bits)
n, err := strconv.ParseUint(sv, 0, bits)
if err != nil {
return errors.Errorf("expected a valid %d bit uint but got %q", bits, sv)
return fmt.Errorf("expected a valid %d bit uint but got %q", bits, sv)
}
target.SetUint(n)
return nil
@@ -382,7 +435,7 @@ func floatDecoder(bits int) MapperFunc {
case string:
n, err := strconv.ParseFloat(v, bits)
if err != nil {
return errors.Errorf("expected a float but got %q (%T)", t, t.Value)
return fmt.Errorf("expected a float but got %q (%T)", t, t.Value)
}
target.SetFloat(n)
@@ -393,10 +446,10 @@ func floatDecoder(bits int) MapperFunc {
target.SetFloat(v)
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
target.Set(reflect.ValueOf(v))
target.SetFloat(reflect.ValueOf(v).Convert(reflect.TypeOf(float64(0))).Float())
default:
return errors.Errorf("expected an int but got %q (%T)", t, t.Value)
return fmt.Errorf("expected a float but got %q (%T)", t, t.Value)
}
return nil
}
@@ -408,32 +461,32 @@ func mapDecoder(r *Registry) MapperFunc {
target.Set(reflect.MakeMap(target.Type()))
}
el := target.Type()
sep := ctx.Value.Tag.MapSep
mapsep := ctx.Value.Tag.MapSep
var childScanner *Scanner
if ctx.Value.Flag != nil {
t := ctx.Scan.Pop()
// If decoding a flag, we need an argument.
// If decoding a flag, we need an value.
if t.IsEOL() {
return errors.Errorf("unexpected EOL")
return fmt.Errorf("missing value, expecting \"<key>=<value>%c...\"", mapsep)
}
switch v := t.Value.(type) {
case string:
childScanner = Scan(SplitEscaped(v, sep)...)
childScanner = ScanAsType(t.Type, SplitEscaped(v, mapsep)...)
case []map[string]interface{}:
case []map[string]any:
for _, m := range v {
err := jsonTranscode(m, target.Addr().Interface())
if err != nil {
return errors.WithStack(err)
return err
}
}
return nil
case map[string]interface{}:
case map[string]any:
return jsonTranscode(v, target.Addr().Interface())
default:
return errors.Errorf("invalid map value %q (of type %T)", t, t.Value)
return fmt.Errorf("invalid map value %q (of type %T)", t, t.Value)
}
} else {
tokens := ctx.Scan.PopWhile(func(t Token) bool { return t.IsValue() })
@@ -447,7 +500,7 @@ func mapDecoder(r *Registry) MapperFunc {
}
parts := strings.SplitN(token, "=", 2)
if len(parts) != 2 {
return errors.Errorf("expected \"<key>=<value>\" but got %q", token)
return fmt.Errorf("expected \"<key>=<value>\" but got %q", token)
}
key, value := parts[0], parts[1]
@@ -455,23 +508,23 @@ func mapDecoder(r *Registry) MapperFunc {
if typ := ctx.Value.Tag.Type; typ != "" {
parts := strings.Split(typ, ":")
if len(parts) != 2 {
return errors.Errorf("type:\"\" on map field must be in the form \"[<keytype>]:[<valuetype>]\"")
return errors.New("type:\"\" on map field must be in the form \"[<keytype>]:[<valuetype>]\"")
}
keyTypeName, valueTypeName = parts[0], parts[1]
}
keyScanner := Scan(key)
keyScanner := ScanAsType(FlagValueToken, key)
keyDecoder := r.ForNamedType(keyTypeName, el.Key())
keyValue := reflect.New(el.Key()).Elem()
if err := keyDecoder.Decode(ctx.WithScanner(keyScanner), keyValue); err != nil {
return errors.Errorf("invalid map key %q", key)
return fmt.Errorf("invalid map key %q", key)
}
valueScanner := Scan(value)
valueScanner := ScanAsType(FlagValueToken, value)
valueDecoder := r.ForNamedType(valueTypeName, el.Elem())
valueValue := reflect.New(el.Elem()).Elem()
if err := valueDecoder.Decode(ctx.WithScanner(valueScanner), valueValue); err != nil {
return errors.Errorf("invalid map value %q", value)
return fmt.Errorf("invalid map value %q", value)
}
target.SetMapIndex(keyValue, valueValue)
@@ -487,19 +540,23 @@ func sliceDecoder(r *Registry) MapperFunc {
var childScanner *Scanner
if ctx.Value.Flag != nil {
t := ctx.Scan.Pop()
// If decoding a flag, we need an argument.
// If decoding a flag, we need a value.
tail := ""
if sep != -1 {
tail += string(sep) + "..."
}
if t.IsEOL() {
return errors.Errorf("unexpected EOL")
return fmt.Errorf("missing value, expecting \"<arg>%s\"", tail)
}
switch v := t.Value.(type) {
case string:
childScanner = Scan(SplitEscaped(v, sep)...)
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 {
@@ -508,13 +565,13 @@ func sliceDecoder(r *Registry) MapperFunc {
}
childDecoder := r.ForNamedType(ctx.Value.Tag.Type, el)
if childDecoder == nil {
return errors.Errorf("no mapper for element type of %s", target.Type())
return fmt.Errorf("no mapper for element type of %s", target.Type())
}
for !childScanner.Peek().IsEOL() {
childValue := reflect.New(el).Elem()
err := childDecoder.Decode(ctx.WithScanner(childScanner), childValue)
if err != nil {
return errors.WithStack(err)
return err
}
target.Set(reflect.Append(target, childValue))
}
@@ -527,33 +584,88 @@ func pathMapper(r *Registry) MapperFunc {
if target.Kind() == reflect.Slice {
return sliceDecoder(r)(ctx, target)
}
originalTarget := target
if target.Kind() == reflect.Ptr && target.Elem().Kind() == reflect.String {
if target.IsNil() {
return nil
}
target = target.Elem()
}
if target.Kind() != reflect.String {
return errors.Errorf("\"path\" type must be applied to a string not %s", target.Type())
return fmt.Errorf("\"path\" type must be applied to a string not %s", target.Type())
}
var path string
err := ctx.Scan.PopValueInto("file", &path)
if err != nil {
return err
}
path = ExpandPath(path)
// Skip if path with default is explicitly set to "". For the current directory use ".".
if ctx.Value.HasDefault && path == "" {
if originalTarget.Kind() == reflect.Ptr {
originalTarget.Set(reflect.Zero(originalTarget.Type()))
}
return nil
}
if path != "-" {
path = ExpandPath(path)
}
target.SetString(path)
return nil
}
}
func fileMapper(r *Registry) MapperFunc {
return func(ctx *DecodeContext, target reflect.Value) error {
if target.Kind() == reflect.Slice {
return sliceDecoder(r)(ctx, target)
}
var path string
err := ctx.Scan.PopValueInto("file", &path)
if err != nil {
return err
}
// Skip if value was already set to avoid opening the default file when an explicit value was provided
if ctx.Value.HasDefault && ctx.Value.Set {
return nil
}
var file *os.File
if path == "-" {
file = os.Stdin
} else {
path = ExpandPath(path)
file, err = os.Open(path) //nolint: gosec
if err != nil {
return err
}
}
target.Set(reflect.ValueOf(file))
return nil
}
}
func existingFileMapper(r *Registry) MapperFunc {
return func(ctx *DecodeContext, target reflect.Value) error {
if target.Kind() == reflect.Slice {
return sliceDecoder(r)(ctx, target)
}
if target.Kind() != reflect.String {
return errors.Errorf("\"existingfile\" type must be applied to a string not %s", target.Type())
return fmt.Errorf("\"existingfile\" type must be applied to a string not %s", target.Type())
}
var path string
err := ctx.Scan.PopValueInto("file", &path)
if err != nil {
return err
}
if !ctx.Value.Active || (ctx.Value.Set && ctx.Value.Target.Type() == target.Type()) {
// early return to avoid checking extra files that may not exist;
// this hack only works because the value provided on the cli is
// checked before the default value is checked (if default is set).
return nil
}
if path != "-" {
path = ExpandPath(path)
stat, err := os.Stat(path)
@@ -561,7 +673,7 @@ func existingFileMapper(r *Registry) MapperFunc {
return err
}
if stat.IsDir() {
return errors.Errorf("%q exists but is a directory", path)
return fmt.Errorf("%q exists but is a directory", path)
}
}
target.SetString(path)
@@ -575,26 +687,106 @@ func existingDirMapper(r *Registry) MapperFunc {
return sliceDecoder(r)(ctx, target)
}
if target.Kind() != reflect.String {
return errors.Errorf("\"existingdir\" must be applied to a string not %s", target.Type())
return fmt.Errorf("\"existingdir\" must be applied to a string not %s", target.Type())
}
var path string
err := ctx.Scan.PopValueInto("file", &path)
if err != nil {
return err
}
if !ctx.Value.Active || (ctx.Value.Set && ctx.Value.Target.Type() == target.Type()) {
// early return to avoid checking extra dirs that may not exist;
// this hack only works because the value provided on the cli is
// checked before the default value is checked (if default is set).
return nil
}
path = ExpandPath(path)
stat, err := os.Stat(path)
if err != nil {
return err
}
if !stat.IsDir() {
return errors.Errorf("%q exists but is not a directory", path)
return fmt.Errorf("%q exists but is not a directory", path)
}
target.SetString(path)
return nil
}
}
func fileContentMapper(r *Registry) MapperFunc {
return func(ctx *DecodeContext, target reflect.Value) error {
if target.Kind() != reflect.Slice || target.Type().Elem().Kind() != reflect.Uint8 {
return fmt.Errorf("\"filecontent\" must be applied to []byte not %s", target.Type())
}
var path string
err := ctx.Scan.PopValueInto("file", &path)
if err != nil {
return err
}
if !ctx.Value.Active || ctx.Value.Set {
// early return to avoid checking extra dirs that may not exist;
// this hack only works because the value provided on the cli is
// checked before the default value is checked (if default is set).
return nil
}
var data []byte
if path != "-" {
path = ExpandPath(path)
data, err = os.ReadFile(path) //nolint:gosec
} else {
data, err = io.ReadAll(os.Stdin)
}
if err != nil {
if info, statErr := os.Stat(path); statErr == nil && info.IsDir() {
return fmt.Errorf("%q exists but is a directory: %w", path, err)
}
return err
}
target.SetBytes(data)
return nil
}
}
type ptrMapper struct {
r *Registry
}
var _ BoolMapperExt = (*ptrMapper)(nil)
// IsBoolFromValue implements BoolMapperExt
func (p ptrMapper) IsBoolFromValue(target reflect.Value) bool {
elem := reflect.New(target.Type().Elem()).Elem()
nestedMapper := p.r.ForValue(elem)
if nestedMapper == nil {
return false
}
if bm, ok := nestedMapper.(BoolMapper); ok && bm.IsBool() {
return true
}
if bm, ok := nestedMapper.(BoolMapperExt); ok && bm.IsBoolFromValue(target) {
return true
}
return target.Kind() == reflect.Ptr && target.Type().Elem().Kind() == reflect.Bool
}
func (p ptrMapper) Decode(ctx *DecodeContext, target reflect.Value) error {
elem := reflect.New(target.Type().Elem()).Elem()
nestedMapper := p.r.ForValue(elem)
if nestedMapper == nil {
return fmt.Errorf("cannot find mapper for %v", target.Type().Elem().String())
}
err := nestedMapper.Decode(ctx, elem)
if err != nil {
return err
}
target.Set(elem.Addr())
return nil
}
func counterMapper() MapperFunc {
return func(ctx *DecodeContext, target reflect.Value) error {
if ctx.Scan.Peek().Type == FlagValueToken {
@@ -602,19 +794,32 @@ func counterMapper() MapperFunc {
if err != nil {
return err
}
var n int64
switch v := t.Value.(type) {
case string:
n, err := strconv.ParseInt(v, 10, 64)
n, err = strconv.ParseInt(v, 10, 64)
if err != nil {
return errors.Errorf("expected a counter but got %q (%T)", t, t.Value)
return fmt.Errorf("expected a counter but got %q (%T)", t, t.Value)
}
target.SetInt(n)
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
target.Set(reflect.ValueOf(v))
n = reflect.ValueOf(v).Convert(reflect.TypeOf(int64(0))).Int()
default:
return errors.Errorf("expected a counter but got %q (%T)", t, t.Value)
return fmt.Errorf("expected a counter but got %q (%T)", t, t.Value)
}
// Assign by the target's kind, like the increment path below, so a
// counter declared as a uint or float field doesn't panic.
switch target.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
target.SetInt(n)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
target.SetUint(uint64(n)) //nolint:gosec // a counter value is small and non-negative
case reflect.Float32, reflect.Float64:
target.SetFloat(float64(n))
default:
return fmt.Errorf("type:\"counter\" must be used with a numeric field")
}
return nil
}
@@ -630,7 +835,7 @@ func counterMapper() MapperFunc {
target.SetFloat(target.Float() + 1)
default:
return errors.Errorf("type:\"counter\" must be used with a numeric field")
return fmt.Errorf("type:\"counter\" must be used with a numeric field")
}
return nil
}
@@ -645,7 +850,7 @@ func urlMapper() MapperFunc {
}
url, err := url.Parse(urlStr)
if err != nil {
return errors.WithStack(err)
return err
}
target.Set(reflect.ValueOf(url))
return nil
@@ -656,19 +861,22 @@ func urlMapper() MapperFunc {
//
// It differs from strings.Split() in that the separator can exist in a field by escaping it with a \. eg.
//
// SplitEscaped(`hello\,there,bob`, ',') == []string{"hello,there", "bob"}
// SplitEscaped(`hello\,there,bob`, ',') == []string{"hello,there", "bob"}
func SplitEscaped(s string, sep rune) (out []string) {
if sep == -1 {
return []string{s}
}
escaped := false
token := ""
for _, ch := range s {
for i, ch := range s {
switch {
case escaped:
if ch != sep {
token += `\`
}
token += string(ch)
escaped = false
case ch == '\\':
case ch == '\\' && i < len(s)-1:
escaped = true
case ch == sep && !escaped:
out = append(out, token)
@@ -686,19 +894,46 @@ func SplitEscaped(s string, sep rune) (out []string) {
// JoinEscaped joins a slice of strings on sep, but also escapes any instances of sep in the fields with \. eg.
//
// JoinEscaped([]string{"hello,there", "bob"}, ',') == `hello\,there,bob`
// JoinEscaped([]string{"hello,there", "bob"}, ',') == `hello\,there,bob`
func JoinEscaped(s []string, sep rune) string {
escaped := []string{}
for _, e := range s {
escaped = append(escaped, strings.Replace(e, string(sep), `\`+string(sep), -1))
escaped = append(escaped, strings.ReplaceAll(e, string(sep), `\`+string(sep)))
}
return strings.Join(escaped, string(sep))
}
// NamedFileContentFlag is a flag value that loads a file's contents and filename into its value.
type NamedFileContentFlag struct {
Filename string
Contents []byte
}
func (f *NamedFileContentFlag) Decode(ctx *DecodeContext) error { //nolint: revive
var filename string
err := ctx.Scan.PopValueInto("filename", &filename)
if err != nil {
return err
}
// This allows unsetting of file content flags.
if filename == "" {
*f = NamedFileContentFlag{}
return nil
}
filename = ExpandPath(filename)
data, err := os.ReadFile(filename) //nolint: gosec
if err != nil {
return fmt.Errorf("failed to open %q: %v", filename, err)
}
f.Contents = data
f.Filename = filename
return nil
}
// FileContentFlag is a flag value that loads a file's contents into its value.
type FileContentFlag []byte
func (f *FileContentFlag) Decode(ctx *DecodeContext) error { // nolint: golint
func (f *FileContentFlag) Decode(ctx *DecodeContext) error { //nolint: revive
var filename string
err := ctx.Scan.PopValueInto("filename", &filename)
if err != nil {
@@ -710,18 +945,21 @@ func (f *FileContentFlag) Decode(ctx *DecodeContext) error { // nolint: golint
return nil
}
filename = ExpandPath(filename)
data, err := ioutil.ReadFile(filename) // nolint: gosec
data, err := os.ReadFile(filename) //nolint: gosec
if err != nil {
return errors.Errorf("failed to open %q: %s", filename, err)
return fmt.Errorf("failed to open %q: %v", filename, err)
}
*f = data
return nil
}
func jsonTranscode(in, out interface{}) error {
func jsonTranscode(in, out any) error {
data, err := json.Marshal(in)
if err != nil {
return errors.WithStack(err)
return err
}
return errors.Wrapf(json.Unmarshal(data, out), "%#v -> %T", in, out)
if err = json.Unmarshal(data, out); err != nil {
return fmt.Errorf("%#v -> %T: %v", in, out, err)
}
return nil
}
+155 -60
View File
@@ -7,8 +7,6 @@ import (
"reflect"
"strconv"
"strings"
"github.com/pkg/errors"
)
// A Visitable component in the model.
@@ -41,18 +39,22 @@ const (
// Node is a branch in the CLI. ie. a command or positional argument.
type Node struct {
Type NodeType
Parent *Node
Name string
Help string // Short help displayed in summaries.
Detail string // Detailed help displayed when describing command/arg alone.
Group string
Hidden bool
Flags []*Flag
Positional []*Positional
Children []*Node
Target reflect.Value // Pointer to the value in the grammar that this Node is associated with.
Tag *Tag
Type NodeType
Parent *Node
Name string
Help string // Short help displayed in summaries.
Detail string // Detailed help displayed when describing command/arg alone.
Group *Group
Hidden bool
Flags []*Flag
Positional []*Positional
Children []*Node
DefaultCmd *Node
Target reflect.Value // Pointer to the value in the grammar that this Node is associated with.
Tag *Tag
Aliases []string
Passthrough bool // Set to true to stop flag parsing when encountered.
Active bool // Denotes the node is part of an active branch in the CLI.
Argument *Value // Populated when Type is ArgumentNode.
}
@@ -67,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")
@@ -97,6 +99,7 @@ func (n *Node) AllFlags(hide bool) (out [][]*Flag) {
group := []*Flag{}
for _, flag := range n.Flags {
if !hide || !flag.Hidden {
flag.Active = true
group = append(group, flag)
}
}
@@ -127,6 +130,20 @@ func (n *Node) Leaves(hide bool) (out []*Node) {
return
}
// Values returns the values directly attached to this node: its argument
// value, flag values and positionals.
func (n *Node) Values() []*Value {
values := []*Value{}
if n.Argument != nil {
values = append(values, n.Argument)
}
for _, flag := range n.Flags {
values = append(values, flag.Value)
}
values = append(values, n.Positional...)
return values
}
// Depth of the command from the application root.
func (n *Node) Depth() int {
depth := 0
@@ -145,14 +162,33 @@ func (n *Node) Summary() string {
summary += " " + flags
}
args := []string{}
optional := 0
for _, arg := range n.Positional {
args = append(args, arg.Summary())
argSummary := arg.Summary()
if arg.Tag.Optional {
optional++
argSummary = strings.TrimRight(argSummary, "]")
}
args = append(args, argSummary)
}
if len(args) != 0 {
summary += " " + strings.Join(args, " ")
summary += " " + strings.Join(args, " ") + strings.Repeat("]", optional)
} else if len(n.Children) > 0 {
summary += " <command>"
}
allFlags := n.Flags
if n.Parent != nil {
allFlags = append(allFlags, n.Parent.Flags...)
}
for _, flag := range allFlags {
if _, ok := flag.Target.Interface().(helpFlag); ok {
continue
}
if !flag.Required {
summary += " [flags]"
break
}
}
return summary
}
@@ -196,27 +232,48 @@ func (n *Node) Path() (out string) {
switch n.Type {
case CommandNode:
out += " " + n.Name
if len(n.Aliases) > 0 {
out += fmt.Sprintf(" (%s)", strings.Join(n.Aliases, ","))
}
case ArgumentNode:
out += " " + "<" + n.Name + ">"
default:
}
return strings.TrimSpace(out)
}
// ClosestGroup finds the first non-nil group in this node and its ancestors.
func (n *Node) ClosestGroup() *Group {
switch {
case n.Group != nil:
return n.Group
case n.Parent != nil:
return n.Parent.ClosestGroup()
default:
return nil
}
}
// A Value is either a flag or a variable positional argument.
type Value struct {
Flag *Flag // Nil if positional argument.
Name string
Help string
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).
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.
@@ -229,6 +286,16 @@ func (v *Value) EnumMap() map[string]bool {
return out
}
// EnumSlice returns a slice of the enums in this value.
func (v *Value) EnumSlice() []string {
parts := strings.Split(v.Enum, ",")
out := make([]string, len(parts))
for i, part := range parts {
out[i] = strings.TrimSpace(part)
}
return out
}
// ShortSummary returns a human-readable summary of the value, not including any placeholders/defaults.
func (v *Value) ShortSummary() string {
if v.Flag != nil {
@@ -279,27 +346,28 @@ func (v *Value) IsMap() bool {
// IsBool returns true if the underlying value is a boolean.
func (v *Value) IsBool() bool {
if m, ok := v.Mapper.(BoolMapperExt); ok && m.IsBoolFromValue(v.Target) {
return true
}
if m, ok := v.Mapper.(BoolMapper); ok && m.IsBool() {
return true
}
return v.Target.Kind() == reflect.Bool
}
// IsCounter returns true if the value is a counter.
func (v *Value) IsCounter() bool {
return v.Tag.Type == "counter"
}
// Parse tokens into value, parse, and validate, but do not write to the field.
func (v *Value) Parse(scan *Scanner, target reflect.Value) (err error) {
defer func() {
if rerr := recover(); rerr != nil {
switch rerr := rerr.(type) {
case Error:
err = errors.Wrap(rerr, v.ShortSummary())
default:
panic(fmt.Sprintf("mapper %T failed to apply to %s: %s", v.Mapper, v.Summary(), rerr))
}
}
}()
if target.Kind() == reflect.Ptr && target.IsNil() {
target.Set(reflect.New(target.Type().Elem()))
}
err = v.Mapper.Decode(&DecodeContext{Value: v, Scan: scan}, target)
if err != nil {
return errors.Wrap(err, v.ShortSummary())
return fmt.Errorf("%s: %w", v.ShortSummary(), err)
}
v.Set = true
return nil
@@ -326,17 +394,20 @@ func (v *Value) ApplyDefault() error {
// Does not include resolvers.
func (v *Value) Reset() error {
v.Target.Set(reflect.Zero(v.Target.Type()))
if v.Tag.Env != "" {
envar := os.Getenv(v.Tag.Env)
if envar != "" {
err := v.Parse(ScanFromTokens(Token{Type: FlagValueToken, Value: envar}), v.Target)
if err != nil {
return fmt.Errorf("%s (from envar %s=%q)", err, v.Tag.Env, envar)
if len(v.Tag.Envs) != 0 {
for _, env := range v.Tag.Envs {
envar, ok := os.LookupEnv(env)
// Parse the first non-empty ENV in the list
if ok {
err := v.Parse(ScanFromTokens(Token{Type: FlagValueToken, Value: envar}), v.Target)
if err != nil {
return fmt.Errorf("%s (from envar %s=%q)", err, env, envar)
}
return nil
}
return nil
}
}
if v.Default != "" {
if v.HasDefault {
return v.Parse(ScanFromTokens(Token{Type: FlagValueToken, Value: v.Default}), v.Target)
}
return nil
@@ -350,12 +421,15 @@ type Positional = Value
// A Flag represents a command-line flag.
type Flag struct {
*Value
Group string // Logical grouping when displaying. May also be used by configuration loaders to group options logically.
Xor string
Group *Group // Logical grouping when displaying. May also be used by configuration loaders to group options logically.
Xor []string
And []string
PlaceHolder string
Env string
Envs []string
Aliases []string
Short rune
Hidden bool
Negated bool
}
func (f *Flag) String() string {
@@ -363,7 +437,7 @@ func (f *Flag) String() string {
if f.Short != 0 {
out = fmt.Sprintf("-%c, %s", f.Short, out)
}
if !f.IsBool() {
if !f.IsBool() && !f.IsCounter() {
out += "=" + f.FormatPlaceHolder()
}
return out
@@ -371,28 +445,46 @@ func (f *Flag) String() string {
// FormatPlaceHolder formats the placeholder string for a Flag.
func (f *Flag) FormatPlaceHolder() string {
placeholderHelper, ok := f.Value.Mapper.(PlaceHolderProvider)
if ok {
return placeholderHelper.PlaceHolder(f)
}
tail := ""
if f.Value.IsSlice() && f.Value.Tag.Sep != -1 {
if f.Value.IsSlice() && f.Value.Tag.Sep != -1 && f.Tag.Type == "" {
tail += string(f.Value.Tag.Sep) + "..."
}
if f.Default != "" {
if f.PlaceHolder != "" {
return f.PlaceHolder + tail
}
if f.HasDefault {
if f.Value.Target.Kind() == reflect.String {
return strconv.Quote(f.Default) + tail
}
return f.Default + tail
}
if f.PlaceHolder != "" {
return f.PlaceHolder + tail
}
if f.Value.IsMap() {
if f.Value.Tag.MapSep != -1 {
if f.Value.Tag.MapSep != -1 && f.Tag.Type == "" {
tail = string(f.Value.Tag.MapSep) + "..."
}
return "KEY=VALUE" + tail
}
if f.Tag != nil && f.Tag.TypeName != "" {
return strings.ToUpper(dashedString(f.Tag.TypeName)) + tail
}
return strings.ToUpper(f.Name) + tail
}
// Group holds metadata about a command or flag group used when printing help.
type Group struct {
// Key is the `group` field tag value used to identify this group.
Key string
// Title is displayed above the grouped items.
Title string
// Description is optional and displayed under the Title when non empty.
// It can be used to introduce the group's purpose to the user.
Description string
}
// This is directly from the Go 1.13 source code.
func reflectValueIsZero(v reflect.Value) bool {
switch v.Kind() {
@@ -428,6 +520,9 @@ func reflectValueIsZero(v reflect.Value) bool {
default:
// This should never happens, but will act as a safeguard for
// later, as a default value doesn't makes sense here.
panic(&reflect.ValueError{"reflect.Value.IsZero", v.Kind()})
panic(&reflect.ValueError{
Method: "reflect.Value.IsZero",
Kind: v.Kind(),
})
}
}
+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
}
}
+322 -44
View File
@@ -1,14 +1,15 @@
package kong
import (
"errors"
"fmt"
"io"
"os"
"os/user"
"path/filepath"
"reflect"
"regexp"
"strings"
"github.com/pkg/errors"
)
// An Option applies optional changes to the Kong application.
@@ -19,7 +20,7 @@ type Option interface {
// OptionFunc is function that adheres to the Option interface.
type OptionFunc func(k *Kong) error
func (o OptionFunc) Apply(k *Kong) error { return o(k) } // nolint: golint
func (o OptionFunc) Apply(k *Kong) error { return o(k) } //nolint: revive
// Vars sets the variables to use for interpolation into help strings and default values.
//
@@ -54,6 +55,61 @@ 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
}
// Embed a struct into the root of the CLI.
//
// "strct" must be a pointer to a structure.
func Embed(strct any, tags ...string) Option {
t := reflect.TypeOf(strct)
if t.Kind() != reflect.Ptr || t.Elem().Kind() != reflect.Struct {
panic("kong: Embed() must be called with a pointer to a struct")
}
return OptionFunc(func(k *Kong) error {
k.embedded = append(k.embedded, embedded{strct, tags})
return nil
})
}
type dynamicCommand struct {
name string
help string
group string
tags []string
cmd any
}
// DynamicCommand registers a dynamically constructed command with the root of the CLI.
//
// This is useful for command-line structures that are extensible via user-provided plugins.
//
// "tags" is a list of extra tag strings to parse, in the form <key>:"<value>".
func DynamicCommand(name, help, group string, cmd any, tags ...string) Option {
return OptionFunc(func(k *Kong) error {
k.dynamicCommands = append(k.dynamicCommands, &dynamicCommand{
name: name,
help: help,
group: group,
cmd: cmd,
tags: tags,
})
return nil
})
}
// NoDefaultHelp disables the default help flags.
func NoDefaultHelp() Option {
return OptionFunc(func(k *Kong) error {
@@ -66,31 +122,59 @@ func NoDefaultHelp() Option {
// parsing occurs.
//
// This is useful for, e.g., adding short options to flags, updating help, etc.
func (k *Kong) PostBuild(fn func(*Kong) error) Option {
func PostBuild(fn func(*Kong) error) Option {
return OptionFunc(func(k *Kong) error {
k.postBuildOptions = append(k.postBuildOptions, OptionFunc(fn))
return nil
})
}
// 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 OptionFunc(func(k *Kong) error {
k.postBuildOptions = append(k.postBuildOptions, OptionFunc(func(k *Kong) error {
k.Model.Name = name
return nil
}))
return PostBuild(func(k *Kong) error {
k.Model.Name = name
return nil
})
}
// Description sets the application description.
func Description(description string) Option {
return OptionFunc(func(k *Kong) error {
k.postBuildOptions = append(k.postBuildOptions, OptionFunc(func(k *Kong) error {
k.Model.Help = description
return nil
}))
return PostBuild(func(k *Kong) error {
k.Model.Help = description
return nil
})
}
@@ -112,7 +196,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
@@ -143,11 +227,11 @@ func Writers(stdout, stderr io.Writer) Option {
//
// There are two hook points:
//
// BeforeApply(...) error
// AfterApply(...) error
// BeforeApply(...) error
// AfterApply(...) error
//
// Called before validation/assignment, and immediately after validation/assignment, respectively.
func Bind(args ...interface{}) Option {
func Bind(args ...any) Option {
return OptionFunc(func(k *Kong) error {
k.bindings.add(args...)
return nil
@@ -156,37 +240,50 @@ func Bind(args ...interface{}) Option {
// BindTo allows binding of implementations to interfaces.
//
// BindTo(impl, (*iface)(nil))
func BindTo(impl, iface interface{}) Option {
// BindTo(impl, (*iface)(nil))
func BindTo(impl, iface any) Option {
return OptionFunc(func(k *Kong) error {
valueOf := reflect.ValueOf(impl)
k.bindings[reflect.TypeOf(iface).Elem()] = func() (reflect.Value, error) { return valueOf, nil }
k.bindings.addTo(impl, iface)
return nil
})
}
// BindToProvider allows binding of provider functions.
// BindFor allows binding of implementations to the compile-time type. It
// is equivalent to BindTo(v, (*T)(nil)), but can usually infer the
// correct type T.
func BindFor[T any](v T) Option {
return BindTo(v, (*T)(nil))
}
// 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 {
pv := reflect.ValueOf(provider)
t := pv.Type()
if t.Kind() != reflect.Func || t.NumIn() != 0 || t.NumOut() != 2 || t.Out(1) != reflect.TypeOf((*error)(nil)).Elem() {
return errors.Errorf("%T must be a function with the signature func()(T, error)", provider)
}
rt := pv.Type().Out(0)
k.bindings[rt] = func() (reflect.Value, error) {
out := pv.Call(nil)
errv := out[1]
var err error
if !errv.IsNil() {
err = errv.Interface().(error)
}
return out[0], err
}
return nil
return k.bindings.addProvider(provider, 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 */)
})
}
@@ -198,7 +295,20 @@ func Help(help HelpPrinter) Option {
})
}
// ShortHelp configures the short usage message.
//
// It should be used together with kong.ShortUsageOnError() to display a
// custom short usage message on errors.
func ShortHelp(shortHelp HelpPrinter) Option {
return OptionFunc(func(k *Kong) error {
k.shortHelp = shortHelp
return nil
})
}
// HelpFormatter configures how the help text is formatted.
//
// Deprecated: Use ValueFormatter() instead.
func HelpFormatter(helpFormatter HelpValueFormatter) Option {
return OptionFunc(func(k *Kong) error {
k.helpFormatter = helpFormatter
@@ -206,6 +316,14 @@ func HelpFormatter(helpFormatter HelpValueFormatter) Option {
})
}
// ValueFormatter configures how the help text is formatted.
func ValueFormatter(helpFormatter HelpValueFormatter) Option {
return OptionFunc(func(k *Kong) error {
k.helpFormatter = helpFormatter
return nil
})
}
// ConfigureHelp sets the HelpOptions to use for printing help.
func ConfigureHelp(options HelpOptions) Option {
return OptionFunc(func(k *Kong) error {
@@ -214,10 +332,75 @@ func ConfigureHelp(options HelpOptions) Option {
})
}
// AutoGroup automatically assigns groups to flags.
func AutoGroup(format func(parent Visitable, flag *Flag) *Group) Option {
return PostBuild(func(kong *Kong) error {
parents := []Visitable{kong.Model}
return Visit(kong.Model, func(node Visitable, next Next) error {
if flag, ok := node.(*Flag); ok && flag.Group == nil {
flag.Group = format(parents[len(parents)-1], flag)
}
parents = append(parents, node)
defer func() { parents = parents[:len(parents)-1] }()
return next(nil)
})
})
}
// Groups associates `group` field tags with group metadata.
//
// This option is used to simplify Kong tags while providing
// rich group information such as title and optional description.
//
// Each key in the "groups" map corresponds to the value of a
// `group` Kong tag, while the first line of the value will be
// the title, and subsequent lines if any will be the description of
// the group.
//
// See also ExplicitGroups for a more structured alternative.
type Groups map[string]string
func (g Groups) Apply(k *Kong) error { //nolint: revive
for key, info := range g {
lines := strings.Split(info, "\n")
title := strings.TrimSpace(lines[0])
description := ""
if len(lines) > 1 {
description = strings.TrimSpace(strings.Join(lines[1:], "\n"))
}
k.groups = append(k.groups, Group{
Key: key,
Title: title,
Description: description,
})
}
return nil
}
// ExplicitGroups associates `group` field tags with their metadata.
//
// It can be used to provide a title or header to a command or flag group.
func ExplicitGroups(groups []Group) Option {
return OptionFunc(func(k *Kong) error {
k.groups = groups
return nil
})
}
// UsageOnError configures Kong to display context-sensitive usage if FatalIfErrorf is called with an error.
func UsageOnError() Option {
return OptionFunc(func(k *Kong) error {
k.usageOnError = true
k.usageOnError = fullUsage
return nil
})
}
// ShortUsageOnError configures Kong to display context-sensitive short
// usage if FatalIfErrorf is called with an error. The default short
// usage message can be overridden with kong.ShortHelp(...).
func ShortUsageOnError() Option {
return OptionFunc(func(k *Kong) error {
k.usageOnError = shortUsage
return nil
})
}
@@ -238,6 +421,31 @@ func Resolvers(resolvers ...Resolver) Option {
})
}
// IgnoreFields will cause kong.New() to skip field names that match any
// of the provided regex patterns. This is useful if you are not able to add a
// kong="-" struct tag to a struct/element before the call to New.
//
// Example: When referencing protoc generated structs, you will likely want to
// ignore/skip XXX_* fields.
func IgnoreFields(regexes ...string) Option {
return OptionFunc(func(k *Kong) error {
for _, r := range regexes {
if r == "" {
return errors.New("regex input cannot be empty")
}
re, err := regexp.Compile(r)
if err != nil {
return fmt.Errorf("unable to compile regex: %v", err)
}
k.ignoreFields = append(k.ignoreFields, re)
}
return nil
})
}
// ConfigurationLoader is a function that builds a resolver from a file.
type ConfigurationLoader func(r io.Reader) (Resolver, error)
@@ -252,12 +460,19 @@ func Configuration(loader ConfigurationLoader, paths ...string) Option {
return OptionFunc(func(k *Kong) error {
k.loader = loader
for _, path := range paths {
if _, err := os.Stat(ExpandPath(path)); os.IsNotExist(err) {
continue
f, err := os.Open(ExpandPath(path))
if err != nil {
if os.IsNotExist(err) || os.IsPermission(err) {
continue
}
return err
}
f.Close()
resolver, err := k.LoadConfig(path)
if err != nil {
return errors.Wrap(err, path)
return fmt.Errorf("%s: %v", path, err)
}
if resolver != nil {
k.resolvers = append(k.resolvers, resolver)
@@ -287,3 +502,66 @@ func ExpandPath(path string) string {
}
return abspath
}
func siftStrings(ss []string, filter func(s string) bool) []string {
i := 0
ss = append([]string(nil), ss...)
for _, s := range ss {
if filter(s) {
ss[i] = s
i++
}
}
return ss[0:i]
}
// DefaultEnvars option inits environment names for flags.
// The name will not generate if tag "env" is "-".
// Predefined environment variables are skipped.
//
// For example:
//
// --some.value -> PREFIX_SOME_VALUE
func DefaultEnvars(prefix string) Option {
processFlag := func(flag *Flag) {
switch env := flag.Envs; {
case flag.Name == "help":
return
case len(env) == 1 && env[0] == "-":
flag.Envs = nil
flag.Value.Tag.Envs = nil
return
case len(env) > 0:
return
}
replacer := strings.NewReplacer("-", "_", ".", "_")
names := append([]string{prefix}, camelCase(replacer.Replace(flag.Name))...)
names = siftStrings(names, func(s string) bool { return !(s == "_" || strings.TrimSpace(s) == "") })
name := strings.ToUpper(strings.Join(names, "_"))
flag.Envs = append(flag.Envs, name)
flag.Value.Tag.Envs = append(flag.Value.Tag.Envs, name)
}
var processNode func(node *Node)
processNode = func(node *Node) {
for _, flag := range node.Flags {
processFlag(flag)
}
for _, node := range node.Children {
processNode(node)
}
}
return PostBuild(func(k *Kong) error {
processNode(k.Model.Node)
return nil
})
}
// FlagNamer allows you to override the default kebab-case automated flag name generation.
func FlagNamer(namer func(fieldName string) string) Option {
return OptionFunc(func(k *Kong) error {
k.flagNamer = namer
return nil
})
}
+18
View File
@@ -0,0 +1,18 @@
{
$schema: "https://docs.renovatebot.com/renovate-schema.json",
extends: [
"config:recommended",
":semanticCommits",
":semanticCommitTypeAll(chore)",
":semanticCommitScope(deps)",
"group:allNonMajor",
"schedule:earlyMondays", // Run once a week.
],
packageRules: [
{
"matchPackageNames": ["golangci-lint"],
"matchManagers": ["hermit"],
"enabled": false
},
]
}
+29 -10
View File
@@ -14,36 +14,55 @@ 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: golint
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: golint
func (r ResolverFunc) Validate(app *Application) error { return nil } //nolint: revive
// JSON returns a Resolver that retrieves values from a JSON source.
//
// Hyphens in flag names are replaced with underscores.
// Flag names are used as JSON keys indirectly, by trying 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) {
name := strings.Replace(flag.Name, "-", "_", -1)
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]
if !ok {
return nil, nil
if ok {
return raw, nil
} else if raw, ok = values[snakeCaseName]; ok {
return raw, nil
}
raw = values
for _, part := range strings.Split(name, ".") {
if values, ok := raw.(map[string]any); ok {
raw, ok = values[part]
if !ok {
return nil, nil
}
} else {
return nil, nil
}
}
return raw, nil
}
return f, nil
}
func snakeCase(name string) string {
name = strings.Join(strings.Split(strings.Title(name), "-"), "") //nolint:staticcheck // Unicode punctuation not an issue
return strings.ToLower(name[:1]) + name[1:]
}
+32 -13
View File
@@ -41,7 +41,7 @@ func (t TokenType) String() string {
// Token created by Scanner.
type Token struct {
Value interface{}
Value any
Type TokenType
}
@@ -82,7 +82,7 @@ func (t Token) InferredType() TokenType {
return t.Type
}
if v, ok := t.Value.(string); ok {
if strings.HasPrefix(v, "--") { // nolint: gocritic
if strings.HasPrefix(v, "--") { //nolint: gocritic
return FlagToken
} else if v == "-" {
return PositionalArgumentToken
@@ -109,18 +109,24 @@ func (t Token) IsValue() bool {
//
// For example, the token "--foo=bar" will be split into the following by the parser:
//
// [{FlagToken, "foo"}, {FlagValueToken, "bar"}]
// [{FlagToken, "foo"}, {FlagValueToken, "bar"}]
type Scanner struct {
args []Token
allowHyphenated bool
args []Token
}
// ScanAsType creates a new Scanner from args with the given type.
func ScanAsType(ttype TokenType, args ...string) *Scanner {
s := &Scanner{}
for _, arg := range args {
s.args = append(s.args, Token{Value: arg, Type: ttype})
}
return s
}
// Scan creates a new Scanner from args with untyped tokens.
func Scan(args ...string) *Scanner {
s := &Scanner{}
for _, arg := range args {
s.args = append(s.args, Token{Value: arg})
}
return s
return ScanAsType(UntypedToken, args...)
}
// ScanFromTokens creates a new Scanner from a slice of tokens.
@@ -128,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)
@@ -157,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
@@ -166,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
@@ -198,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
}
+263 -58
View File
@@ -1,6 +1,7 @@
package kong
import (
"errors"
"fmt"
"reflect"
"strconv"
@@ -8,54 +9,101 @@ 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
Default string
Format string
PlaceHolder string
Env 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.
Embed 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
}
type tagChars struct {
sep, quote, assign rune
func (t *Tag) String() string {
out := []string{}
for key, list := range t.items {
for _, value := range list {
out = append(out, fmt.Sprintf("%s:%q", key, value))
}
}
return strings.Join(out, " ")
}
var kongChars = tagChars{sep: ',', quote: '\'', assign: '='}
var bareChars = tagChars{sep: ' ', quote: '"', assign: ':'}
type tagChars struct {
sep, quote, assign rune
needsUnquote bool
}
func parseTagItems(tagString string, chr tagChars) map[string][]string {
var kongChars = tagChars{sep: ',', quote: '\'', assign: '=', needsUnquote: false}
var bareChars = tagChars{sep: ' ', quote: '"', assign: ':', needsUnquote: true}
//nolint:gocyclo
func parseTagItems(tagString string, chr tagChars) (map[string][]string, error) {
d := map[string][]string{}
key := []rune{}
value := []rune{}
quotes := false
inKey := true
add := func() {
d[string(key)] = append(d[string(key)], string(value))
add := func() error {
// Bare tags are quoted, therefore we need to unquote them in the same fashion reflect.Lookup() (implicitly)
// unquotes "kong tags".
s := string(value)
if chr.needsUnquote && s != "" {
if unquoted, err := strconv.Unquote(fmt.Sprintf(`"%s"`, s)); err == nil {
s = unquoted
} else {
return fmt.Errorf("unquoting tag value `%s`: %w", s, err)
}
}
d[string(key)] = append(d[string(key)], s)
key = []rune{}
value = []rune{}
inKey = true
return nil
}
runes := []rune(tagString)
@@ -69,7 +117,10 @@ func parseTagItems(tagString string, chr tagChars) map[string][]string {
eof = true
}
if !quotes && r == chr.sep {
add()
if err := add(); err != nil {
return nil, err
}
continue
}
if r == chr.assign && inKey {
@@ -79,6 +130,12 @@ func parseTagItems(tagString string, chr tagChars) map[string][]string {
if r == '\\' {
if next == chr.quote {
idx++
// We need to keep the backslashes, otherwise subsequent unquoting cannot work
if chr.needsUnquote {
value = append(value, r)
}
r = chr.quote
}
} else if r == chr.quote {
@@ -87,11 +144,10 @@ func parseTagItems(tagString string, chr tagChars) map[string][]string {
if next == chr.sep || eof {
continue
}
fail("%v has an unexpected char at pos %v", tagString, idx)
} else {
quotes = true
continue
return nil, fmt.Errorf("%v has an unexpected char at pos %v", tagString, idx)
}
quotes = true
continue
}
if inKey {
key = append(key, r)
@@ -100,77 +156,194 @@ func parseTagItems(tagString string, chr tagChars) map[string][]string {
}
}
if quotes {
fail("%v is not quoted properly", tagString)
return nil, fmt.Errorf("%v is not quoted properly", tagString)
}
add()
if err := add(); err != nil {
return nil, err
}
return d
return d, nil
}
func getTagInfo(ft reflect.StructField) (string, tagChars) {
s, ok := ft.Tag.Lookup("kong")
if ok {
return s, kongChars
func parseStructTagItems(tag reflect.StructTag) (map[string][]string, error) {
items, err := parseTagItems(string(tag), bareChars)
if err != nil {
return nil, err
}
delete(items, "kong")
return string(ft.Tag), bareChars
kongTag, ok := tag.Lookup("kong")
if !ok {
return items, nil
}
kongItems, err := parseTagItems(kongTag, kongChars)
if err != nil {
return nil, err
}
for key, value := range kongItems {
items[key] = append(value, items[key]...)
}
return items, nil
}
func newEmptyTag() *Tag {
return &Tag{items: map[string][]string{}}
}
func parseTag(fv reflect.Value, ft reflect.StructField) *Tag {
func tagSplitFn(r rune) bool {
return r == ',' || r == ' '
}
func parseTagString(s string) (*Tag, error) {
items, err := parseTagItems(s, bareChars)
if err != nil {
return nil, err
}
t := &Tag{
items: items,
}
err = hydrateTag(t, nil)
if err != nil {
return nil, fmt.Errorf("%s: %s", s, err)
}
return t, nil
}
func parseTag(parent reflect.Value, ft reflect.StructField) (*Tag, error) {
if ft.Tag.Get("kong") == "-" {
t := newEmptyTag()
t.Ignored = true
return t
return t, nil
}
items := map[string][]string{}
// First use a [Signature] if present
signatureTag, ok := maybeGetSignature(ft.Type)
if ok {
signatureItems, err := parseStructTagItems(signatureTag)
if err != nil {
return nil, err
}
items = signatureItems
}
// Next overlay the field's tags.
fieldItems, err := parseStructTagItems(ft.Tag)
if err != nil {
return nil, err
}
for key, value := range fieldItems {
// Prepend field tag values
items[key] = append(value, items[key]...)
}
t := &Tag{
items: parseTagItems(getTagInfo(ft)),
items: items,
}
err = hydrateTag(t, ft.Type)
if err != nil {
return nil, failField(parent, ft, "%s", err)
}
return t, nil
}
func hydrateTag(t *Tag, typ reflect.Type) error { //nolint: gocyclo
var typeName string
var isBool bool
var isBoolPtr bool
if typ != nil {
typeName = typ.Name()
isBool = typ.Kind() == reflect.Bool
isBoolPtr = typ.Kind() == reflect.Ptr && typ.Elem().Kind() == reflect.Bool
}
var err error
t.Cmd = t.Has("cmd")
t.Arg = t.Has("arg")
required := t.Has("required")
optional := t.Has("optional")
if required && optional {
fail("can't specify both required and optional")
return fmt.Errorf("can't specify both required and optional")
}
t.Required = required
t.Optional = optional
t.HasDefault = t.Has("default")
t.Default = t.Get("default")
// Arguments with defaults are always optional.
if t.Arg && t.Default != "" {
if t.Arg && t.HasDefault {
t.Optional = true
} else if t.Arg && !optional { // Arguments are required unless explicitly made optional.
t.Required = true
}
t.Name = t.Get("name")
t.Help = t.Get("help")
t.Type = t.Get("type")
t.Env = t.Get("env")
t.Short, _ = t.GetRune("short")
t.TypeName = typeName
for _, env := range t.GetAll("env") {
t.Envs = append(t.Envs, strings.FieldsFunc(env, tagSplitFn)...)
}
t.Short, err = t.GetRune("short")
if err != nil && t.Get("short") != "" {
return fmt.Errorf("invalid short flag name %q: %s", t.Get("short"), err)
}
t.Hidden = t.Has("hidden")
t.Format = t.Get("format")
t.Sep, _ = t.GetSep("sep", ',')
t.MapSep, _ = t.GetSep("mapsep", ';')
t.Group = t.Get("group")
t.Xor = t.Get("xor")
for _, xor := range t.GetAll("xor") {
t.Xor = append(t.Xor, strings.FieldsFunc(xor, tagSplitFn)...)
}
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")
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
}
aliases := t.Get("aliases")
if len(aliases) > 0 {
t.Aliases = append(t.Aliases, strings.FieldsFunc(aliases, tagSplitFn)...)
}
t.Vars = Vars{}
for _, set := range t.GetAll("set") {
parts := strings.SplitN(set, "=", 2)
if len(parts) == 0 {
fail("set should be in the form key=value but got %q", set)
return fmt.Errorf("set should be in the form key=value but got %q", set)
}
t.Vars[parts[0]] = parts[1]
}
t.PlaceHolder = t.Get("placeholder")
if t.PlaceHolder == "" {
t.PlaceHolder = strings.ToUpper(dashedString(fv.Type().Name()))
}
t.Enum = t.Get("enum")
return t
scalarType := typ == nil || !(typ.Kind() == reflect.Slice || typ.Kind() == reflect.Map || typ.Kind() == reflect.Ptr)
if t.Enum != "" && !(t.Required || t.HasDefault) && scalarType {
return fmt.Errorf("enum value is only valid if it is either required or has a valid default value")
}
passthrough := t.Has("passthrough")
if passthrough && !t.Arg && !t.Cmd {
return fmt.Errorf("passthrough only makes sense for positional arguments or commands")
}
t.Passthrough = passthrough
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
}
// Has returns true if the tag contained the given key.
@@ -212,9 +385,10 @@ func (t *Tag) GetInt(k string) (int64, error) {
// GetRune parses the given tag as a rune.
func (t *Tag) GetRune(k string) (rune, error) {
r, _ := utf8.DecodeRuneInString(t.Get(k))
if r == utf8.RuneError {
return 0, fmt.Errorf("%v has a rune error", t.Get(k))
value := t.Get(k)
r, size := utf8.DecodeRuneInString(value)
if r == utf8.RuneError || size < len(value) {
return 0, errors.New("invalid rune")
}
return r, nil
}
@@ -238,3 +412,34 @@ func (t *Tag) GetSep(k string, dflt rune) (rune, error) {
}
return r, nil
}
// Signature allows flags, args and commands to supply a default set of tags,
// that can be overridden by the field itself.
type Signature interface {
// Signature returns default tags for the flag, arg or command.
//
// eg. `name:"migrate" help:"Run migrations" aliases:"mig,mg"`.
Signature() string
}
var signatureOverrideType = reflect.TypeOf((*Signature)(nil)).Elem()
func maybeGetSignature(t reflect.Type) (reflect.StructTag, bool) {
ut := t
if ut.Kind() == reflect.Pointer {
ut = ut.Elem()
}
ptr := reflect.New(ut)
var sig string
for _, v := range []reflect.Value{ptr, ptr.Elem()} {
if v.Type().Implements(signatureOverrideType) {
sig = v.Interface().(Signature).Signature() //nolint:forcetypeassert
break
}
}
sig = strings.TrimSpace(sig)
if sig == "" {
return "", false
}
return reflect.StructTag(sig), true
}
+25 -3
View File
@@ -2,6 +2,8 @@ package kong
import (
"fmt"
"os"
"reflect"
)
// ConfigFlag uses the configured (via kong.Configuration(loader)) configuration loader to load configuration
@@ -15,7 +17,7 @@ func (c ConfigFlag) BeforeResolve(kong *Kong, ctx *Context, trace *Path) error {
if kong.loader == nil {
return fmt.Errorf("kong must be configured with kong.Configuration(...)")
}
path := string(ctx.FlagValue(trace.Flag).(ConfigFlag))
path := string(ctx.FlagValue(trace.Flag).(ConfigFlag)) //nolint
resolver, err := kong.LoadConfig(path)
if err != nil {
return err
@@ -27,9 +29,29 @@ func (c ConfigFlag) BeforeResolve(kong *Kong, ctx *Context, trace *Path) error {
// VersionFlag is a flag type that can be used to display a version number, stored in the "version" variable.
type VersionFlag bool
// BeforeApply writes the version variable and terminates with a 0 exit status.
func (v VersionFlag) BeforeApply(app *Kong, vars Vars) error {
// BeforeReset writes the version variable and terminates with a 0 exit status.
func (v VersionFlag) BeforeReset(app *Kong, vars Vars) error {
fmt.Fprintln(app.Stdout, vars["version"])
app.Exit(0)
return nil
}
// ChangeDirFlag changes the current working directory to a path specified by a flag
// early in the parsing process, changing how other flags resolve relative paths.
//
// Use this flag to provide a "git -C" like functionality.
//
// It is not compatible with custom named decoders, e.g., existingdir.
type ChangeDirFlag string
// Decode is used to create a side effect of changing the current working directory.
func (c ChangeDirFlag) Decode(ctx *DecodeContext) error {
var path string
err := ctx.Scan.PopValueInto("string", &path)
if err != nil {
return err
}
path = ExpandPath(path)
ctx.Value.Target.Set(reflect.ValueOf(ChangeDirFlag(path)))
return os.Chdir(path)
}