⬆️ Update dependencies
This commit is contained in:
-1
@@ -1 +0,0 @@
|
||||
bin
|
||||
|
||||
+49
-6
@@ -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
@@ -2,36 +2,49 @@
|
||||
<p align="center"><img width="90%" src="kong.png" /></p>
|
||||
|
||||
# Kong is a command-line parser for Go
|
||||
|
||||
[](http://godoc.org/github.com/alecthomas/kong) [](https://circleci.com/gh/alecthomas/kong) [](https://goreportcard.com/report/github.com/alecthomas/kong) [](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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user