⬆️ Update dependencies

This commit is contained in:
2026-11-01 15:12:52 +01:00
parent fd0d7ddee6
commit 312884dd59
501 changed files with 98424 additions and 35494 deletions
-1
View File
@@ -1 +0,0 @@
bin
+49 -6
View File
@@ -7,25 +7,56 @@ output:
linters:
enable-all: true
disable:
- maligned
- lll
- gochecknoglobals
- wsl
- funlen
- gocognit
- gomnd
- goprintffuncname
- paralleltest
- nlreturn
- testpackage
- wrapcheck
- forbidigo
- gci
- godot
- gofumpt
- cyclop
- errorlint
- nestif
- tagliatelle
- thelper
- godox
- goconst
- varnamelen
- ireturn
- exhaustruct
- nonamedreturns
- nilnil
- depguard # nothing to guard against yet
- tagalign # hurts readability of kong tags
- tenv # deprecated since v1.64, but not removed yet
- mnd
- perfsprint
- err113
- copyloopvar
- intrange
- nakedret
- recvcheck # value receivers are intentionally used for copies
linters-settings:
govet:
check-shadowing: true
# These govet checks are disabled by default, but they're useful.
enable:
- niliness
- sortslice
- unusedwrite
dupl:
threshold: 100
goconst:
min-len: 5
min-occurrences: 3
gocyclo:
min-complexity: 20
exhaustive:
default-signifies-exhaustive: true
issues:
max-per-linter: 0
@@ -40,3 +71,15 @@ issues:
- 'bad syntax for struct tag key'
- 'bad syntax for struct tag pair'
- 'result .* \(error\) is always nil'
- 'Error return value of `fmt.Fprintln` is not checked'
exclude-rules:
# Don't warn on unused parameters.
# Parameter names are useful for documentation.
# Replacing them with '_' hides useful information.
- linters: [revive]
text: 'unused-parameter: parameter \S+ seems to be unused, consider removing or renaming it as _'
# Duplicate words are okay in tests.
- linters: [dupword]
path: _test\.go
+343 -104
View File
@@ -2,36 +2,49 @@
<p align="center"><img width="90%" src="kong.png" /></p>
# Kong is a command-line parser for Go
[![](https://godoc.org/github.com/alecthomas/kong?status.svg)](http://godoc.org/github.com/alecthomas/kong) [![CircleCI](https://img.shields.io/circleci/project/github/alecthomas/kong.svg)](https://circleci.com/gh/alecthomas/kong) [![Go Report Card](https://goreportcard.com/badge/github.com/alecthomas/kong)](https://goreportcard.com/report/github.com/alecthomas/kong) [![Slack chat](https://img.shields.io/static/v1?logo=slack&style=flat&label=slack&color=green&message=gophers)](https://gophers.slack.com/messages/CN9DS8YF3)
[TOC levels=2-3 numbered]: # "#### Table of Contents"
- [Version 1.0.0 Release](#version-100-release)
- [Introduction](#introduction)
- [Help](#help)
- [Help as a user of a Kong application](#help-as-a-user-of-a-kong-application)
- [Defining help in Kong](#defining-help-in-kong)
- [Command handling](#command-handling)
- [Switch on the command string](#switch-on-the-command-string)
- [Attach a `Run(...) error` method to each command](#attach-a-run-error-method-to-each-command)
- [Hooks: BeforeReset(), BeforeResolve(), BeforeApply(), AfterApply()](#hooks-beforereset-beforeresolve-beforeapply-afterapply)
- [The Bind() option](#the-bind-option)
- [Flags](#flags)
- [Commands and sub-commands](#commands-and-sub-commands)
- [Branching positional arguments](#branching-positional-arguments)
- [Positional arguments](#positional-arguments)
- [Slices](#slices)
- [Maps](#maps)
- [Pointers](#pointers)
- [Nested data structure](#nested-data-structure)
- [Custom named decoders](#custom-named-decoders)
- [Supported field types](#supported-field-types)
- [Custom decoders (mappers)](#custom-decoders-mappers)
- [Supported tags](#supported-tags)
- [Plugins](#plugins)
- [Dynamic Commands](#dynamic-commands)
- [Variable interpolation](#variable-interpolation)
- [Validation](#validation)
- [Modifying Kong's behaviour](#modifying-kongs-behaviour)
- [`Name(help)` and `Description(help)` - set the application name description](#namehelp-and-descriptionhelp---set-the-application-name-description)
- [`Configuration(loader, paths...)` - load defaults from configuration files](#configurationloader-paths---load-defaults-from-configuration-files)
- [`Resolver(...)` - support for default values from external sources](#resolver---support-for-default-values-from-external-sources)
- [`*Mapper(...)` - customising how the command-line is mapped to Go values](#mapper---customising-how-the-command-line-is-mapped-to-go-values)
- [`ConfigureHelp(HelpOptions)` and `Help(HelpFunc)` - customising help](#configurehelphelpoptions-and-helphelpfunc---customising-help)
- [Injecting values into `Run()` methods](#injecting-values-into-run-methods)
- [Other options](#other-options)
#### Table of Contents
1. [Introduction](#introduction)
1. [Help](#help)
1. [Command handling](#command-handling)
1. [Switch on the command string](#switch-on-the-command-string)
1. [Attach a `Run(...) error` method to each command](#attach-a-run-error-method-to-each-command)
1. [Hooks: BeforeResolve(), BeforeApply(), AfterApply() and the Bind() option](#hooks-beforeresolve-beforeapply-afterapply-and-the-bind-option)
1. [Flags](#flags)
1. [Commands and sub-commands](#commands-and-sub-commands)
1. [Branching positional arguments](#branching-positional-arguments)
1. [Terminating positional arguments](#terminating-positional-arguments)
1. [Slices](#slices)
1. [Maps](#maps)
1. [Custom named decoders](#custom-named-decoders)
1. [Custom decoders (mappers)](#custom-decoders-mappers)
1. [Supported tags](#supported-tags)
1. [Variable interpolation](#variable-interpolation)
1. [Modifying Kong's behaviour](#modifying-kongs-behaviour)
1. [`Name(help)` and `Description(help)` - set the application name description](#namehelp-and-descriptionhelp---set-the-application-name-description)
1. [`Configuration(loader, paths...)` - load defaults from configuration files](#configurationloader-paths---load-defaults-from-configuration-files)
1. [`Resolver(...)` - support for default values from external sources](#resolver---support-for-default-values-from-external-sources)
1. [`*Mapper(...)` - customising how the command-line is mapped to Go values](#mapper---customising-how-the-command-line-is-mapped-to-go-values)
1. [`ConfigureHelp(HelpOptions)` and `Help(HelpFunc)` - customising help](#configurehelphelpoptions-and-helphelpfunc---customising-help)
1. [`Bind(...)` - bind values for callback hooks and Run() methods](#bind---bind-values-for-callback-hooks-and-run-methods)
1. [Other options](#other-options)
## Version 1.0.0 Release
Kong has been stable for a long time, so it seemed appropriate to cut a 1.0 release.
There is one breaking change, [#436](https://github.com/alecthomas/kong/pull/436), which should effect relatively few users.
## Introduction
@@ -56,12 +69,12 @@ var CLI struct {
Force bool `help:"Force removal."`
Recursive bool `help:"Recursively remove files."`
Paths []string `arg name:"path" help:"Paths to remove." type:"path"`
} `cmd help:"Remove files."`
Paths []string `arg:"" name:"path" help:"Paths to remove." type:"path"`
} `cmd:"" help:"Remove files."`
Ls struct {
Paths []string `arg optional name:"path" help:"Paths to list." type:"path"`
} `cmd help:"List paths."`
Paths []string `arg:"" optional:"" name:"path" help:"Paths to list." type:"path"`
} `cmd:"" help:"List paths."`
}
func main() {
@@ -77,7 +90,9 @@ func main() {
## Help
Help is automatically generated. With no other arguments provided, help will display a full summary of all available commands.
### Help as a user of a Kong application
Every Kong application includes a `--help` flag that will display auto-generated help.
eg.
@@ -115,9 +130,73 @@ eg.
-f, --force Force removal.
-r, --recursive Recursively remove files.
For flags with associated environment variables, the variable `${env}` can be
interpolated into the help string. In the absence of this variable in the help,
### Defining help in Kong
Help is automatically generated from the command-line structure itself,
including `help:""` and other tags. [Variables](#variable-interpolation) will
also be interpolated into the help string.
Finally, any command, or argument type implementing the interface
`Help() string` will have this function called to retrieve more detail to
augment the help tag. This allows for much more descriptive text than can
fit in Go tags. [See \_examples/shell/help](./_examples/shell/help)
#### Showing the _command_'s detailed help
A command's additional help text is _not_ shown from top-level help, but can be displayed within contextual help:
**Top level help**
```bash
$ go run ./_examples/shell/help --help
Usage: help <command>
An app demonstrating HelpProviders
Flags:
-h, --help Show context-sensitive help.
--flag Regular flag help
Commands:
echo Regular command help
```
**Contextual**
```bash
$ go run ./_examples/shell/help echo --help
Usage: help echo <msg>
Regular command help
🚀 additional command help
Arguments:
<msg> Regular argument help
Flags:
-h, --help Show context-sensitive help.
--flag Regular flag help
```
#### Showing an _argument_'s detailed help
Custom help will only be shown for _positional arguments with named fields_ ([see the README section on positional arguments for more details on what that means](#branching-positional-arguments))
**Contextual argument help**
```bash
$ go run ./_examples/shell/help msg --help
Usage: help echo <msg>
Regular argument help
📣 additional argument help
Flags:
-h, --help Show context-sensitive help.
--flag Regular flag help
```
## Command handling
@@ -127,7 +206,7 @@ There are two ways to handle commands in Kong.
When you call `kong.Parse()` it will return a unique string representation of the command. Each command branch in the hierarchy will be a bare word and each branching argument or required positional argument will be the name surrounded by angle brackets. Here's an example:
There's an example of this pattern [here](https://github.com/alecthomas/kong/blob/master/_examples/shell/main.go).
There's an example of this pattern [here](https://github.com/alecthomas/kong/blob/master/_examples/shell/commandstring/main.go).
eg.
@@ -141,12 +220,12 @@ var CLI struct {
Force bool `help:"Force removal."`
Recursive bool `help:"Recursively remove files."`
Paths []string `arg name:"path" help:"Paths to remove." type:"path"`
} `cmd help:"Remove files."`
Paths []string `arg:"" name:"path" help:"Paths to remove." type:"path"`
} `cmd:"" help:"Remove files."`
Ls struct {
Paths []string `arg optional name:"path" help:"Paths to list." type:"path"`
} `cmd help:"List paths."`
Paths []string `arg:"" optional:"" name:"path" help:"Paths to list." type:"path"`
} `cmd:"" help:"List paths."`
}
func main() {
@@ -173,7 +252,7 @@ A more robust approach is to break each command out into their own structs:
Once a command node is selected by Kong it will search from that node back to the root. Each
encountered command node with a `Run(...) error` will be called in reverse order. This allows
sub-trees to be re-used fairly conveniently.
sub-trees to be reused fairly conveniently.
In addition to values bound with the `kong.Bind(...)` option, any values
passed through to `kong.Context.Run(...)` are also bindable to the target's
@@ -194,7 +273,7 @@ type RmCmd struct {
Force bool `help:"Force removal."`
Recursive bool `help:"Recursively remove files."`
Paths []string `arg name:"path" help:"Paths to remove." type:"path"`
Paths []string `arg:"" name:"path" help:"Paths to remove." type:"path"`
}
func (r *RmCmd) Run(ctx *Context) error {
@@ -203,7 +282,7 @@ func (r *RmCmd) Run(ctx *Context) error {
}
type LsCmd struct {
Paths []string `arg optional name:"path" help:"Paths to list." type:"path"`
Paths []string `arg:"" optional:"" name:"path" help:"Paths to list." type:"path"`
}
func (l *LsCmd) Run(ctx *Context) error {
@@ -214,8 +293,8 @@ func (l *LsCmd) Run(ctx *Context) error {
var cli struct {
Debug bool `help:"Enable debug mode."`
Rm RmCmd `cmd help:"Remove files."`
Ls LsCmd `cmd help:"List paths."`
Rm RmCmd `cmd:"" help:"Remove files."`
Ls LsCmd `cmd:"" help:"List paths."`
}
func main() {
@@ -227,13 +306,21 @@ func main() {
```
## Hooks: BeforeResolve(), BeforeApply(), AfterApply() and the Bind() option
## Hooks: BeforeReset(), BeforeResolve(), BeforeApply(), AfterApply(), AfterRun()
If a node in the grammar has a `BeforeResolve(...)`, `BeforeApply(...) error` and/or `AfterApply(...) error` method, those methods will be called before validation/assignment and after validation/assignment, respectively.
If a node in the CLI, or any of its embedded fields, implements a `BeforeReset(...) error`, `BeforeResolve
(...) error`, `BeforeApply(...) error`, `AfterApply(...) error`, and/or `AfterRun(...) error` method, those
will be called as Kong resets, resolves, validates, and assigns values to the node.
The `--help` flag is implemented with a `BeforeApply` hook.
| Hook | Description |
| --------------- | ----------------------------------------------------------------------------------------------------------- |
| `BeforeReset` | Invoked before values are reset to their defaults (as defined by the grammar) or to zero values |
| `BeforeResolve` | Invoked before resolvers are applied to a node |
| `BeforeApply` | Invoked before the traced command line arguments are applied to the grammar |
| `AfterApply` | Invoked after command line arguments are applied to the grammar **and validated** |
| `AfterRun` | Invoked after `Run()` returns. Will not be called if `os.Exit()` is manually called. |
Arguments to hooks are provided via the `Run(...)` method or `Bind(...)` option. `*Kong`, `*Context` and `*Path` are also bound and finally, hooks can also contribute bindings via `kong.Context.Bind()` and `kong.Context.BindTo()`.
The `--help` flag is implemented with a `BeforeReset` hook.
eg.
@@ -252,7 +339,7 @@ var cli struct {
func main() {
// Debug logger going to discard.
logger := log.New(ioutil.Discard, "", log.LstdFlags)
logger := log.New(io.Discard, "", log.LstdFlags)
ctx := kong.Parse(&cli, kong.Bind(logger))
@@ -260,9 +347,47 @@ func main() {
}
```
It's also possible to register these hooks with the functional options
`kong.WithBeforeReset`, `kong.WithBeforeResolve`, `kong.WithBeforeApply`, and
`kong.WithAfterApply`.
## The Bind() option
Arguments to hooks are provided via the `Run(...)` method or `Bind(...)` option. `*Kong`, `*Context`, `*Path` and parent commands are also bound and finally, hooks can also contribute bindings via `kong.Context.Bind()` and `kong.Context.BindTo()`.
eg:
```go
type CLI struct {
Debug bool `help:"Enable debug mode."`
Rm RmCmd `cmd:"" help:"Remove files."`
Ls LsCmd `cmd:"" help:"List paths."`
}
type AuthorName string
// ...
func (l *LsCmd) Run(cli *CLI) error {
// use cli.Debug here !!
return nil
}
func (r *RmCmd) Run(author AuthorName) error{
// use binded author here
return nil
}
func main() {
var cli CLI
ctx := kong.Parse(&cli, Bind(AuthorName("penguin")))
err := ctx.Run()
```
## Flags
Any [mapped](#mapper---customising-how-the-command-line-is-mapped-to-go-values) field in the command structure *not* tagged with `cmd` or `arg` will be a flag. Flags are optional by default.
Any [mapped](#mapper---customising-how-the-command-line-is-mapped-to-go-values) field in the command structure _not_ tagged with `cmd` or `arg` will be a flag. Flags are optional by default.
eg. The command-line `app [--flag="foo"]` can be represented by the following.
@@ -289,7 +414,7 @@ type CLI struct {
}
```
If a sub-command is tagged with `default:"1"` it will be selected if there are no further arguments.
If a sub-command is tagged with `default:"1"` it will be selected if there are no further arguments. If a sub-command is tagged with `default:"withargs"` it will be selected even if there are further arguments or flags and those arguments or flags are valid for the sub-command. This allows the user to omit the sub-command name on the CLI if its arguments/flags are not ambiguous with the sibling commands or flags.
## Branching positional arguments
@@ -318,11 +443,14 @@ var CLI struct {
This looks a little verbose in this contrived example, but typically this will not be the case.
## Terminating positional arguments
## Positional arguments
If a [mapped type](#mapper---customising-how-the-command-line-is-mapped-to-go-values) is tagged with `arg` it will be treated as the final positional values to be parsed on the command line.
If a field is tagged with `arg:""` it will be treated as the final positional
value to be parsed on the command line. By default positional arguments are
required, but specifying `optional:""` will alter this.
If a positional argument is a slice, all remaining arguments will be appended to that slice.
If a positional argument is a slice, all remaining arguments will be appended
to that slice.
## Slices
@@ -337,7 +465,7 @@ You would use the following:
```go
var CLI struct {
Ls struct {
Files []string `arg type:"existingfile"`
Files []string `arg:"" type:"existingfile"`
} `cmd`
}
```
@@ -356,7 +484,7 @@ You would use the following:
var CLI struct {
Config struct {
Set struct {
Config map[string]float64 `arg type:"file:"`
Config map[string]float64 `arg:"" type:"file:"`
} `cmd`
} `cmd`
}
@@ -364,29 +492,66 @@ var CLI struct {
For flags, multiple key+value pairs should be separated by `mapsep:"rune"` tag (defaults to `;`) eg. `--set="key1=value1;key2=value2"`.
## Pointers
Pointers work like the underlying type, except that you can differentiate between the presence of the zero value and no value being supplied.
For example:
```go
var CLI struct {
Foo *int
}
```
Would produce a nil value for `Foo` if no `--foo` argument is supplied, but would have a pointer to the value 0 if the argument `--foo=0` was supplied.
## Nested data structure
Kong support a nested data structure as well with `embed:""`. You can combine `embed:""` with `prefix:""`:
```go
var CLI struct {
Logging struct {
Level string `enum:"debug,info,warn,error" default:"info"`
Type string `enum:"json,console" default:"console"`
} `embed:"" prefix:"logging."`
}
```
This configures Kong to accept flags `--logging.level` and `--logging.type`.
## Custom named decoders
Kong includes a number of builtin custom type mappers. These can be used by
specifying the tag `type:"<type>"`. They are registered with the option
function `NamedMapper(name, mapper)`.
| Name | Description
|-------------------|---------------------------------------------------
| `path` | A path. ~ expansion is applied.
| `existingfile` | An existing file. ~ expansion is applied. `-` is accepted for stdin.
| `existingdir` | An existing directory. ~ expansion is applied.
| `counter` | Increment a numeric field. Useful for `-vvv`. Can accept `-s`, `--long` or `--long=N`.
| Name | Description |
| -------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `path` | A path. ~ expansion is applied. `-` is accepted for stdout, and will be passed unaltered. |
| `existingfile` | An existing file. ~ expansion is applied. `-` is accepted for stdin, and will be passed unaltered. |
| `existingdir` | An existing directory. ~ expansion is applied. |
| `counter` | Increment a numeric field. Useful for `-vvv`. Can accept `-s`, `--long` or `--long=N`. |
| `filecontent` | Read the file at path into the field. ~ expansion is applied. `-` is accepted for stdin, and will be passed unaltered. |
Slices and maps treat type tags specially. For slices, the `type:""` tag
specifies the element type. For maps, the tag has the format
`tag:"[<key>]:[<value>]"` where either may be omitted.
## Supported field types
## Custom decoders (mappers)
Any field implementing `encoding.TextUnmarshaler` or `json.Unmarshaler` will use those interfaces
for decoding values.
for decoding values. Kong also includes builtin support for many common Go types:
| Type | Description |
| --------------- | ----------------------------------------------------------------------------------------------------------- |
| `time.Duration` | Populated using `time.ParseDuration()`. |
| `time.Time` | Populated using `time.Parse()`. Format defaults to RFC3339 but can be overridden with the `format:"X"` tag. |
| `*os.File` | Path to a file that will be opened, or `-` for `os.Stdin`. File must be closed by the user. |
| `*url.URL` | Populated with `url.Parse()`. |
For more fine-grained control, if a field implements the
[MapperValue](https://godoc.org/github.com/alecthomas/kong#MapperValue)
@@ -397,40 +562,76 @@ interface it will be used to decode arguments into the field.
Tags can be in two forms:
1. Standard Go syntax, eg. `kong:"required,name='foo'"`.
2. Bare tags, eg. `required name:"foo"`
2. Bare tags, eg. `required:"" name:"foo"`
Both can coexist with standard Tag parsing.
Tag | Description
-----------------------| -------------------------------------------
`cmd` | If present, struct is a command.
`arg` | If present, field is an argument.
`env:"X"` | Specify envar to use for default value.
`name:"X"` | Long name, for overriding field name.
`help:"X"` | Help text.
`type:"X"` | Specify [named types](#custom-named-decoders) to use.
`placeholder:"X"` | Placeholder text.
`default:"X"` | Default value.
`default:"1"` | On a command, make it the default.
`short:"X"` | Short name, if flag.
`required` | If present, flag/arg is required.
`optional` | If present, flag/arg is optional.
`hidden` | If present, command or flag is hidden.
`format:"X"` | Format for parsing input, if supported.
`sep:"X"` | Separator for sequences (defaults to ","). May be `none` to disable splitting.
`mapsep:"X"` | Separator for maps (defaults to ";"). May be `none` to disable splitting.
`enum:"X,Y,..."` | Set of valid values allowed for this flag.
`group:"X"` | Logical group for a flag or command.
`xor:"X"` | Exclusive OR group for flags. Only one flag in the group can be used which is restricted within the same command.
`prefix:"X"` | Prefix for all sub-flags.
`set:"K=V"` | Set a variable for expansion by child elements. Multiples can occur.
`embed` | If present, this field's children will be embedded in the parent. Useful for composition.
`-` | Ignore the field. Useful for adding non-CLI fields to a configuration struct.
| Tag | Description |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `cmd:""` | If present, struct is a command. |
| `arg:""` | If present, field is an argument. Required by default. |
| `env:"X,Y,..."` | Specify envars to use for default value. The envs are resolved in the declared order. The first value found is used. |
| `name:"X"` | Long name, for overriding field name. |
| `help:"X"` | Help text. |
| `type:"X"` | Specify [named types](#custom-named-decoders) to use. |
| `placeholder:"X"` | Placeholder input, if flag. e.g. `` `placeholder:"<the-placeholder>"` `` will show `--flag-name=<the-placeholder>` when displaying help. |
| `default:"X"` | Default value. |
| `default:"1"` | On a command, make it the default. |
| `default:"withargs"` | On a command, make it the default and allow args/flags from that command |
| `short:"X"` | Short name, if flag. |
| `aliases:"X,Y"` | One or more aliases (for cmd or flag). |
| `required:""` | If present, flag/arg is required. |
| `optional:""` | If present, flag/arg is optional. |
| `hidden:""` | If present, command or flag is hidden. |
| `negatable:""` | If present on a `bool` field, supports prefixing a flag with `--no-` to invert the default value |
| `negatable:"X"` | If present on a `bool` field, supports `--X` to invert the default value |
| `format:"X"` | Format for parsing input, if supported. |
| `sep:"X"` | Separator for sequences (defaults to ","). May be `none` to disable splitting. |
| `mapsep:"X"` | Separator for maps (defaults to ";"). May be `none` to disable splitting. |
| `enum:"X,Y,..."` | Set of valid values allowed for this flag. An enum field must be `required` or have a valid `default`. |
| `group:"X"` | Logical group for a flag or command. |
| `xor:"X,Y,..."` | Exclusive OR groups for flags. Only one flag in the group can be used which is restricted within the same command. When combined with `required`, at least one of the `xor` group will be required. |
| `and:"X,Y,..."` | AND groups for flags. All flags in the group must be used in the same command. When combined with `required`, all flags in the group will be required. |
| `prefix:"X"` | Prefix for all sub-flags. |
| `envprefix:"X"` | Envar prefix for all sub-flags. |
| `xorprefix:"X"` | Prefix for all sub-flags in XOR/AND groups. |
| `set:"K=V"` | Set a variable for expansion by child elements. Multiples can occur. |
| `embed:""` | If present, this field's children will be embedded in the parent. Useful for composition. |
| `passthrough:"<mode>"`[^1] | If present on a positional argument, it stops flag parsing when encountered, as if `--` was processed before. Useful for external command wrappers, like `exec`. On a command it requires that the command contains only one argument of type `[]string` which is then filled with everything following the command, unparsed. |
| `-` | Ignore the field. Useful for adding non-CLI fields to a configuration struct. e.g `` `kong:"-"` `` |
[^1]: `<mode>` can be `partial` or `all` (the default). `all` will pass through all arguments including flags. `partial` will validate flags until the first positional argument is encountered, then pass through all remaining
positional arguments.
## Plugins
Kong CLI's can be extended by embedding the `kong.Plugin` type and populating it with pointers to Kong annotated structs. For example:
```go
var pluginOne struct {
PluginOneFlag string
}
var pluginTwo struct {
PluginTwoFlag string
}
var cli struct {
BaseFlag string
kong.Plugins
}
cli.Plugins = kong.Plugins{&pluginOne, &pluginTwo}
```
Additionally if an interface type is embedded, it can also be populated with a Kong annotated struct.
## Dynamic Commands
While plugins give complete control over extending command-line interfaces, Kong
also supports dynamically adding commands via `kong.DynamicCommand()`.
## Variable interpolation
Kong supports limited variable interpolation into help strings, enum lists and
default values.
Kong supports limited variable interpolation into help strings, placeholder strings,
enum lists and default values.
Variables are in the form:
@@ -450,6 +651,10 @@ are defined from the value itself:
${default}
${enum}
For flags with associated environment variables, the variable `${env}` can be
interpolated into the help string. In the absence of this variable in the
help string, Kong will append `($$${env})` to the help string.
eg.
```go
@@ -465,9 +670,29 @@ func main() {
}
```
## Validation
Kong does validation on the structure of a command-line, but also supports
extensible validation. Any node in the tree may implement either of the following interfaces:
```go
type Validatable interface {
Validate() error
}
```
```go
type Validatable interface {
Validate(kctx *kong.Context) error
}
```
If one of these nodes is in the active command-line it will be called during
normal validation.
## Modifying Kong's behaviour
Each Kong parser can be configured via functional options passed to `New(cli interface{}, options...Option)`.
Each Kong parser can be configured via functional options passed to `New(cli any, options...Option)`.
The full set of options can be found [here](https://godoc.org/github.com/alecthomas/kong#Option).
@@ -489,7 +714,14 @@ eg.
kong.Parse(&cli, kong.Configuration(kong.JSON, "/etc/myapp.json", "~/.myapp.json"))
```
[See the tests](https://github.com/alecthomas/kong/blob/master/resolver_test.go#L103) for an example of how the JSON file is structured.
[See the tests](https://github.com/alecthomas/kong/blob/master/resolver_test.go#L206) for an example of how the JSON file is structured.
#### List of Configuration Loaders
- [YAML](https://github.com/alecthomas/kong-yaml)
- [HCL](https://github.com/alecthomas/kong-hcl)
- [TOML](https://github.com/alecthomas/kong-toml)
- [JSON](https://github.com/alecthomas/kong)
### `Resolver(...)` - support for default values from external sources
@@ -502,13 +734,14 @@ Example resolvers can be found in [resolver.go](https://github.com/alecthomas/ko
Command-line arguments are mapped to Go values via the Mapper interface:
```go
// A Mapper knows how to map command-line input to Go.
// A Mapper represents how a field is mapped from command-line values to Go.
//
// Mappers can be associated with concrete fields via pointer, reflect.Type, reflect.Kind, or via a "type" tag.
//
// Additionally, if a type implements the MapperValue interface, it will be used.
type Mapper interface {
// Decode scan into target.
//
// "ctx" contains context about the value being decoded that may be useful
// to some mapperss.
Decode(ctx *MapperContext, scan *Scanner, target reflect.Value) error
// Decode ctx.Value with ctx.Scanner into target.
Decode(ctx *DecodeContext, target reflect.Value) error
}
```
@@ -517,19 +750,25 @@ All builtin Go types (as well as a bunch of useful stdlib types like `time.Time`
1. `NamedMapper(string, Mapper)` and using the tag key `type:"<name>"`.
2. `KindMapper(reflect.Kind, Mapper)`.
3. `TypeMapper(reflect.Type, Mapper)`.
4. `ValueMapper(interface{}, Mapper)`, passing in a pointer to a field of the grammar.
4. `ValueMapper(any, Mapper)`, passing in a pointer to a field of the grammar.
### `ConfigureHelp(HelpOptions)` and `Help(HelpFunc)` - customising help
The default help output is usually sufficient, but if not there are two solutions.
1. Use `ConfigureHelp(HelpOptions)` to configure how help is formatted (see [HelpOptions](https://godoc.org/github.com/alecthomas/kong#HelpOptions) for details).
2. Custom help can be wired into Kong via the `Help(HelpFunc)` option. The `HelpFunc` is passed a `Context`, which contains the parsed context for the current command-line. See the implementation of `PrintHelp` for an example.
3. Use `HelpFormatter(HelpValueFormatter)` if you want to just customize the help text that is accompanied by flags and arguments.
2. Custom help can be wired into Kong via the `Help(HelpFunc)` option. The `HelpFunc` is passed a `Context`, which contains the parsed context for the current command-line. See the implementation of `DefaultHelpPrinter` for an example.
3. Use `ValueFormatter(HelpValueFormatter)` if you want to just customize the help text that is accompanied by flags and arguments.
4. Use `Groups([]Group)` if you want to customize group titles or add a header.
### `Bind(...)` - bind values for callback hooks and Run() methods
### Injecting values into `Run()` methods
See the [section on hooks](#hooks-beforeresolve-beforeapply-afterapply-and-the-bind-option) for details.
There are several ways to inject values into `Run()` methods:
1. Use `Bind()` to bind values directly.
2. Use `BindTo()` to bind values to an interface type.
3. Use `BindToProvider()` to bind values to a function that provides the value.
4. Implement `Provide<Type>() error` methods on the command structure.
### Other options
+285 -74
View File
@@ -4,10 +4,15 @@ import (
"fmt"
"reflect"
"strings"
"unicode/utf8"
)
func build(k *Kong, ast interface{}) (app *Application, err error) {
defer catch(&err)
// Plugins are dynamically embedded command-line structures.
//
// Each element in the Plugins list *must* be a pointer to a structure.
type Plugins []any
func build(k *Kong, ast any) (app *Application, err error) {
v := reflect.ValueOf(ast)
iv := reflect.Indirect(v)
if v.Kind() != reflect.Ptr || iv.Kind() != reflect.Struct {
@@ -20,10 +25,17 @@ func build(k *Kong, ast interface{}) (app *Application, err error) {
for _, flag := range extraFlags {
seenFlags[flag.Name] = true
}
node := buildNode(k, iv, ApplicationNode, seenFlags)
node, err := buildNode(k, iv, ApplicationNode, newEmptyTag(), seenFlags)
if err != nil {
return nil, err
}
if len(node.Positional) > 0 && len(node.Children) > 0 {
return nil, fmt.Errorf("can't mix positional arguments and branching arguments on %T", ast)
}
if provider, ok := v.Interface().(HelpProvider); ok {
node.Detail = provider.Help()
}
app.Node = node
app.Node.Flags = append(extraFlags, app.Node.Flags...)
app.Tag = newEmptyTag()
@@ -41,97 +53,216 @@ type flattenedField struct {
tag *Tag
}
func flattenedFields(v reflect.Value) (out []flattenedField) {
func flattenedFields(v reflect.Value, ptag *Tag) (out []flattenedField, err error) {
v = reflect.Indirect(v)
if v.Kind() != reflect.Struct {
return out, nil
}
ignored := map[string]bool{}
for i := 0; i < v.NumField(); i++ {
ft := v.Type().Field(i)
fv := v.Field(i)
tag := parseTag(fv, ft)
if tag.Ignored {
tag, err := parseTag(v, ft)
if err != nil {
return nil, err
}
if tag.Ignored || ignored[ft.Name] {
ignored[ft.Name] = true
continue
}
if ft.Anonymous || tag.Embed {
if fv.Kind() == reflect.Interface {
fv = fv.Elem()
// Assign group if it's not already set.
if tag.Group == "" {
tag.Group = ptag.Group
}
// Accumulate prefixes.
tag.Prefix = ptag.Prefix + tag.Prefix
tag.EnvPrefix = ptag.EnvPrefix + tag.EnvPrefix
tag.XorPrefix = ptag.XorPrefix + tag.XorPrefix
// Combine parent vars.
tag.Vars = ptag.Vars.CloneWith(tag.Vars)
// Command and embedded structs can be pointers, so we hydrate them now.
if (tag.Cmd || tag.Embed) && ft.Type.Kind() == reflect.Ptr {
fv = reflect.New(ft.Type.Elem()).Elem()
v.FieldByIndex(ft.Index).Set(fv.Addr())
}
if !ft.Anonymous && !tag.Embed {
if fv.CanSet() {
field := flattenedField{field: ft, value: fv, tag: tag}
out = append(out, field)
}
sub := flattenedFields(fv)
for _, subf := range sub {
// Assign parent if it's not already set.
if subf.tag.Group == "" {
subf.tag.Group = tag.Group
continue
}
// Embedded type.
if fv.Kind() == reflect.Interface {
fv = fv.Elem()
} else if fv.Type() == reflect.TypeOf(Plugins{}) {
for i := 0; i < fv.Len(); i++ {
fields, ferr := flattenedFields(fv.Index(i).Elem(), tag)
if ferr != nil {
return nil, ferr
}
// Accumulate prefixes.
subf.tag.Prefix = tag.Prefix + subf.tag.Prefix
// Combine parent vars.
subf.tag.Vars = tag.Vars.CloneWith(subf.tag.Vars)
out = append(out, fields...)
}
out = append(out, sub...)
continue
}
if !fv.CanSet() {
continue
sub, err := flattenedFields(fv, tag)
if err != nil {
return nil, err
}
out = append(out, flattenedField{field: ft, value: fv, tag: tag})
out = append(out, sub...)
}
return out
out = removeIgnored(out, ignored)
return out, nil
}
func buildNode(k *Kong, v reflect.Value, typ NodeType, seenFlags map[string]bool) *Node {
func removeIgnored(fields []flattenedField, ignored map[string]bool) []flattenedField {
j := 0
for i := 0; i < len(fields); i++ {
if ignored[fields[i].field.Name] {
continue
}
if i != j {
fields[j] = fields[i]
}
j++
}
if j != len(fields) {
fields = fields[:j]
}
return fields
}
// Build a Node in the Kong data model.
//
// "v" is the value to create the node from, "typ" is the output Node type.
func buildNode(k *Kong, v reflect.Value, typ NodeType, tag *Tag, seenFlags map[string]bool) (*Node, error) { //nolint:gocyclo
node := &Node{
Type: typ,
Target: v,
Tag: newEmptyTag(),
Tag: tag,
}
for _, field := range flattenedFields(v) {
fields, err := flattenedFields(v, tag)
if err != nil {
return nil, err
}
MAIN:
for _, field := range fields {
for _, r := range k.ignoreFields {
if r.MatchString(v.Type().Name() + "." + field.field.Name) {
continue MAIN
}
}
ft := field.field
fv := field.value
tag := field.tag
name := tag.Name
if name == "" {
name = tag.Prefix + strings.ToLower(dashedString(ft.Name))
name = tag.Prefix + k.flagNamer(ft.Name)
} else {
name = tag.Prefix + name
}
if len(tag.Envs) != 0 {
for i := range tag.Envs {
tag.Envs[i] = tag.EnvPrefix + tag.Envs[i]
}
}
if len(tag.Xor) != 0 {
for i := range tag.Xor {
tag.Xor[i] = tag.XorPrefix + tag.Xor[i]
}
}
if len(tag.And) != 0 {
for i := range tag.And {
tag.And[i] = tag.XorPrefix + tag.And[i]
}
}
// Nested structs are either commands or args, unless they implement the Mapper interface.
if ft.Type.Kind() == reflect.Struct && (tag.Cmd || tag.Arg) && k.registry.ForValue(fv) == nil {
if field.value.Kind() == reflect.Struct && (tag.Cmd || tag.Arg) && k.registry.ForValue(fv) == nil {
typ := CommandNode
if tag.Arg {
typ = ArgumentNode
}
buildChild(k, node, typ, v, ft, fv, tag, name, seenFlags)
err = buildChild(k, node, typ, v, ft, fv, tag, name, seenFlags)
} else {
buildField(k, node, v, ft, fv, tag, name, seenFlags)
err = buildField(k, node, v, ft, fv, tag, name, seenFlags)
}
if err != nil {
return nil, err
}
}
// Validate if there are no duplicate names
if err := checkDuplicateNames(node, v); err != nil {
return nil, err
}
// "Unsee" flags.
for _, flag := range node.Flags {
delete(seenFlags, flag.Name)
}
// Scan through argument positionals to ensure optional is never before a required.
last := true
for i, p := range node.Positional {
if !last && p.Required {
fail("argument %q can not be required after an optional", p.Name)
delete(seenFlags, "--"+flag.Name)
if flag.Short != 0 {
delete(seenFlags, "-"+string(flag.Short))
}
if negFlag := negatableFlagName(flag.Name, flag.Tag.Negatable); negFlag != "" {
delete(seenFlags, negFlag)
}
for _, aflag := range flag.Aliases {
if utf8.RuneCountInString(aflag) == 1 {
delete(seenFlags, "-"+aflag)
} else {
delete(seenFlags, "--"+aflag)
}
}
last = p.Required
p.Position = i
}
return node
if err := validatePositionalArguments(node); err != nil {
return nil, err
}
return node, nil
}
func buildChild(k *Kong, node *Node, typ NodeType, v reflect.Value, ft reflect.StructField, fv reflect.Value, tag *Tag, name string, seenFlags map[string]bool) {
child := buildNode(k, fv, typ, seenFlags)
func validatePositionalArguments(node *Node) error {
var last *Value
for i, curr := range node.Positional {
if last != nil {
// Scan through argument positionals to ensure optional is never before a required.
if !last.Required && curr.Required {
return fmt.Errorf("%s: required %q cannot come after optional %q", node.FullPath(), curr.Name, last.Name)
}
// Cumulative argument needs to be last.
if last.IsCumulative() {
return fmt.Errorf("%s: argument %q cannot come after cumulative %q", node.FullPath(), curr.Name, last.Name)
}
}
last = curr
curr.Position = i
}
return nil
}
func buildChild(k *Kong, node *Node, typ NodeType, v reflect.Value, ft reflect.StructField, fv reflect.Value, tag *Tag, name string, seenFlags map[string]bool) error {
child, err := buildNode(k, fv, typ, newEmptyTag(), seenFlags)
if err != nil {
return err
}
child.Name = name
child.Tag = tag
child.Parent = node
child.Help = tag.Help
child.Hidden = tag.Hidden
child.Group = tag.Group
child.Group = buildGroupForKey(k, tag.Group)
child.Aliases = tag.Aliases
if provider, ok := fv.Addr().Interface().(HelpProvider); ok {
child.Detail = provider.Help()
@@ -141,48 +272,68 @@ func buildChild(k *Kong, node *Node, typ NodeType, v reflect.Value, ft reflect.S
// a positional argument is provided to the child, and move it to the branching argument field.
if tag.Arg {
if len(child.Positional) == 0 {
fail("positional branch %s.%s must have at least one child positional argument named %q",
v.Type().Name(), ft.Name, name)
return failField(v, ft, "positional branch must have at least one child positional argument named %q", name)
}
if child.Positional[0].Name != name {
return failField(v, ft, "first field in positional branch must have the same name as the parent field (%s).", child.Name)
}
value := child.Positional[0]
child.Argument = child.Positional[0]
child.Positional = child.Positional[1:]
if child.Help == "" {
child.Help = value.Help
child.Help = child.Argument.Help
}
child.Name = value.Name
if child.Name != name {
fail("first field in positional branch %s.%s must have the same name as the parent field (%s).",
v.Type().Name(), ft.Name, child.Name)
}
child.Argument = value
} else {
child.Name = name
if tag.HasDefault {
if node.DefaultCmd != nil {
return failField(v, ft, "can't have more than one default command under %s", node.Summary())
}
if tag.Default != "withargs" && (len(child.Children) > 0 || len(child.Positional) > 0) {
return failField(v, ft, "default command %s must not have subcommands or arguments", child.Summary())
}
node.DefaultCmd = child
}
if tag.Passthrough {
if len(child.Children) > 0 || len(child.Flags) > 0 {
return failField(v, ft, "passthrough command %s must not have subcommands or flags", child.Summary())
}
if len(child.Positional) != 1 {
return failField(v, ft, "passthrough command %s must contain exactly one positional argument", child.Summary())
}
if !checkPassthroughArg(child.Positional[0].Target) {
return failField(v, ft, "passthrough command %s must contain exactly one positional argument of []string type", child.Summary())
}
child.Passthrough = true
}
}
node.Children = append(node.Children, child)
if len(child.Positional) > 0 && len(child.Children) > 0 {
fail("can't mix positional arguments and branching arguments on %s.%s", v.Type().Name(), ft.Name)
return failField(v, ft, "can't mix positional arguments and branching arguments")
}
return nil
}
func buildField(k *Kong, node *Node, v reflect.Value, ft reflect.StructField, fv reflect.Value, tag *Tag, name string, seenFlags map[string]bool) {
func buildField(k *Kong, node *Node, v reflect.Value, ft reflect.StructField, fv reflect.Value, tag *Tag, name string, seenFlags map[string]bool) error {
mapper := k.registry.ForNamedValue(tag.Type, fv)
if mapper == nil {
fail("unsupported field type %s.%s (of type %s)", v.Type(), ft.Name, ft.Type)
return failField(v, ft, "unsupported field type %s, perhaps missing a cmd:\"\" tag?", ft.Type)
}
value := &Value{
Name: name,
Help: tag.Help,
Default: tag.Default,
DefaultValue: reflect.New(fv.Type()).Elem(),
Mapper: mapper,
Tag: tag,
Target: fv,
Enum: tag.Enum,
Name: name,
Help: tag.Help,
OrigHelp: tag.Help,
HasDefault: tag.HasDefault,
Default: tag.Default,
DefaultValue: reflect.New(fv.Type()).Elem(),
Mapper: mapper,
Tag: tag,
Target: fv,
Enum: tag.Enum,
Passthrough: tag.Passthrough,
PassthroughMode: tag.PassthroughMode,
// Flags are optional by default, and args are required by default.
Required: (!tag.Arg && tag.Required) || (tag.Arg && !tag.Optional),
@@ -192,20 +343,80 @@ func buildField(k *Kong, node *Node, v reflect.Value, ft reflect.StructField, fv
if tag.Arg {
node.Positional = append(node.Positional, value)
} else {
if seenFlags[value.Name] {
fail("duplicate flag --%s", value.Name)
if seenFlags["--"+value.Name] {
return failField(v, ft, "duplicate flag --%s", value.Name)
}
seenFlags["--"+value.Name] = true
for _, alias := range tag.Aliases {
aliasFlag := "--" + alias
if utf8.RuneCountInString(alias) == 1 {
aliasFlag = "-" + alias
}
if seenFlags[aliasFlag] {
return failField(v, ft, "duplicate flag %s", aliasFlag)
}
seenFlags[aliasFlag] = true
}
if tag.Short != 0 {
if seenFlags["-"+string(tag.Short)] {
return failField(v, ft, "duplicate short flag -%c", tag.Short)
}
seenFlags["-"+string(tag.Short)] = true
}
if tag.Negatable != "" {
negFlag := negatableFlagName(value.Name, tag.Negatable)
if seenFlags[negFlag] {
return failField(v, ft, "duplicate negation flag %s", negFlag)
}
seenFlags[negFlag] = true
}
seenFlags[value.Name] = true
flag := &Flag{
Value: value,
Aliases: tag.Aliases,
Short: tag.Short,
PlaceHolder: tag.PlaceHolder,
Env: tag.Env,
Group: tag.Group,
Envs: tag.Envs,
Group: buildGroupForKey(k, tag.Group),
Xor: tag.Xor,
And: tag.And,
Hidden: tag.Hidden,
}
value.Flag = flag
node.Flags = append(node.Flags, flag)
}
return nil
}
func buildGroupForKey(k *Kong, key string) *Group {
if key == "" {
return nil
}
for _, group := range k.groups {
if group.Key == key {
return &group
}
}
// No group provided with kong.ExplicitGroups. We create one ad-hoc for this key.
return &Group{
Key: key,
Title: key,
}
}
func checkDuplicateNames(node *Node, v reflect.Value) error {
seenNames := make(map[string]struct{})
for _, node := range node.Children {
if _, ok := seenNames[node.Name]; ok {
name := v.Type().Name()
if name == "" {
name = "<anonymous struct>"
}
return fmt.Errorf("duplicate command name %q in command %q", node.Name, name)
}
seenNames[node.Name] = struct{}{}
}
return nil
}
+208 -22
View File
@@ -3,10 +3,63 @@ package kong
import (
"fmt"
"reflect"
"runtime"
"strings"
)
type bindings map[reflect.Type]func() (reflect.Value, error)
// binding is a single binding registered with Kong.
type binding struct {
// fn is a function that returns a value of the target type.
fn reflect.Value
// val is a value of the target type.
// Must be set if done and singleton are true.
val reflect.Value
// singleton indicates whether the binding is a singleton.
// If true, the binding will be resolved once and cached.
singleton bool
// done indicates whether a singleton binding has been resolved.
// If singleton is false, this field is ignored.
done bool
}
// newValueBinding builds a binding with an already resolved value.
func newValueBinding(v reflect.Value) *binding {
return &binding{val: v, done: true, singleton: true}
}
// newFunctionBinding builds a binding with a function
// that will return a value of the target type.
//
// The function signature must be func(...) (T, error) or func(...) T
// where parameters are recursively resolved.
func newFunctionBinding(f reflect.Value, singleton bool) *binding {
return &binding{fn: f, singleton: singleton}
}
// Get returns the pre-resolved value for the binding,
// or false if the binding is not resolved.
func (b *binding) Get() (v reflect.Value, ok bool) {
return b.val, b.done
}
// Set sets the value of the binding to the given value,
// marking it as resolved.
//
// If the binding is not a singleton, this method does nothing.
func (b *binding) Set(v reflect.Value) {
if b.singleton {
b.val = v
b.done = true
}
}
// A map of type to function that returns a value of that type.
//
// The function should have the signature func(...) (T, error). Arguments are recursively resolved.
type bindings map[reflect.Type]*binding
func (b bindings) String() string {
out := []string{}
@@ -16,14 +69,39 @@ func (b bindings) String() string {
return "bindings{" + strings.Join(out, ", ") + "}"
}
func (b bindings) add(values ...interface{}) bindings {
func (b bindings) add(values ...any) bindings {
for _, v := range values {
v := v
b[reflect.TypeOf(v)] = func() (reflect.Value, error) { return reflect.ValueOf(v), nil }
val := reflect.ValueOf(v)
b[val.Type()] = newValueBinding(val)
}
return b
}
func (b bindings) addTo(impl, iface any) {
val := reflect.ValueOf(impl)
b[reflect.TypeOf(iface).Elem()] = newValueBinding(val)
}
func (b bindings) addProvider(provider any, singleton bool) error {
pv := reflect.ValueOf(provider)
t := pv.Type()
if t.Kind() != reflect.Func {
return fmt.Errorf("%T must be a function", provider)
}
if t.NumOut() == 0 {
return fmt.Errorf("%T must be a function with the signature func(...)(T, error) or func(...) T", provider)
}
if t.NumOut() == 2 {
if t.Out(1) != reflect.TypeOf((*error)(nil)).Elem() {
return fmt.Errorf("missing error; %T must be a function with the signature func(...)(T, error) or func(...) T", provider)
}
}
rt := pv.Type().Out(0)
b[rt] = newFunctionBinding(pv, singleton)
return nil
}
// Clone and add values.
func (b bindings) clone() bindings {
out := make(bindings, len(b))
@@ -50,27 +128,135 @@ func getMethod(value reflect.Value, name string) reflect.Value {
return method
}
func callMethod(name string, v, f reflect.Value, bindings bindings) error {
in := []reflect.Value{}
t := f.Type()
if t.NumOut() != 1 || t.Out(0) != callbackReturnSignature {
return fmt.Errorf("return value of %T.%s() must be exactly \"error\"", v.Type(), name)
func getExplicitMethod(value reflect.Value, name string) reflect.Value {
if isExplicitMethod(value.Type(), name) {
return value.MethodByName(name)
}
for i := 0; i < t.NumIn(); i++ {
pt := t.In(i)
if argf, ok := bindings[pt]; ok {
argv, err := argf()
if err != nil {
return err
}
in = append(in, argv)
} else {
return fmt.Errorf("couldn't find binding of type %s for parameter %d of %s.%s(), use kong.Bind(%s)", pt, i, v.Type(), name, pt)
if value.CanAddr() && isExplicitMethod(value.Addr().Type(), name) {
return value.Addr().MethodByName(name)
}
return reflect.Value{}
}
func isExplicitMethod(t reflect.Type, name string) bool {
method, ok := t.MethodByName(name)
if !ok {
return false
}
// Promoted embedded methods are compiler-generated wrappers. The embedded
// value itself is visited separately, so skip those wrappers here.
fn := runtime.FuncForPC(method.Func.Pointer())
if fn == nil {
return true
}
file, _ := fn.FileLine(method.Func.Pointer())
return file != "<autogenerated>"
}
// getMethods gets all methods with the given name from the given value
// and any embedded fields.
//
// Returns a slice of bound methods that can be called directly.
func getMethods(value reflect.Value, name string) (methods []reflect.Value) {
walkEmbedded(value, func(v reflect.Value) {
if method := getExplicitMethod(v, name); method.IsValid() {
methods = append(methods, method)
}
})
return
}
// walkEmbedded calls visit on v and recursively on every exported field
// of v that is either a standard Go anonymous field or tagged `embed:""`.
// Pointer values are dereferenced before traversal; nil/invalid pointers
// are skipped. [Plugins] are descended into element-by-element, matching how
// [flattenedFields] treats them at build time.
func walkEmbedded(value reflect.Value, visit func(reflect.Value)) {
if value.Kind() == reflect.Pointer {
value = value.Elem()
}
out := f.Call(in)
if out[0].IsNil() {
if !value.IsValid() {
return
}
visit(value)
if value.Type() == reflect.TypeOf(Plugins{}) {
for i := 0; i < value.Len(); i++ {
walkEmbedded(value.Index(i).Elem(), visit)
}
return
}
if value.Kind() != reflect.Struct {
return
}
t := value.Type()
for i := 0; i < value.NumField(); i++ {
field := t.Field(i)
if !field.IsExported() {
continue
}
_, isEmbedded := field.Tag.Lookup("embed")
if !isEmbedded && !field.Anonymous {
continue
}
walkEmbedded(value.Field(i), visit)
}
}
func callFunction(f reflect.Value, bindings bindings) error {
if f.Kind() != reflect.Func {
return fmt.Errorf("expected function, got %s", f.Type())
}
t := f.Type()
if t.NumOut() != 1 || !t.Out(0).Implements(callbackReturnSignature) {
return fmt.Errorf("return value of %s must implement \"error\"", t)
}
out, err := callAnyFunction(f, bindings)
if err != nil {
return err
}
ferr := out[0]
if ferrv := reflect.ValueOf(ferr); !ferrv.IsValid() || ((ferrv.Kind() == reflect.Interface || ferrv.Kind() == reflect.Pointer) && ferrv.IsNil()) {
return nil
}
return out[0].Interface().(error)
return ferr.(error) //nolint:forcetypeassert
}
func callAnyFunction(f reflect.Value, bindings bindings) (out []any, err error) {
if f.Kind() != reflect.Func {
return nil, fmt.Errorf("expected function, got %s", f.Type())
}
in := []reflect.Value{}
t := f.Type()
for i := 0; i < t.NumIn(); i++ {
pt := t.In(i)
binding, ok := bindings[pt]
if !ok {
return nil, fmt.Errorf("couldn't find binding of type %s for parameter %d of %s(), use kong.Bind(%s)", pt, i, t, pt)
}
// Don't need to call the function if the value is already resolved.
if val, ok := binding.Get(); ok {
in = append(in, val)
continue
}
// Recursively resolve binding functions.
argv, err := callAnyFunction(binding.fn, bindings)
if err != nil {
return nil, fmt.Errorf("%s: %w", pt, err)
}
if ferrv := reflect.ValueOf(argv[len(argv)-1]); ferrv.IsValid() && ferrv.Type().Implements(callbackReturnSignature) && !ferrv.IsNil() {
return nil, ferrv.Interface().(error) //nolint:forcetypeassert
}
val := reflect.ValueOf(argv[0])
binding.Set(val)
in = append(in, val)
}
outv := f.Call(in)
out = make([]any, len(outv))
for i, v := range outv {
out[i] = v.Interface()
}
return out, nil
}
+24 -24
View File
@@ -13,37 +13,37 @@ import (
//
// Examples
//
// "" => [""]
// "lowercase" => ["lowercase"]
// "Class" => ["Class"]
// "MyClass" => ["My", "Class"]
// "MyC" => ["My", "C"]
// "HTML" => ["HTML"]
// "PDFLoader" => ["PDF", "Loader"]
// "AString" => ["A", "String"]
// "SimpleXMLParser" => ["Simple", "XML", "Parser"]
// "vimRPCPlugin" => ["vim", "RPC", "Plugin"]
// "GL11Version" => ["GL", "11", "Version"]
// "99Bottles" => ["99", "Bottles"]
// "May5" => ["May", "5"]
// "BFG9000" => ["BFG", "9000"]
// "BöseÜberraschung" => ["Böse", "Überraschung"]
// "Two spaces" => ["Two", " ", "spaces"]
// "BadUTF8\xe2\xe2\xa1" => ["BadUTF8\xe2\xe2\xa1"]
// "" => [""]
// "lowercase" => ["lowercase"]
// "Class" => ["Class"]
// "MyClass" => ["My", "Class"]
// "MyC" => ["My", "C"]
// "HTML" => ["HTML"]
// "PDFLoader" => ["PDF", "Loader"]
// "AString" => ["A", "String"]
// "SimpleXMLParser" => ["Simple", "XML", "Parser"]
// "vimRPCPlugin" => ["vim", "RPC", "Plugin"]
// "GL11Version" => ["GL", "11", "Version"]
// "99Bottles" => ["99", "Bottles"]
// "May5" => ["May", "5"]
// "BFG9000" => ["BFG", "9000"]
// "BöseÜberraschung" => ["Böse", "Überraschung"]
// "Two spaces" => ["Two", " ", "spaces"]
// "BadUTF8\xe2\xe2\xa1" => ["BadUTF8\xe2\xe2\xa1"]
//
// Splitting rules
//
// 1) If string is not valid UTF-8, return it without splitting as
// 1. If string is not valid UTF-8, return it without splitting as
// single item array.
// 2) Assign all unicode characters into one of 4 sets: lower case
// 2. Assign all unicode characters into one of 4 sets: lower case
// letters, upper case letters, numbers, and all other characters.
// 3) Iterate through characters of string, introducing splits
// 3. Iterate through characters of string, introducing splits
// between adjacent characters that belong to different sets.
// 4) Iterate through array of split strings, and if a given string
// 4. Iterate through array of split strings, and if a given string
// is upper case:
// if subsequent string is lower case:
// move last character of upper case string to beginning of
// lower case string
// if subsequent string is lower case:
// move last character of upper case string to beginning of
// lower case string
func camelCase(src string) (entries []string) {
// don't split invalid utf8
if !utf8.ValidString(src) {
+552 -116
View File
@@ -1,13 +1,14 @@
package kong
import (
"errors"
"fmt"
"os"
"reflect"
"sort"
"strconv"
"strings"
"github.com/pkg/errors"
"unicode/utf8"
)
// Path records the nodes and parsed values from the current command-line.
@@ -26,6 +27,9 @@ type Path struct {
// True if this Path element was created as the result of a resolver.
Resolved bool
// Remaining tokens after this node
remainder []Token
}
// Node returns the Node associated with this Path, or nil if Path is a non-Node.
@@ -43,6 +47,36 @@ func (p *Path) Node() *Node {
return nil
}
// Visitable returns the Visitable for this path element.
func (p *Path) Visitable() Visitable {
switch {
case p.App != nil:
return p.App
case p.Argument != nil:
return p.Argument
case p.Command != nil:
return p.Command
case p.Flag != nil:
return p.Flag
case p.Positional != nil:
return p.Positional
}
return nil
}
// Remainder returns the remaining unparsed args after this Path element.
func (p *Path) Remainder() []string {
args := []string{}
for _, token := range p.remainder {
args = append(args, token.String())
}
return args
}
// Context contains the current parse context.
type Context struct {
*Kong
@@ -66,14 +100,15 @@ type Context struct {
// This just constructs a new trace. To fully apply the trace you must call Reset(), Resolve(),
// Validate() and Apply().
func Trace(k *Kong, args []string) (*Context, error) {
s := Scan(args...).AllowHyphenPrefixedParameters(k.allowHyphenated)
c := &Context{
Kong: k,
Args: args,
Path: []*Path{
{App: k.Model, Flags: k.Model.Flags},
{App: k.Model, Flags: k.Model.Flags, remainder: s.PeekAll()},
},
values: map[*Value]reflect.Value{},
scan: Scan(args...),
scan: s,
bindings: bindings{},
}
c.Error = c.trace(c.Model.Node)
@@ -81,7 +116,7 @@ func Trace(k *Kong, args []string) (*Context, error) {
}
// Bind adds bindings to the Context.
func (c *Context) Bind(args ...interface{}) {
func (c *Context) Bind(args ...any) {
c.bindings.add(args...)
}
@@ -89,10 +124,29 @@ func (c *Context) Bind(args ...interface{}) {
//
// This will typically have to be called like so:
//
// BindTo(impl, (*MyInterface)(nil))
func (c *Context) BindTo(impl, iface interface{}) {
valueOf := reflect.ValueOf(impl)
c.bindings[reflect.TypeOf(iface).Elem()] = func() (reflect.Value, error) { return valueOf, nil }
// BindTo(impl, (*MyInterface)(nil))
func (c *Context) BindTo(impl, iface any) {
c.bindings.addTo(impl, iface)
}
// BindToProvider allows binding of provider functions.
//
// This is useful when the Run() function of different commands require different values that may
// not all be initialisable from the main() function.
//
// "provider" must be a function with the signature func(...) (T, error) or func(...) T,
// where ... will be recursively injected with bound values.
func (c *Context) BindToProvider(provider any) error {
return c.bindings.addProvider(provider, false /* singleton */)
}
// BindSingletonProvider allows binding of provider functions.
// The provider will be called once and the result cached.
//
// "provider" must be a function with the signature func(...) (T, error) or func(...) T,
// where ... will be recursively injected with bound values.
func (c *Context) BindSingletonProvider(provider any) error {
return c.bindings.addProvider(provider, true /* singleton */)
}
// Value returns the value for a particular path element.
@@ -133,19 +187,53 @@ func (c *Context) Empty() bool {
}
// Validate the current context.
func (c *Context) Validate() error { // nolint: gocyclo
err := Visit(c.Model, func(node Visitable, next Next) error {
if value, ok := node.(*Value); ok {
if value.Enum != "" && (!value.Required || value.Default != "") {
func (c *Context) Validate() error { //nolint: gocyclo
// Only check nodes on the selected command path: an envar shared with
// another command may hold a value that is invalid there.
for _, path := range c.Path {
node := path.Node()
if node == nil {
continue
}
for _, value := range node.Values() {
ok := atLeastOneEnvSet(value.Tag.Envs)
if value.Enum != "" && (!value.Required || value.HasDefault || (len(value.Tag.Envs) != 0 && ok)) {
if err := checkEnum(value, value.Target); err != nil {
return err
}
}
}
return next(nil)
})
if err != nil {
return err
}
for _, el := range c.Path {
var (
value reflect.Value
desc string
)
switch node := el.Visitable().(type) {
case *Value:
value = node.Target
desc = node.ShortSummary()
case *Flag:
value = node.Target
desc = node.ShortSummary()
case *Application:
value = node.Target
desc = ""
case *Node:
value = node.Target
desc = node.Path()
}
for _, validate := range getValidators(value) {
if err := validate.Validate(c); err != nil {
if desc != "" {
return fmt.Errorf("%s: %w", desc, err)
}
return err
}
}
}
for _, resolver := range c.combineResolvers() {
if err := resolver.Validate(c.Model); err != nil {
@@ -190,7 +278,7 @@ func (c *Context) Validate() error { // nolint: gocyclo
if err := checkMissingPositionals(positionals, node.Positional); err != nil {
return err
}
if err := checkXorDuplicates(c.Path); err != nil {
if err := checkXorDuplicatedAndAndMissing(c.Path); err != nil {
return err
}
@@ -237,7 +325,7 @@ func (c *Context) AddResolver(resolver Resolver) {
}
// FlagValue returns the set value of a flag if it was encountered and exists, or its default value.
func (c *Context) FlagValue(flag *Flag) interface{} {
func (c *Context) FlagValue(flag *Flag) any {
for _, trace := range c.Path {
if trace.Flag == flag {
v, ok := c.values[trace.Flag.Value]
@@ -255,22 +343,73 @@ func (c *Context) FlagValue(flag *Flag) interface{} {
// Reset recursively resets values to defaults (as specified in the grammar) or the zero value.
func (c *Context) Reset() error {
selected := c.selectedValues()
return Visit(c.Model.Node, func(node Visitable, next Next) error {
if value, ok := node.(*Value); ok {
return next(value.Reset())
value, ok := node.(*Value)
if !ok {
return next(nil)
}
return next(nil)
err := value.Reset()
if err != nil && !selected[value] {
// An envar shared with a node outside the selected command path
// may not parse there; that must not fail this parse.
value.Target.Set(reflect.Zero(value.Target.Type()))
err = nil
}
return next(err)
})
}
func (c *Context) trace(node *Node) (err error) { // nolint: gocyclo
// selectedValues returns the set of values attached to nodes on the traced
// command path.
func (c *Context) selectedValues() map[*Value]bool {
selected := map[*Value]bool{}
for _, path := range c.Path {
if node := path.Node(); node != nil {
for _, value := range node.Values() {
selected[value] = true
}
}
}
return selected
}
func (c *Context) endParsing() {
args := []string{}
for {
token := c.scan.Pop()
if token.Type == EOLToken {
break
}
args = append(args, token.String())
}
// Note: tokens must be pushed in reverse order.
for i := range args {
c.scan.PushTyped(args[len(args)-1-i], PositionalArgumentToken)
}
}
//nolint:maintidx
func (c *Context) trace(node *Node) (err error) { //nolint: gocyclo
positional := 0
node.Active = true
flags := []*Flag{}
for _, group := range node.AllFlags(false) {
flagNode := node
if node.DefaultCmd != nil && node.DefaultCmd.Tag.Default == "withargs" {
// Add flags of the default command if the current node has one
// and that default command allows args / flags without explicitly
// naming the command on the CLI.
flagNode = node.DefaultCmd
}
for _, group := range flagNode.AllFlags(false) {
flags = append(flags, group...)
}
if node.Passthrough {
c.endParsing()
}
for !c.scan.Peek().IsEOL() {
token := c.scan.Peek()
switch token.Type {
@@ -281,24 +420,17 @@ func (c *Context) trace(node *Node) (err error) { // nolint: gocyclo
switch {
case v == "-":
fallthrough
default: // nolint
default: //nolint
c.scan.Pop()
c.scan.PushTyped(token.Value, PositionalArgumentToken)
// Indicates end of parsing. All remaining arguments are treated as positional arguments only.
case v == "--":
c.scan.Pop()
args := []string{}
for {
token = c.scan.Pop()
if token.Type == EOLToken {
break
}
args = append(args, token.String())
}
// Note: tokens must be pushed in reverse order.
for i := range args {
c.scan.PushTyped(args[len(args)-1-i], PositionalArgumentToken)
c.endParsing()
// Pop the -- token unless the next positional argument accepts passthrough arguments.
if !(positional < len(node.Positional) && node.Positional[positional].Passthrough) {
c.scan.Pop()
}
// Long flag.
@@ -335,12 +467,22 @@ func (c *Context) trace(node *Node) (err error) { // nolint: gocyclo
case FlagToken:
if err := c.parseFlag(flags, token.String()); err != nil {
return err
if isUnknownFlagError(err) && positional < len(node.Positional) && node.Positional[positional].PassthroughMode == PassThroughModeAll {
c.scan.Pop()
c.scan.PushTyped(token.String(), PositionalArgumentToken)
} else {
return err
}
}
case ShortFlagToken:
if err := c.parseFlag(flags, token.String()); err != nil {
return err
if isUnknownFlagError(err) && positional < len(node.Positional) && node.Positional[positional].PassthroughMode == PassThroughModeAll {
c.scan.Pop()
c.scan.PushTyped(token.String(), PositionalArgumentToken)
} else {
return err
}
}
case FlagValueToken:
@@ -352,6 +494,12 @@ func (c *Context) trace(node *Node) (err error) { // nolint: gocyclo
// Ensure we've consumed all positional arguments.
if positional < len(node.Positional) {
arg := node.Positional[positional]
if arg.Passthrough {
c.endParsing()
}
arg.Active = true
err := arg.Parse(c.scan, c.getValue(arg))
if err != nil {
return err
@@ -359,11 +507,30 @@ func (c *Context) trace(node *Node) (err error) { // nolint: gocyclo
c.Path = append(c.Path, &Path{
Parent: node,
Positional: arg,
remainder: c.scan.PeekAll(),
})
positional++
break
}
// Assign token value to a branch name if tagged as an alias
// An alias will be ignored in the case of an existing command
cmds := make(map[string]bool)
for _, branch := range node.Children {
if branch.Type == CommandNode {
cmds[branch.Name] = true
}
}
for _, branch := range node.Children {
for _, a := range branch.Aliases {
_, ok := cmds[a]
if token.Value == a && !ok {
token.Value = branch.Name
break
}
}
}
// After positional arguments have been consumed, check commands next...
for _, branch := range node.Children {
if branch.Type == CommandNode && !branch.Hidden {
@@ -372,9 +539,10 @@ func (c *Context) trace(node *Node) (err error) { // nolint: gocyclo
if branch.Type == CommandNode && branch.Name == token.Value {
c.scan.Pop()
c.Path = append(c.Path, &Path{
Parent: node,
Command: branch,
Flags: branch.Flags,
Parent: node,
Command: branch,
Flags: branch.Flags,
remainder: c.scan.PeekAll(),
})
return c.trace(branch)
}
@@ -386,15 +554,28 @@ func (c *Context) trace(node *Node) (err error) { // nolint: gocyclo
arg := branch.Argument
if err := arg.Parse(c.scan, c.getValue(arg)); err == nil {
c.Path = append(c.Path, &Path{
Parent: node,
Argument: branch,
Flags: branch.Flags,
Parent: node,
Argument: branch,
Flags: branch.Flags,
remainder: c.scan.PeekAll(),
})
return c.trace(branch)
}
}
}
// If there is a default command that allows args and nothing else
// matches, take the branch of the default command
if node.DefaultCmd != nil && node.DefaultCmd.Tag.Default == "withargs" {
c.Path = append(c.Path, &Path{
Parent: node,
Command: node.DefaultCmd,
Flags: node.DefaultCmd.Flags,
remainder: c.scan.PeekAll(),
})
return c.trace(node.DefaultCmd)
}
return findPotentialCandidates(token.String(), candidates, "unexpected argument %s", token)
default:
return fmt.Errorf("unexpected token %s", token)
@@ -403,29 +584,26 @@ func (c *Context) trace(node *Node) (err error) { // nolint: gocyclo
return c.maybeSelectDefault(flags, node)
}
// IgnoreDefault can be implemented by flags that want to be applied before any default commands.
type IgnoreDefault interface {
IgnoreDefault()
}
// End of the line, check for a default command, but only if we're not displaying help,
// otherwise we'd only ever display the help for the default command.
func (c *Context) maybeSelectDefault(flags []*Flag, node *Node) error {
for _, flag := range flags {
if flag.Name == "help" && flag.Set {
if _, ok := flag.Target.Interface().(IgnoreDefault); ok && flag.Set {
return nil
}
}
var defaultNode *Path
for _, child := range node.Children {
if child.Type == CommandNode && child.Tag.Default != "" {
if defaultNode != nil {
return fmt.Errorf("can't have more than one default command under %s", node.Summary())
}
defaultNode = &Path{
Parent: child,
Command: child,
Flags: child.Flags,
}
}
}
if defaultNode != nil {
c.Path = append(c.Path, defaultNode)
if node.DefaultCmd != nil {
c.Path = append(c.Path, &Path{
Parent: node.DefaultCmd,
Command: node.DefaultCmd,
Flags: node.DefaultCmd.Flags,
remainder: c.scan.PeekAll(),
})
}
return nil
}
@@ -446,11 +624,11 @@ func (c *Context) Resolve() error {
}
// Pick the last resolved value.
var selected interface{}
var selected any
for _, resolver := range resolvers {
s, err := resolver.Resolve(c, path, flag)
if err != nil {
return errors.Wrap(err, flag.ShortSummary())
return fmt.Errorf("%s: %w", flag.ShortSummary(), err)
}
if s == nil {
continue
@@ -469,12 +647,13 @@ func (c *Context) Resolve() error {
return err
}
inserted = append(inserted, &Path{
Flag: flag,
Resolved: true,
Flag: flag,
Resolved: true,
remainder: c.scan.PeekAll(),
})
}
}
c.Path = append(inserted, c.Path...)
c.Path = append(c.Path, inserted...)
return nil
}
@@ -490,6 +669,15 @@ func (c *Context) getValue(value *Value) reflect.Value {
v, ok := c.values[value]
if !ok {
v = reflect.New(value.Target.Type()).Elem()
switch v.Kind() {
case reflect.Ptr:
v.Set(reflect.New(v.Type().Elem()))
case reflect.Slice:
v.Set(reflect.MakeSlice(v.Type(), 0, 0))
case reflect.Map:
v.Set(reflect.MakeMap(v.Type()))
default:
}
c.values[value] = v
}
return v
@@ -546,32 +734,92 @@ func (c *Context) Apply() (string, error) {
return strings.Join(path, " "), nil
}
func flipBoolValue(value reflect.Value) error {
if value.Kind() == reflect.Bool {
value.SetBool(!value.Bool())
return nil
}
if value.Kind() == reflect.Ptr {
if !value.IsNil() {
return flipBoolValue(value.Elem())
}
return nil
}
return fmt.Errorf("cannot negate a value of %s", value.Type().String())
}
func (c *Context) parseFlag(flags []*Flag, match string) (err error) {
defer catch(&err)
candidates := []string{}
for _, flag := range flags {
long := "--" + flag.Name
short := "-" + string(flag.Short)
matched := long == match
candidates = append(candidates, long)
if flag.Short != 0 {
short := "-" + string(flag.Short)
matched = matched || (short == match)
candidates = append(candidates, short)
}
if short != match && long != match {
for _, alias := range flag.Aliases {
aliasFlag := "--" + alias
if utf8.RuneCountInString(alias) == 1 {
aliasFlag = "-" + alias
}
matched = matched || (aliasFlag == match)
candidates = append(candidates, aliasFlag)
}
neg := negatableFlagName(flag.Name, flag.Tag.Negatable)
if !matched && match != neg {
continue
}
// Found a matching flag.
c.scan.Pop()
if match == neg && flag.Tag.Negatable != "" {
flag.Negated = true
}
err := flag.Parse(c.scan, c.getValue(flag.Value))
if err != nil {
if e, ok := errors.Cause(err).(*expectedError); ok && e.token.InferredType().IsAny(FlagToken, ShortFlagToken) {
return errors.Errorf("%s; perhaps try %s=%q?", err, flag.ShortSummary(), e.token)
var expected *expectedError
if errors.As(err, &expected) && expected.token.InferredType().IsAny(FlagToken, ShortFlagToken) {
return fmt.Errorf("%s; perhaps try %s=%q?", err.Error(), flag.ShortSummary(), expected.token)
}
return err
}
c.Path = append(c.Path, &Path{Flag: flag})
if flag.Negated {
value := c.getValue(flag.Value)
err := flipBoolValue(value)
if err != nil {
return err
}
flag.Value.Apply(value)
}
c.Path = append(c.Path, &Path{
Flag: flag,
remainder: c.scan.PeekAll(),
})
return nil
}
return findPotentialCandidates(match, candidates, "unknown flag %s", match)
return &unknownFlagError{Cause: findPotentialCandidates(match, candidates, "unknown flag %s", match)}
}
func isUnknownFlagError(err error) bool {
var unknown *unknownFlagError
return errors.As(err, &unknown)
}
type unknownFlagError struct{ Cause error }
func (e *unknownFlagError) Unwrap() error { return e.Cause }
func (e *unknownFlagError) Error() string { return e.Cause.Error() }
// Call an arbitrary function filling arguments with bound values.
func (c *Context) Call(fn any, binds ...any) (out []any, err error) {
fv := reflect.ValueOf(fn)
bindings := c.Kong.bindings.clone().add(binds...).add(c).merge(c.bindings)
return callAnyFunction(fv, bindings)
}
// RunNode calls the Run() method on an arbitrary node.
@@ -580,7 +828,7 @@ func (c *Context) parseFlag(flags []*Flag, match string) (err error) {
//
// Any passed values will be bindable to arguments of the target Run() method. Additionally,
// all parent nodes in the command structure will be bound.
func (c *Context) RunNode(node *Node, binds ...interface{}) (err error) {
func (c *Context) RunNode(node *Node, binds ...any) (err error) {
type targetMethod struct {
node *Node
method reflect.Value
@@ -593,6 +841,19 @@ func (c *Context) RunNode(node *Node, binds ...interface{}) (err error) {
methodBinds = methodBinds.clone()
for p := node; p != nil; p = p.Parent {
methodBinds = methodBinds.add(p.Target.Addr().Interface())
// Try value and pointer to value.
for _, p := range []reflect.Value{p.Target, p.Target.Addr()} {
t := p.Type()
for i := 0; i < p.NumMethod(); i++ {
methodt := t.Method(i)
if strings.HasPrefix(methodt.Name, "Provide") {
method := p.Method(i)
if err := methodBinds.addProvider(method.Interface(), false /* singleton */); err != nil {
return fmt.Errorf("%s.%s: %w", t.Name(), methodt.Name, err)
}
}
}
}
}
if method.IsValid() {
methods = append(methods, targetMethod{node, method, methodBinds})
@@ -601,13 +862,8 @@ func (c *Context) RunNode(node *Node, binds ...interface{}) (err error) {
if len(methods) == 0 {
return fmt.Errorf("no Run() method found in hierarchy of %s", c.Selected().Summary())
}
_, err = c.Apply()
if err != nil {
return err
}
for _, method := range methods {
if err = callMethod("Run", method.node.Target, method.method, method.binds); err != nil {
if err = callFunction(method.method, method.binds); err != nil {
return err
}
}
@@ -618,13 +874,27 @@ func (c *Context) RunNode(node *Node, binds ...interface{}) (err error) {
//
// Any passed values will be bindable to arguments of the target Run() method. Additionally,
// all parent nodes in the command structure will be bound.
func (c *Context) Run(binds ...interface{}) (err error) {
defer catch(&err)
func (c *Context) Run(binds ...any) (err error) {
node := c.Selected()
if node == nil {
return fmt.Errorf("no command selected")
if len(c.Path) == 0 {
return fmt.Errorf("no command selected")
}
selected := c.Path[0].Node()
if selected.Type == ApplicationNode {
method := getMethod(selected.Target, "Run")
if method.IsValid() {
node = selected
}
}
if node == nil {
return fmt.Errorf("no command selected")
}
}
return c.RunNode(node, binds...)
runErr := c.RunNode(node, binds...)
err = c.Kong.applyHook(c, "AfterRun")
return errors.Join(runErr, err)
}
// PrintUsage to Kong's stdout.
@@ -633,25 +903,82 @@ func (c *Context) Run(binds ...interface{}) (err error) {
func (c *Context) PrintUsage(summary bool) error {
options := c.helpOptions
options.Summary = summary
_ = c.help(options, c)
return nil
return c.printHelp(options)
}
func (c *Context) printHelp(options HelpOptions) error {
options.ValueFormatter = c.Kong.helpFormatter
return c.help(options, c)
}
func checkMissingFlags(flags []*Flag) error {
xorGroupSet := map[string]bool{}
xorGroup := map[string][]string{}
andGroupSet := map[string]bool{}
andGroup := map[string][]string{}
missing := []string{}
andGroupRequired := getRequiredAndGroupMap(flags)
for _, flag := range flags {
for _, and := range flag.And {
flag.Required = andGroupRequired[and]
}
if flag.Set {
for _, xor := range flag.Xor {
xorGroupSet[xor] = true
}
for _, and := range flag.And {
andGroupSet[and] = true
}
}
if !flag.Required || flag.Set {
continue
}
missing = append(missing, flag.Summary())
if len(flag.Xor) > 0 || len(flag.And) > 0 {
for _, xor := range flag.Xor {
if xorGroupSet[xor] {
continue
}
xorGroup[xor] = append(xorGroup[xor], flag.Summary())
}
for _, and := range flag.And {
andGroup[and] = append(andGroup[and], flag.Summary())
}
} else {
missing = append(missing, flag.Summary())
}
}
for xor, flags := range xorGroup {
if !xorGroupSet[xor] && len(flags) > 1 {
missing = append(missing, strings.Join(flags, " or "))
}
}
for _, flags := range andGroup {
if len(flags) > 1 {
missing = append(missing, strings.Join(flags, " and "))
}
}
if len(missing) == 0 {
return nil
}
sort.Strings(missing)
return fmt.Errorf("missing flags: %s", strings.Join(missing, ", "))
}
func getRequiredAndGroupMap(flags []*Flag) map[string]bool {
andGroupRequired := map[string]bool{}
for _, flag := range flags {
for _, and := range flag.And {
if flag.Required {
andGroupRequired[and] = true
}
}
}
return andGroupRequired
}
func checkMissingChildren(node *Node) error {
missing := []string{}
@@ -665,7 +992,10 @@ func checkMissingChildren(node *Node) error {
missing = append(missing, strconv.Quote(strings.Join(missingArgs, " ")))
}
haveDefault := 0
// A node with a Run() method may run on its own, so it does not require one
// of its subcommands to be selected.
runnable := node.Target.IsValid() && getMethod(node.Target, "Run").IsValid()
for _, child := range node.Children {
if child.Hidden {
continue
@@ -675,20 +1005,11 @@ func checkMissingChildren(node *Node) error {
continue
}
missing = append(missing, strconv.Quote(child.Summary()))
} else {
if child.Tag.Default != "" {
if len(child.Children) > 0 {
return fmt.Errorf("default command %s must not have subcommands or arguments", child.Summary())
}
haveDefault++
}
} else if !runnable {
missing = append(missing, strconv.Quote(child.Name))
}
}
if haveDefault > 1 {
return fmt.Errorf("more than one default command found under %s", node.Summary())
}
if len(missing) == 0 || haveDefault > 0 {
if len(missing) == 0 {
return nil
}
@@ -698,7 +1019,7 @@ func checkMissingChildren(node *Node) error {
if len(missing) == 1 {
return fmt.Errorf("expected %s", missing[0])
}
return fmt.Errorf("expected one of %s", strings.Join(missing, ", "))
return fmt.Errorf("expected one of %s", strings.Join(missing, ", "))
}
// If we're missing any positionals and they're required, return an error.
@@ -715,7 +1036,17 @@ func checkMissingPositionals(positional int, values []*Value) error {
missing := []string{}
for ; positional < len(values); positional++ {
missing = append(missing, "<"+values[positional].Name+">")
arg := values[positional]
// TODO(aat): Fix hardcoding of these env checks all over the place :\
if len(arg.Tag.Envs) != 0 {
if atLeastOneEnvSet(arg.Tag.Envs) {
continue
}
}
missing = append(missing, "<"+arg.Name+">")
}
if len(missing) == 0 {
return nil
}
return fmt.Errorf("missing positional arguments %s", strings.Join(missing, " "))
}
@@ -731,23 +1062,51 @@ func checkEnum(value *Value, target reflect.Value) error {
return nil
case reflect.Map, reflect.Struct:
return errors.Errorf("enum can only be applied to a slice or value")
return errors.New("enum can only be applied to a slice or value")
default:
enumMap := value.EnumMap()
v := fmt.Sprintf("%v", target)
if enumMap[v] {
case reflect.Ptr:
if target.IsNil() {
return nil
}
return checkEnum(value, target.Elem())
default:
enumSlice := value.EnumSlice()
v := fmt.Sprintf("%v", target)
enums := []string{}
for enum := range enumMap {
for _, enum := range enumSlice {
if enum == v {
return nil
}
enums = append(enums, fmt.Sprintf("%q", enum))
}
sort.Strings(enums)
return fmt.Errorf("%s must be one of %s but got %q", value.ShortSummary(), strings.Join(enums, ","), target.Interface())
return fmt.Errorf("%s must be one of %s but got %q", value.ShortSummary(), strings.Join(enums, ","), fmt.Sprintf("%v", target.Interface()))
}
}
func checkPassthroughArg(target reflect.Value) bool {
typ := target.Type()
switch typ.Kind() {
case reflect.Slice:
return typ.Elem().Kind() == reflect.String
default:
return false
}
}
func checkXorDuplicatedAndAndMissing(paths []*Path) error {
errs := []string{}
if err := checkXorDuplicates(paths); err != nil {
errs = append(errs, err.Error())
}
if err := checkAndMissing(paths); err != nil {
errs = append(errs, err.Error())
}
if len(errs) > 0 {
return errors.New(strings.Join(errs, ", "))
}
return nil
}
func checkXorDuplicates(paths []*Path) error {
for _, path := range paths {
seen := map[string]*Flag{}
@@ -755,19 +1114,50 @@ func checkXorDuplicates(paths []*Path) error {
if !flag.Set {
continue
}
if flag.Xor == "" {
continue
for _, xor := range flag.Xor {
if seen[xor] != nil {
return fmt.Errorf("--%s and --%s can't be used together", seen[xor].Name, flag.Name)
}
seen[xor] = flag
}
if seen[flag.Xor] != nil {
return fmt.Errorf("--%s and --%s can't be used together", seen[flag.Xor].Name, flag.Name)
}
seen[flag.Xor] = flag
}
}
return nil
}
func findPotentialCandidates(needle string, haystack []string, format string, args ...interface{}) error {
func checkAndMissing(paths []*Path) error {
for _, path := range paths {
missingMsgs := []string{}
andGroups := map[string][]*Flag{}
for _, flag := range path.Flags {
for _, and := range flag.And {
andGroups[and] = append(andGroups[and], flag)
}
}
for _, flags := range andGroups {
oneSet := false
notSet := []*Flag{}
flagNames := []string{}
for _, flag := range flags {
flagNames = append(flagNames, flag.Name)
if flag.Set {
oneSet = true
} else {
notSet = append(notSet, flag)
}
}
if len(notSet) > 0 && oneSet {
missingMsgs = append(missingMsgs, fmt.Sprintf("--%s must be used together", strings.Join(flagNames, " and --")))
}
}
if len(missingMsgs) > 0 {
return fmt.Errorf("%s", strings.Join(missingMsgs, ", "))
}
}
return nil
}
func findPotentialCandidates(needle string, haystack []string, format string, args ...any) error {
if len(haystack) == 0 {
return fmt.Errorf(format, args...)
}
@@ -785,3 +1175,49 @@ func findPotentialCandidates(needle string, haystack []string, format string, ar
}
return fmt.Errorf("%s", prefix)
}
type validatable interface{ Validate() error }
type extendedValidatable interface {
Validate(kctx *Context) error
}
// Proxy a validatable function to the extendedValidatable interface
type validatableFunc func() error
func (f validatableFunc) Validate(kctx *Context) error { return f() }
func isValidatable(v reflect.Value) extendedValidatable {
if !v.IsValid() || (v.Kind() == reflect.Ptr || v.Kind() == reflect.Slice || v.Kind() == reflect.Map) && v.IsNil() {
return nil
}
if validate, ok := v.Interface().(validatable); ok {
return validatableFunc(validate.Validate)
}
if validate, ok := v.Interface().(extendedValidatable); ok {
return validate
}
if v.CanAddr() {
return isValidatable(v.Addr())
}
return nil
}
// getValidators returns validators implemented by v and by any embedded fields,
// matching how hooks are discovered (see getMethods).
func getValidators(v reflect.Value) (validators []extendedValidatable) {
walkEmbedded(v, func(v reflect.Value) {
if validate := isValidatable(v); validate != nil {
validators = append(validators, validate)
}
})
return
}
func atLeastOneEnvSet(envs []string) bool {
for _, env := range envs {
if _, ok := os.LookupEnv(env); ok {
return true
}
}
return false
}
+1 -1
View File
@@ -1,7 +1,7 @@
package kong
// ApplyDefaults if they are not already set.
func ApplyDefaults(target interface{}, options ...Option) error {
func ApplyDefaults(target any, options ...Option) error {
app, err := New(target, options...)
if err != nil {
return err
+17 -17
View File
@@ -2,31 +2,31 @@
//
// Here's an example:
//
// shell rm [-f] [-r] <paths> ...
// shell ls [<paths> ...]
// shell rm [-f] [-r] <paths> ...
// shell ls [<paths> ...]
//
// This can be represented by the following command-line structure:
//
// package main
// package main
//
// import "github.com/alecthomas/kong"
// import "github.com/alecthomas/kong"
//
// var CLI struct {
// Rm struct {
// Force bool `short:"f" help:"Force removal."`
// Recursive bool `short:"r" help:"Recursively remove files."`
// var CLI struct {
// Rm struct {
// Force bool `short:"f" help:"Force removal."`
// Recursive bool `short:"r" help:"Recursively remove files."`
//
// Paths []string `arg help:"Paths to remove." type:"path"`
// } `cmd help:"Remove files."`
// Paths []string `arg help:"Paths to remove." type:"path"`
// } `cmd help:"Remove files."`
//
// Ls struct {
// Paths []string `arg optional help:"Paths to list." type:"path"`
// } `cmd help:"List paths."`
// }
// Ls struct {
// Paths []string `arg optional help:"Paths to list." type:"path"`
// } `cmd help:"List paths."`
// }
//
// func main() {
// kong.Parse(&CLI)
// }
// func main() {
// kong.Parse(&CLI)
// }
//
// See https://github.com/alecthomas/kong for details.
package kong
+12 -3
View File
@@ -5,8 +5,17 @@ package kong
// It contains the parse Context that triggered the error.
type ParseError struct {
error
Context *Context
Context *Context
exitCode int
}
// Cause returns the original cause of the error.
func (p *ParseError) Cause() error { return p.error }
// Unwrap returns the original cause of the error.
func (p *ParseError) Unwrap() error { return p.error }
// ExitCode returns the status that Kong should exit with if it fails with a ParseError.
func (p *ParseError) ExitCode() int {
if p.exitCode == 0 {
return exitNotOk
}
return p.exitCode
}
+32
View File
@@ -0,0 +1,32 @@
package kong
import "errors"
const (
exitOk = 0
exitNotOk = 1
// Semantic exit codes from https://github.com/square/exit?tab=readme-ov-file#about
exitUsageError = 80
)
// ExitCoder is an interface that may be implemented by an error value to
// provide an integer exit code. The method ExitCode should return an integer
// that is intended to be used as the exit code for the application.
type ExitCoder interface {
ExitCode() int
}
// exitCodeFromError returns the exit code for the given error.
// If err implements the exitCoder interface, the ExitCode method is called.
// Otherwise, exitCodeFromError returns 0 if err is nil, and 1 if it is not.
func exitCodeFromError(err error) int {
var e ExitCoder
if errors.As(err, &e) {
return e.ExitCode()
} else if err == nil {
return exitOk
}
return exitNotOk
}
+1 -1
View File
@@ -5,7 +5,7 @@ import (
)
// Parse constructs a new parser and parses the default command-line.
func Parse(cli interface{}, options ...Option) *Context {
func Parse(cli any, options ...Option) *Context {
parser, err := New(cli, options...)
if err != nil {
panic(err)
-10
View File
@@ -1,10 +0,0 @@
module github.com/alecthomas/kong
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pkg/errors v0.8.1
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/stretchr/testify v1.2.2
)
go 1.13
-8
View File
@@ -1,8 +0,0 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+1 -1
View File
@@ -1,4 +1,4 @@
// +build appengine !linux,!freebsd,!darwin,!dragonfly,!netbsd,!openbsd
//go:build tinygo || appengine || (!linux && !freebsd && !darwin && !dragonfly && !netbsd && !openbsd)
package kong
+3 -3
View File
@@ -1,4 +1,4 @@
// +build !appengine,linux freebsd darwin dragonfly netbsd openbsd
//go:build !tinygo && ((!appengine && linux) || freebsd || darwin || dragonfly || netbsd || openbsd)
package kong
@@ -26,9 +26,9 @@ func guessWidth(w io.Writer) int {
if _, _, err := syscall.Syscall6(
syscall.SYS_IOCTL,
uintptr(fd), // nolint: unconvert
uintptr(fd), //nolint: unconvert
uintptr(syscall.TIOCGWINSZ),
uintptr(unsafe.Pointer(&dimensions)), // nolint: gas
uintptr(unsafe.Pointer(&dimensions)), //nolint: gas
0, 0, 0,
); err == 0 {
if dimensions[1] == 0 {
+231 -60
View File
@@ -14,12 +14,14 @@ const (
)
// Help flag.
type helpValue bool
type helpFlag bool
func (h helpValue) BeforeApply(ctx *Context) error {
func (h helpFlag) IgnoreDefault() {}
func (h helpFlag) BeforeReset(ctx *Context) error {
options := ctx.Kong.helpOptions
options.Summary = false
err := ctx.Kong.help(options, ctx)
err := ctx.printHelp(options)
if err != nil {
return err
}
@@ -41,10 +43,28 @@ type HelpOptions struct {
// Tree writes command chains in a tree structure instead of listing them separately.
Tree bool
// Place the flags after the commands listing.
FlagsLast bool
// Indenter modulates the given prefix for the next layer in the tree view.
// The following exported templates can be used: kong.SpaceIndenter, kong.LineIndenter, kong.TreeIndenter
// The kong.SpaceIndenter will be used by default.
Indenter HelpIndenter
// Don't show the help associated with subcommands
NoExpandSubcommands bool
// Clamp the help wrap width to a value smaller than the terminal width.
// If this is set to a non-positive number, the terminal width is used; otherwise,
// the min of this value or the terminal width is used.
WrapUpperBound int
// ValueFormatter is used to format the help text of flags and positional arguments.
ValueFormatter HelpValueFormatter
// NoAppDescFormat skips all formatting of app description text (wrapping, paragraph
// reflow, newline merging, etc).
NoAppDescFormat bool
}
// Apply options to Kong as a configuration option.
@@ -59,6 +79,11 @@ type HelpProvider interface {
Help() string
}
// PlaceHolderProvider can be implemented by mappers to provide custom placeholder text.
type PlaceHolderProvider interface {
PlaceHolder(flag *Flag) string
}
// HelpIndenter is used to indent new layers in the help tree.
type HelpIndenter func(prefix string) string
@@ -70,10 +95,10 @@ type HelpValueFormatter func(value *Value) string
// DefaultHelpValueFormatter is the default HelpValueFormatter.
func DefaultHelpValueFormatter(value *Value) string {
if value.Tag.Env == "" {
if len(value.Tag.Envs) == 0 || HasInterpolatedVar(value.OrigHelp, "env") {
return value.Help
}
suffix := "($" + value.Tag.Env + ")"
suffix := "(" + formatEnvs(value.Tag.Envs) + ")"
switch {
case strings.HasSuffix(value.Help, "."):
return value.Help[:len(value.Help)-1] + " " + suffix + "."
@@ -84,6 +109,21 @@ func DefaultHelpValueFormatter(value *Value) string {
}
}
// DefaultShortHelpPrinter is the default HelpPrinter for short help on error.
func DefaultShortHelpPrinter(options HelpOptions, ctx *Context) error {
w := newHelpWriter(ctx, options)
cmd := ctx.Selected()
app := ctx.Model
if cmd == nil {
w.Printf("Usage: %s%s", app.Name, app.Summary())
w.Printf(`Run "%s --help" for more information.`, app.Name)
} else {
w.Printf("Usage: %s %s", app.Name, cmd.Summary())
w.Printf(`Run "%s --help" for more information.`, cmd.FullPath())
}
return w.Write(ctx.Stdout)
}
// DefaultHelpPrinter is the default HelpPrinter.
func DefaultHelpPrinter(options HelpOptions, ctx *Context) error {
if ctx.Empty() {
@@ -126,10 +166,16 @@ func printCommand(w *helpWriter, app *Application, cmd *Command) {
}
}
func printNodeDetail(w *helpWriter, node *Node, hide bool) {
func printNodeDetail(w *helpWriter, node *Node, hide bool) { //nolint:gocyclo
if node.Help != "" {
w.Print("")
w.Wrap(node.Help)
if w.NoAppDescFormat && node.Parent == nil {
for _, line := range strings.Split(node.Help, "\n") {
w.Print(line)
}
} else {
w.Wrap(node.Help)
}
}
if w.Summary {
return
@@ -143,26 +189,60 @@ func printNodeDetail(w *helpWriter, node *Node, hide bool) {
w.Print("Arguments:")
writePositionals(w.Indent(), node.Positional)
}
if flags := node.AllFlags(true); len(flags) > 0 {
w.Print("")
w.Print("Flags:")
writeFlags(w.Indent(), flags)
}
cmds := node.Leaves(hide)
if len(cmds) > 0 {
w.Print("")
w.Print("Commands:")
if w.Tree {
writeCommandTree(w, node)
} else {
iw := w.Indent()
if w.Compact {
writeCompactCommandList(cmds, iw)
} else {
writeCommandList(cmds, iw)
printFlags := func() {
if flags := node.AllFlags(true); len(flags) > 0 {
groupedFlags := collectFlagGroups(flags)
for _, group := range groupedFlags {
w.Print("")
if group.Metadata.Title != "" {
w.Wrap(group.Metadata.Title)
}
if group.Metadata.Description != "" {
w.Indent().Wrap(group.Metadata.Description)
w.Print("")
}
writeFlags(w.Indent(), group.Flags)
}
}
}
if !w.FlagsLast {
printFlags()
}
var cmds []*Node
if w.NoExpandSubcommands {
cmds = node.Children
} else {
cmds = node.Leaves(hide)
}
if len(cmds) > 0 {
iw := w.Indent()
if w.Tree {
w.Print("")
w.Print("Commands:")
writeCommandTree(iw, node)
} else {
groupedCmds := collectCommandGroups(cmds)
for _, group := range groupedCmds {
w.Print("")
if group.Metadata.Title != "" {
w.Wrap(group.Metadata.Title)
}
if group.Metadata.Description != "" {
w.Indent().Wrap(group.Metadata.Description)
w.Print("")
}
if w.Compact {
writeCompactCommandList(group.Commands, iw)
} else {
writeCommandList(group.Commands, iw)
}
}
}
}
if w.FlagsLast {
printFlags()
}
}
func writeCommandList(cmds []*Node, iw *helpWriter) {
@@ -189,7 +269,6 @@ func writeCompactCommandList(cmds []*Node, iw *helpWriter) {
}
func writeCommandTree(w *helpWriter, node *Node) {
iw := w.Indent()
rows := make([][2]string, 0, len(node.Children)*2)
for i, cmd := range node.Children {
if cmd.Hidden {
@@ -200,27 +279,93 @@ func writeCommandTree(w *helpWriter, node *Node) {
rows = append(rows, [2]string{"", ""})
}
}
writeTwoColumns(iw, rows)
writeTwoColumns(w, rows)
}
type helpFlagGroup struct {
Metadata *Group
Flags [][]*Flag
}
func collectFlagGroups(flags [][]*Flag) []helpFlagGroup {
// Group keys in order of appearance.
groups := []*Group{}
// Flags grouped by their group key.
flagsByGroup := map[string][][]*Flag{}
for _, levelFlags := range flags {
levelFlagsByGroup := map[string][]*Flag{}
for _, flag := range levelFlags {
key := ""
if flag.Group != nil {
key = flag.Group.Key
groupAlreadySeen := false
for _, group := range groups {
if key == group.Key {
groupAlreadySeen = true
break
}
}
if !groupAlreadySeen {
groups = append(groups, flag.Group)
}
}
levelFlagsByGroup[key] = append(levelFlagsByGroup[key], flag)
}
for key, flags := range levelFlagsByGroup {
flagsByGroup[key] = append(flagsByGroup[key], flags)
}
}
out := []helpFlagGroup{}
// Ungrouped flags are always displayed first.
if ungroupedFlags, ok := flagsByGroup[""]; ok {
out = append(out, helpFlagGroup{
Metadata: &Group{Title: "Flags:"},
Flags: ungroupedFlags,
})
}
for _, group := range groups {
out = append(out, helpFlagGroup{Metadata: group, Flags: flagsByGroup[group.Key]})
}
return out
}
// nolint: unused
type helpCommandGroup struct {
Name string
Metadata *Group
Commands []*Node
}
// nolint: unused, deadcode
func collectCommandGroups(nodes []*Node) []helpCommandGroup {
groups := map[string][]*Node{}
// Groups in order of appearance.
groups := []*Group{}
// Nodes grouped by their group key.
nodesByGroup := map[string][]*Node{}
for _, node := range nodes {
groups[node.Group] = append(groups[node.Group], node)
}
out := []helpCommandGroup{}
for name, nodes := range groups {
if name == "" {
name = "Commands"
key := ""
if group := node.ClosestGroup(); group != nil {
key = group.Key
if _, ok := nodesByGroup[key]; !ok {
groups = append(groups, group)
}
}
out = append(out, helpCommandGroup{Name: name, Commands: nodes})
nodesByGroup[key] = append(nodesByGroup[key], node)
}
out := []helpCommandGroup{}
// Ungrouped nodes are always displayed first.
if ungroupedNodes, ok := nodesByGroup[""]; ok {
out = append(out, helpCommandGroup{
Metadata: &Group{Title: "Commands:"},
Commands: ungroupedNodes,
})
}
for _, group := range groups {
out = append(out, helpCommandGroup{Metadata: group, Commands: nodesByGroup[group.Key]})
}
return out
}
@@ -233,26 +378,28 @@ func printCommandSummary(w *helpWriter, cmd *Command) {
}
type helpWriter struct {
indent string
width int
lines *[]string
helpFormatter HelpValueFormatter
indent string
width int
lines *[]string
HelpOptions
}
func newHelpWriter(ctx *Context, options HelpOptions) *helpWriter {
lines := []string{}
wrapWidth := guessWidth(ctx.Stdout)
if options.WrapUpperBound > 0 && wrapWidth > options.WrapUpperBound {
wrapWidth = options.WrapUpperBound
}
w := &helpWriter{
indent: "",
width: guessWidth(ctx.Stdout),
lines: &lines,
helpFormatter: ctx.Kong.helpFormatter,
HelpOptions: options,
indent: "",
width: wrapWidth,
lines: &lines,
HelpOptions: options,
}
return w
}
func (h *helpWriter) Printf(format string, args ...interface{}) {
func (h *helpWriter) Printf(format string, args ...any) {
h.Print(fmt.Sprintf(format, args...))
}
@@ -260,8 +407,9 @@ func (h *helpWriter) Print(text string) {
*h.lines = append(*h.lines, strings.TrimRight(h.indent+text, " "))
}
// Indent returns a new helpWriter indented by two characters.
func (h *helpWriter) Indent() *helpWriter {
return &helpWriter{indent: h.indent + " ", lines: h.lines, width: h.width - 2, HelpOptions: h.HelpOptions, helpFormatter: h.helpFormatter}
return &helpWriter{indent: h.indent + " ", lines: h.lines, width: h.width - 2, HelpOptions: h.HelpOptions}
}
func (h *helpWriter) String() string {
@@ -280,7 +428,7 @@ func (h *helpWriter) Write(w io.Writer) error {
func (h *helpWriter) Wrap(text string) {
w := bytes.NewBuffer(nil)
doc.ToText(w, strings.TrimSpace(text), "", " ", h.width)
doc.ToText(w, strings.TrimSpace(text), "", " ", h.width) //nolint:staticcheck // cross-package links not possible
for _, line := range strings.Split(strings.TrimSpace(w.String()), "\n") {
h.Print(line)
}
@@ -289,7 +437,7 @@ func (h *helpWriter) Wrap(text string) {
func writePositionals(w *helpWriter, args []*Positional) {
rows := [][2]string{}
for _, arg := range args {
rows = append(rows, [2]string{arg.Summary(), w.helpFormatter(arg)})
rows = append(rows, [2]string{arg.Summary(), w.HelpOptions.ValueFormatter(arg)})
}
writeTwoColumns(w, rows)
}
@@ -311,7 +459,7 @@ func writeFlags(w *helpWriter, groups [][]*Flag) {
}
for _, flag := range group {
if !flag.Hidden {
rows = append(rows, [2]string{formatFlag(haveShort, flag), w.helpFormatter(flag.Value)})
rows = append(rows, [2]string{formatFlag(haveShort, flag), w.HelpOptions.ValueFormatter(flag.Value)})
}
}
}
@@ -335,7 +483,7 @@ func writeTwoColumns(w *helpWriter, rows [][2]string) {
for _, row := range rows {
buf := bytes.NewBuffer(nil)
doc.ToText(buf, row[1], "", strings.Repeat(" ", defaultIndent), w.width-leftSize-defaultColumnPadding)
doc.ToText(buf, row[1], "", strings.Repeat(" ", defaultIndent), w.width-leftSize-defaultColumnPadding) //nolint:staticcheck // cross-package links not possible
lines := strings.Split(strings.TrimRight(buf.String(), "\n"), "\n")
line := fmt.Sprintf("%-*s", leftSize, row[0])
@@ -355,16 +503,24 @@ func formatFlag(haveShort bool, flag *Flag) string {
flagString := ""
name := flag.Name
isBool := flag.IsBool()
isCounter := flag.IsCounter()
short := ""
if flag.Short != 0 {
flagString += fmt.Sprintf("-%c, --%s", flag.Short, name)
} else {
if haveShort {
flagString += fmt.Sprintf(" --%s", name)
} else {
flagString += fmt.Sprintf("--%s", name)
}
short = "-" + string(flag.Short) + ", "
} else if haveShort {
short = " "
}
if !isBool {
if isBool && flag.Tag.Negatable == negatableDefault {
name = "[no-]" + name
} else if isBool && flag.Tag.Negatable != "" {
name += "/" + flag.Tag.Negatable
}
flagString += fmt.Sprintf("%s--%s", short, name)
if !isBool && !isCounter {
flagString += fmt.Sprintf("=%s", flag.FormatPlaceHolder())
}
return flagString
@@ -376,6 +532,9 @@ func (h *HelpOptions) CommandTree(node *Node, prefix string) (rows [][2]string)
switch node.Type {
default:
nodeName += prefix + node.Name
if len(node.Aliases) != 0 {
nodeName += fmt.Sprintf(" (%s)", strings.Join(node.Aliases, ","))
}
case ArgumentNode:
nodeName += prefix + "<" + node.Name + ">"
}
@@ -389,6 +548,9 @@ func (h *HelpOptions) CommandTree(node *Node, prefix string) (rows [][2]string)
rows = append(rows, [2]string{prefix + arg.Summary(), arg.Help})
}
for _, subCmd := range node.Children {
if subCmd.Hidden {
continue
}
rows = append(rows, h.CommandTree(subCmd, prefix)...)
}
return
@@ -414,3 +576,12 @@ func TreeIndenter(prefix string) string {
}
return "|" + strings.Repeat(" ", defaultIndent) + prefix
}
func formatEnvs(envs []string) string {
formatted := make([]string, len(envs))
for i := range envs {
formatted[i] = "$" + envs[i]
}
return strings.Join(formatted, ", ")
}
+17 -4
View File
@@ -1,19 +1,32 @@
package kong
// BeforeResolve is a documentation-only interface describing hooks that run before values are set.
// BeforeReset is a documentation-only interface describing hooks that run before defaults values are applied.
type BeforeReset interface {
// This is not the correct signature - see README for details.
BeforeReset(args ...any) error
}
// BeforeResolve is a documentation-only interface describing hooks that run before resolvers are applied.
type BeforeResolve interface {
// This is not the correct signature - see README for details.
BeforeResolve(args ...interface{}) error
BeforeResolve(args ...any) error
}
// BeforeApply is a documentation-only interface describing hooks that run before values are set.
type BeforeApply interface {
// This is not the correct signature - see README for details.
BeforeApply(args ...interface{}) error
BeforeApply(args ...any) error
}
// AfterApply is a documentation-only interface describing hooks that run after values are set.
type AfterApply interface {
// This is not the correct signature - see README for details.
AfterApply(args ...interface{}) error
AfterApply(args ...any) error
}
// AfterRun is a documentation-only interface describing hooks that run after Run() returns.
type AfterRun interface {
// This is not the correct signature - see README for details.
// AfterRun is called after Run() returns.
AfterRun(args ...any) error
}
+17 -4
View File
@@ -5,7 +5,18 @@ import (
"regexp"
)
var interpolationRegex = regexp.MustCompile(`((?:\${([[:alpha:]_][[:word:]]*))(?:=([^}]+))?})|(\$)|([^$]+)`)
var interpolationRegex = regexp.MustCompile(`(\$\$)|((?:\${([[:alpha:]_][[:word:]]*))(?:=([^}]+))?})|(\$)|([^$]+)`)
// HasInterpolatedVar returns true if the variable "v" is interpolated in "s".
func HasInterpolatedVar(s string, v string) bool {
matches := interpolationRegex.FindAllStringSubmatch(s, -1)
for _, match := range matches {
if name := match[3]; name == v {
return true
}
}
return false
}
// Interpolate variables from vars into s for substrings in the form ${var} or ${var=default}.
func interpolate(s string, vars Vars, updatedVars map[string]string) (string, error) {
@@ -21,14 +32,16 @@ func interpolate(s string, vars Vars, updatedVars map[string]string) (string, er
}
}
for _, match := range matches {
if name := match[2]; name != "" {
if dollar := match[1]; dollar != "" {
out += "$"
} else if name := match[3]; name != "" {
value, ok := vars[name]
if !ok {
// No default value.
if match[3] == "" {
if match[4] == "" {
return "", fmt.Errorf("undefined variable ${%s}", name)
}
value = match[3]
value = match[4]
}
out += value
} else {
+211 -69
View File
@@ -1,11 +1,13 @@
package kong
import (
"errors"
"fmt"
"io"
"os"
"path/filepath"
"reflect"
"regexp"
"strings"
)
@@ -13,17 +15,16 @@ var (
callbackReturnSignature = reflect.TypeOf((*error)(nil)).Elem()
)
// Error reported by Kong.
type Error struct{ msg string }
func (e Error) Error() string { return e.msg }
func fail(format string, args ...interface{}) {
panic(Error{msg: fmt.Sprintf(format, args...)})
func failField(parent reflect.Value, field reflect.StructField, format string, args ...any) error {
name := parent.Type().Name()
if name == "" {
name = "<anonymous struct>"
}
return fmt.Errorf("%s.%s: %s", name, field.Name, fmt.Sprintf(format, args...))
}
// Must creates a new Parser or panics if there is an error.
func Must(ast interface{}, options ...Option) *Kong {
func Must(ast any, options ...Option) *Kong {
k, err := New(ast, options...)
if err != nil {
panic(err)
@@ -31,6 +32,13 @@ func Must(ast interface{}, options ...Option) *Kong {
return k
}
type usageOnError int
const (
shortUsage usageOnError = iota + 1
fullUsage
)
// Kong is the main parser type.
type Kong struct {
// Grammar model.
@@ -42,27 +50,36 @@ type Kong struct {
Stdout io.Writer
Stderr io.Writer
bindings bindings
loader ConfigurationLoader
resolvers []Resolver
registry *Registry
bindings bindings
loader ConfigurationLoader
resolvers []Resolver
registry *Registry
ignoreFields []*regexp.Regexp
noDefaultHelp bool
usageOnError bool
help HelpPrinter
helpFormatter HelpValueFormatter
helpOptions HelpOptions
helpFlag *Flag
vars Vars
noDefaultHelp bool
allowHyphenated bool
usageOnError usageOnError
help HelpPrinter
shortHelp HelpPrinter
helpFormatter HelpValueFormatter
helpOptions HelpOptions
helpFlag *Flag
groups []Group
vars Vars
flagNamer func(string) string
// Set temporarily by Options. These are applied after build().
postBuildOptions []Option
embedded []embedded
dynamicCommands []*dynamicCommand
hooks map[string][]reflect.Value
}
// New creates a new Kong parser on grammar.
//
// See the README (https://github.com/alecthomas/kong) for usage instructions.
func New(grammar interface{}, options ...Option) (*Kong, error) {
func New(grammar any, options ...Option) (*Kong, error) {
k := &Kong{
Exit: os.Exit,
Stdout: os.Stdout,
@@ -70,7 +87,12 @@ func New(grammar interface{}, options ...Option) (*Kong, error) {
registry: NewRegistry().RegisterDefaults(),
vars: Vars{},
bindings: bindings{},
hooks: make(map[string][]reflect.Value),
helpFormatter: DefaultHelpValueFormatter,
ignoreFields: make([]*regexp.Regexp, 0),
flagNamer: func(s string) string {
return strings.ToLower(dashedString(s))
},
}
options = append(options, Bind(k))
@@ -85,6 +107,10 @@ func New(grammar interface{}, options ...Option) (*Kong, error) {
k.help = DefaultHelpPrinter
}
if k.shortHelp == nil {
k.shortHelp = DefaultShortHelpPrinter
}
model, err := build(k, grammar)
if err != nil {
return k, err
@@ -93,6 +119,45 @@ func New(grammar interface{}, options ...Option) (*Kong, error) {
k.Model = model
k.Model.HelpFlag = k.helpFlag
// Embed any embedded structs.
for _, embed := range k.embedded {
tag, err := parseTagString(strings.Join(embed.tags, " "))
if err != nil {
return nil, err
}
tag.Embed = true
v := reflect.Indirect(reflect.ValueOf(embed.strct))
node, err := buildNode(k, v, CommandNode, tag, map[string]bool{})
if err != nil {
return nil, err
}
for _, child := range node.Children {
child.Parent = k.Model.Node
k.Model.Children = append(k.Model.Children, child)
}
k.Model.Flags = append(k.Model.Flags, node.Flags...)
}
// Synthesise command nodes.
for _, dcmd := range k.dynamicCommands {
tag, terr := parseTagString(strings.Join(dcmd.tags, " "))
if terr != nil {
return nil, terr
}
tag.Name = dcmd.name
tag.Help = dcmd.help
tag.Group = dcmd.group
tag.Cmd = true
v := reflect.Indirect(reflect.ValueOf(dcmd.cmd))
err = buildChild(k, k.Model.Node, CommandNode, reflect.Value{}, reflect.StructField{
Name: dcmd.name,
Type: v.Type(),
}, v, tag, dcmd.name, map[string]bool{})
if err != nil {
return nil, err
}
}
for _, option := range k.postBuildOptions {
if err = option.Apply(k); err != nil {
return nil, err
@@ -106,9 +171,42 @@ func New(grammar interface{}, options ...Option) (*Kong, error) {
k.bindings.add(k.vars)
if err = checkOverlappingXorAnd(k); err != nil {
return nil, err
}
return k, nil
}
func checkOverlappingXorAnd(k *Kong) error {
xorGroups := map[string][]string{}
andGroups := map[string][]string{}
for _, flag := range k.Model.Node.Flags {
for _, xor := range flag.Xor {
xorGroups[xor] = append(xorGroups[xor], flag.Name)
}
for _, and := range flag.And {
andGroups[and] = append(andGroups[and], flag.Name)
}
}
for xor, xorSet := range xorGroups {
for and, andSet := range andGroups {
overlappingEntries := []string{}
for _, xorTag := range xorSet {
for _, andTag := range andSet {
if xorTag == andTag {
overlappingEntries = append(overlappingEntries, xorTag)
}
}
}
if len(overlappingEntries) > 1 {
return fmt.Errorf("invalid xor and combination, %s and %s overlap with more than one: %s", xor, and, overlappingEntries)
}
}
}
return nil
}
type varStack []Vars
func (v *varStack) head() Vars { return (*v)[len(*v)-1] }
@@ -147,16 +245,44 @@ func (k *Kong) interpolateValue(value *Value, vars Vars) (err error) {
if len(value.Tag.Vars) > 0 {
vars = vars.CloneWith(value.Tag.Vars)
}
if varsContributor, ok := value.Mapper.(VarsContributor); ok {
vars = vars.CloneWith(varsContributor.Vars(value))
}
initialVars := vars.CloneWith(nil)
for n, v := range initialVars {
if vars[n], err = interpolate(v, initialVars, nil); err != nil {
return fmt.Errorf("variable %s for %s: %s", n, value.Summary(), err)
}
}
if value.Default, err = interpolate(value.Default, vars, nil); err != nil {
return fmt.Errorf("default value for %s: %s", value.Summary(), err)
}
if value.Enum, err = interpolate(value.Enum, vars, nil); err != nil {
return fmt.Errorf("enum value for %s: %s", value.Summary(), err)
}
value.Help, err = interpolate(value.Help, vars, map[string]string{
updatedVars := map[string]string{
"default": value.Default,
"enum": value.Enum,
})
}
for i, env := range value.Tag.Envs {
if value.Tag.Envs[i], err = interpolate(env, vars, updatedVars); err != nil {
return fmt.Errorf("env value for %s: %s", value.Summary(), err)
}
}
updatedVars["env"] = ""
if len(value.Tag.Envs) != 0 {
updatedVars["env"] = value.Tag.Envs[0]
}
if value.Flag != nil {
value.Flag.Envs = value.Tag.Envs
value.Flag.PlaceHolder, err = interpolate(value.Flag.PlaceHolder, vars, updatedVars)
if err != nil {
return fmt.Errorf("placeholder value for %s: %s", value.Summary(), err)
}
}
value.Help, err = interpolate(value.Help, vars, updatedVars)
if err != nil {
return fmt.Errorf("help for %s: %s", value.Summary(), err)
}
@@ -168,13 +294,14 @@ func (k *Kong) extraFlags() []*Flag {
if k.noDefaultHelp {
return nil
}
var helpTarget helpValue
var helpTarget helpFlag
value := reflect.ValueOf(&helpTarget).Elem()
helpFlag := &Flag{
Short: 'h',
Value: &Value{
Name: "help",
Help: "Show context-sensitive help.",
OrigHelp: "Show context-sensitive help.",
Target: value,
Tag: &Tag{},
Mapper: k.registry.ForValue(value),
@@ -194,13 +321,15 @@ func (k *Kong) extraFlags() []*Flag {
// Will return a ParseError if a *semantically* invalid command-line is encountered (as opposed to a syntactically
// invalid one, which will report a normal error).
func (k *Kong) Parse(args []string) (ctx *Context, err error) {
defer catch(&err)
ctx, err = Trace(k, args)
if err != nil {
return nil, err
if err != nil { // Trace is not expected to return an err
return nil, &ParseError{error: err, Context: ctx, exitCode: exitUsageError}
}
if ctx.Error != nil {
return nil, &ParseError{error: ctx.Error, Context: ctx}
return nil, &ParseError{error: ctx.Error, Context: ctx, exitCode: exitUsageError}
}
if err = k.applyHook(ctx, "BeforeReset"); err != nil {
return nil, &ParseError{error: err, Context: ctx}
}
if err = ctx.Reset(); err != nil {
return nil, &ParseError{error: err, Context: ctx}
@@ -214,11 +343,11 @@ func (k *Kong) Parse(args []string) (ctx *Context, err error) {
if err = k.applyHook(ctx, "BeforeApply"); err != nil {
return nil, &ParseError{error: err, Context: ctx}
}
if _, err = ctx.Apply(); err != nil {
if _, err = ctx.Apply(); err != nil { // Apply is not expected to return an err
return nil, &ParseError{error: err, Context: ctx}
}
if err = ctx.Validate(); err != nil {
return nil, &ParseError{error: err, Context: ctx}
return nil, &ParseError{error: err, Context: ctx, exitCode: exitUsageError}
}
if err = k.applyHook(ctx, "AfterApply"); err != nil {
return nil, &ParseError{error: err, Context: ctx}
@@ -243,23 +372,31 @@ func (k *Kong) applyHook(ctx *Context, name string) error {
default:
panic("unsupported Path")
}
method := getMethod(value, name)
if !method.IsValid() {
continue
}
binds := k.bindings.clone()
binds.add(ctx, trace)
binds.add(trace.Node().Vars().CloneWith(k.vars))
binds.merge(ctx.bindings)
if err := callMethod(name, value, method, binds); err != nil {
return err
for _, method := range k.getMethods(value, name) {
binds := k.bindings.clone()
binds.add(ctx, trace)
binds.add(trace.Node().Vars().CloneWith(k.vars))
binds.merge(ctx.bindings)
if err := callFunction(method, binds); err != nil {
return err
}
}
}
// Path[0] will always be the app root.
return k.applyHookToDefaultFlags(ctx, ctx.Path[0].Node(), name)
}
// Call hook on any unset flags with default values.
func (k *Kong) getMethods(value reflect.Value, name string) []reflect.Value {
return append(
// Identify callbacks by reflecting on value
getMethods(value, name),
// Identify callbacks that were registered with a kong.Option
k.hooks[name]...,
)
}
// Call hook on any unset flags with default values or values supplied via env.
func (k *Kong) applyHookToDefaultFlags(ctx *Context, node *Node, name string) error {
if node == nil {
return nil
@@ -271,24 +408,30 @@ func (k *Kong) applyHookToDefaultFlags(ctx *Context, node *Node, name string) er
}
binds := k.bindings.clone().add(ctx).add(node.Vars().CloneWith(k.vars))
for _, flag := range node.Flags {
if flag.Default == "" || ctx.values[flag.Value].IsValid() || !flag.Target.IsValid() {
// Flags handled here are the ones that won't show up in ctx.Path:
// they got their value from the default tag or an env var, both of
// which Reset() applies straight to the target without touching the
// parse path. Anything actually parsed off argv or set by a resolver
// shows up in ctx.values and is covered by the main hook loop.
if ctx.values[flag.Value].IsValid() || !flag.Target.IsValid() {
continue
}
method := getMethod(flag.Target, name)
if !method.IsValid() {
if !flag.HasDefault && !atLeastOneEnvSet(flag.Tag.Envs) {
continue
}
path := &Path{Flag: flag}
if err := callMethod(name, flag.Target, method, binds.clone().add(path)); err != nil {
return next(err)
for _, method := range getMethods(flag.Target, name) {
path := &Path{Flag: flag}
if err := callFunction(method, binds.clone().add(path)); err != nil {
return next(err)
}
}
}
return next(nil)
})
}
func formatMultilineMessage(w io.Writer, leaders []string, format string, args ...interface{}) {
lines := strings.Split(fmt.Sprintf(format, args...), "\n")
func formatMultilineMessage(w io.Writer, leaders []string, format string, args ...any) {
lines := strings.Split(strings.TrimRight(fmt.Sprintf(format, args...), "\n"), "\n")
leader := ""
for _, l := range leaders {
if l == "" {
@@ -303,40 +446,48 @@ func formatMultilineMessage(w io.Writer, leaders []string, format string, args .
}
// Printf writes a message to Kong.Stdout with the application name prefixed.
func (k *Kong) Printf(format string, args ...interface{}) *Kong {
func (k *Kong) Printf(format string, args ...any) *Kong {
formatMultilineMessage(k.Stdout, []string{k.Model.Name}, format, args...)
return k
}
// Errorf writes a message to Kong.Stderr with the application name prefixed.
func (k *Kong) Errorf(format string, args ...interface{}) *Kong {
func (k *Kong) Errorf(format string, args ...any) *Kong {
formatMultilineMessage(k.Stderr, []string{k.Model.Name, "error"}, format, args...)
return k
}
// Fatalf writes a message to Kong.Stderr with the application name prefixed then exits with a non-zero status.
func (k *Kong) Fatalf(format string, args ...interface{}) {
// Fatalf writes a message to Kong.Stderr with the application name prefixed then exits with status 1.
func (k *Kong) Fatalf(format string, args ...any) {
k.Errorf(format, args...)
k.Exit(1)
}
// FatalIfErrorf terminates with an error message if err != nil.
func (k *Kong) FatalIfErrorf(err error, args ...interface{}) {
// If the error implements the ExitCoder interface, the ExitCode() method is called and
// the application exits with that status. Otherwise, the application exits with status 1.
func (k *Kong) FatalIfErrorf(err error, args ...any) {
if err == nil {
return
}
msg := err.Error()
if len(args) > 0 {
msg = fmt.Sprintf(args[0].(string), args[1:]...) + ": " + err.Error()
msg = fmt.Sprintf(args[0].(string), args[1:]...) + ": " + err.Error() //nolint
}
// Maybe display usage information.
if err, ok := err.(*ParseError); ok && k.usageOnError {
options := k.helpOptions
_ = k.help(options, err.Context)
fmt.Fprintln(k.Stdout)
var parseErr *ParseError
if errors.As(err, &parseErr) {
switch k.usageOnError {
case fullUsage:
_ = parseErr.Context.printHelp(k.helpOptions)
fmt.Fprintln(k.Stdout)
case shortUsage:
_ = k.shortHelp(k.helpOptions, parseErr.Context)
fmt.Fprintln(k.Stdout)
}
}
k.Errorf("%s", msg)
k.Exit(1)
k.Exit(exitCodeFromError(err))
}
// LoadConfig from path using the loader configured via Configuration(loader).
@@ -349,7 +500,7 @@ func (k *Kong) LoadConfig(path string) (Resolver, error) {
if err != nil {
return nil, err
}
r, err := os.Open(path) // nolint: gas
r, err := os.Open(path) //nolint: gas
if err != nil {
return nil, err
}
@@ -357,12 +508,3 @@ func (k *Kong) LoadConfig(path string) (Resolver, error) {
return k.loader(r)
}
func catch(err *error) {
msg := recover()
if test, ok := msg.(Error); ok {
*err = test
} else if msg != nil {
panic(msg)
}
}
+11
View File
@@ -0,0 +1,11 @@
output:
- success
- failure
pre-push:
parallel: true
jobs:
- name: test
run: go test -v ./...
- name: lint
run: golangci-lint run
+3 -3
View File
@@ -2,8 +2,8 @@ package kong
import "unicode/utf8"
// https://en.wikibooks.org/wiki/Algorithm_Implementation/Strings/Levenshtein_distance#Go
// License: https://creativecommons.org/licenses/by-sa/3.0/
// Copied from https://github.com/daviddengcn/go-algs/blob/fe23fabd9d0670e4675326040ba7c285c7117b4c/ed/ed.go#L31
// License: https://github.com/daviddengcn/go-algs/blob/fe23fabd9d0670e4675326040ba7c285c7117b4c/LICENSE
func levenshtein(a, b string) int {
f := make([]int, utf8.RuneCountInString(b)+1)
@@ -31,7 +31,7 @@ func levenshtein(a, b string) int {
return f[len(f)-1]
}
func min(a, b int) int {
func min(a, b int) int { //nolint:predeclared
if a <= b {
return a
}
+328 -90
View File
@@ -3,8 +3,9 @@ package kong
import (
"encoding"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"io"
"math/bits"
"net/url"
"os"
@@ -12,13 +13,12 @@ import (
"strconv"
"strings"
"time"
"github.com/pkg/errors"
)
var (
mapperValueType = reflect.TypeOf((*MapperValue)(nil)).Elem()
boolMapperType = reflect.TypeOf((*BoolMapper)(nil)).Elem()
boolMapperValueType = reflect.TypeOf((*BoolMapperValue)(nil)).Elem()
jsonUnmarshalerType = reflect.TypeOf((*json.Unmarshaler)(nil)).Elem()
textUnmarshalerType = reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem()
binaryUnmarshalerType = reflect.TypeOf((*encoding.BinaryUnmarshaler)(nil)).Elem()
)
@@ -42,19 +42,26 @@ func (r *DecodeContext) WithScanner(scan *Scanner) *DecodeContext {
}
// MapperValue may be implemented by fields in order to provide custom mapping.
// Mappers may additionally implement PlaceHolderProvider to provide custom placeholder text.
type MapperValue interface {
Decode(ctx *DecodeContext) error
}
// BoolMapperValue may be implemented by fields in order to provide custom mappings for boolean values.
type BoolMapperValue interface {
MapperValue
IsBool() bool
}
type mapperValueAdapter struct {
isBool bool
}
func (m *mapperValueAdapter) Decode(ctx *DecodeContext, target reflect.Value) error {
if target.Type().Implements(mapperValueType) {
return target.Interface().(MapperValue).Decode(ctx)
return target.Interface().(MapperValue).Decode(ctx) //nolint
}
return target.Addr().Interface().(MapperValue).Decode(ctx)
return target.Addr().Interface().(MapperValue).Decode(ctx) //nolint
}
func (m *mapperValueAdapter) IsBool() bool {
@@ -70,9 +77,9 @@ func (m *textUnmarshalerAdapter) Decode(ctx *DecodeContext, target reflect.Value
return err
}
if target.Type().Implements(textUnmarshalerType) {
return target.Interface().(encoding.TextUnmarshaler).UnmarshalText([]byte(value))
return target.Interface().(encoding.TextUnmarshaler).UnmarshalText([]byte(value)) //nolint
}
return target.Addr().Interface().(encoding.TextUnmarshaler).UnmarshalText([]byte(value))
return target.Addr().Interface().(encoding.TextUnmarshaler).UnmarshalText([]byte(value)) //nolint
}
type binaryUnmarshalerAdapter struct{}
@@ -83,33 +90,59 @@ func (m *binaryUnmarshalerAdapter) Decode(ctx *DecodeContext, target reflect.Val
if err != nil {
return err
}
if target.Type().Implements(textUnmarshalerType) {
return target.Interface().(encoding.BinaryUnmarshaler).UnmarshalBinary([]byte(value))
if target.Type().Implements(binaryUnmarshalerType) {
return target.Interface().(encoding.BinaryUnmarshaler).UnmarshalBinary([]byte(value)) //nolint
}
return target.Addr().Interface().(encoding.BinaryUnmarshaler).UnmarshalBinary([]byte(value))
return target.Addr().Interface().(encoding.BinaryUnmarshaler).UnmarshalBinary([]byte(value)) //nolint
}
type jsonUnmarshalerAdapter struct{}
func (j *jsonUnmarshalerAdapter) Decode(ctx *DecodeContext, target reflect.Value) error {
var value string
err := ctx.Scan.PopValueInto("value", &value)
if err != nil {
return err
}
if target.Type().Implements(jsonUnmarshalerType) {
return target.Interface().(json.Unmarshaler).UnmarshalJSON([]byte(value)) //nolint
}
return target.Addr().Interface().(json.Unmarshaler).UnmarshalJSON([]byte(value)) //nolint
}
// A Mapper represents how a field is mapped from command-line values to Go.
//
// Mappers can be associated with concrete fields via pointer, reflect.Type, reflect.Kind, or via a "type" tag.
//
// Additionally, if a type implements the MappverValue interface, it will be used.
// Additionally, if a type implements the MapperValue interface, it will be used.
type Mapper interface {
// Decode ctx.Value with ctx.Scanner into target.
Decode(ctx *DecodeContext, target reflect.Value) error
}
// VarsContributor can be implemented by a Mapper to contribute Vars during interpolation.
type VarsContributor interface {
Vars(ctx *Value) Vars
}
// A BoolMapper is a Mapper to a value that is a boolean.
//
// This is used solely for formatting help.
type BoolMapper interface {
Mapper
IsBool() bool
}
// BoolMapperExt allows a Mapper to dynamically determine if a value is a boolean.
type BoolMapperExt interface {
Mapper
IsBoolFromValue(v reflect.Value) bool
}
// A MapperFunc is a single function that complies with the Mapper interface.
type MapperFunc func(ctx *DecodeContext, target reflect.Value) error
func (m MapperFunc) Decode(ctx *DecodeContext, target reflect.Value) error { // nolint: golint
func (m MapperFunc) Decode(ctx *DecodeContext, target reflect.Value) error { //nolint: revive
return m(ctx, target)
}
@@ -166,7 +199,8 @@ func (r *Registry) ForType(typ reflect.Type) Mapper {
// Check if the type implements MapperValue.
for _, impl := range []reflect.Type{typ, reflect.PtrTo(typ)} {
if impl.Implements(mapperValueType) {
return &mapperValueAdapter{impl.Implements(boolMapperType)}
// FIXME: This should pass in the bool mapper.
return &mapperValueAdapter{impl.Implements(boolMapperValueType)}
}
}
// Next, try explicitly registered types.
@@ -177,10 +211,13 @@ func (r *Registry) ForType(typ reflect.Type) Mapper {
}
// Next try stdlib unmarshaler interfaces.
for _, impl := range []reflect.Type{typ, reflect.PtrTo(typ)} {
if impl.Implements(textUnmarshalerType) {
switch {
case impl.Implements(textUnmarshalerType):
return &textUnmarshalerAdapter{}
} else if impl.Implements(binaryUnmarshalerType) {
case impl.Implements(binaryUnmarshalerType):
return &binaryUnmarshalerAdapter{}
case impl.Implements(jsonUnmarshalerType):
return &jsonUnmarshalerAdapter{}
}
}
// Finally try registered kinds.
@@ -200,8 +237,8 @@ func (r *Registry) RegisterKind(kind reflect.Kind, mapper Mapper) *Registry {
//
// eg.
//
// Mapper string `kong:"type='colour'`
// registry.RegisterName("colour", ...)
// Mapper string `kong:"type='colour'`
// registry.RegisterName("colour", ...)
func (r *Registry) RegisterName(name string, mapper Mapper) *Registry {
r.names[name] = mapper
return r
@@ -214,7 +251,7 @@ func (r *Registry) RegisterType(typ reflect.Type, mapper Mapper) *Registry {
}
// RegisterValue registers a Mapper by pointer to the field value.
func (r *Registry) RegisterValue(ptr interface{}, mapper Mapper) *Registry {
func (r *Registry) RegisterValue(ptr any, mapper Mapper) *Registry {
key := reflect.ValueOf(ptr)
if key.Kind() != reflect.Ptr {
panic("expected a pointer")
@@ -231,16 +268,15 @@ func (r *Registry) RegisterDefaults() *Registry {
RegisterKind(reflect.Int16, intDecoder(16)).
RegisterKind(reflect.Int32, intDecoder(32)).
RegisterKind(reflect.Int64, intDecoder(64)).
RegisterKind(reflect.Uint, uintDecoder(64)).
RegisterKind(reflect.Uint8, uintDecoder(bits.UintSize)).
RegisterKind(reflect.Uint, uintDecoder(bits.UintSize)).
RegisterKind(reflect.Uint8, uintDecoder(8)).
RegisterKind(reflect.Uint16, uintDecoder(16)).
RegisterKind(reflect.Uint32, uintDecoder(32)).
RegisterKind(reflect.Uint64, uintDecoder(64)).
RegisterKind(reflect.Float32, floatDecoder(32)).
RegisterKind(reflect.Float64, floatDecoder(64)).
RegisterKind(reflect.String, MapperFunc(func(ctx *DecodeContext, target reflect.Value) error {
err := ctx.Scan.PopValueInto("string", target.Addr().Interface())
return err
return ctx.Scan.PopValueInto("string", target.Addr().Interface())
})).
RegisterKind(reflect.Bool, boolMapper{}).
RegisterKind(reflect.Slice, sliceDecoder(r)).
@@ -248,10 +284,13 @@ func (r *Registry) RegisterDefaults() *Registry {
RegisterType(reflect.TypeOf(time.Time{}), timeDecoder()).
RegisterType(reflect.TypeOf(time.Duration(0)), durationDecoder()).
RegisterType(reflect.TypeOf(&url.URL{}), urlMapper()).
RegisterType(reflect.TypeOf(&os.File{}), fileMapper(r)).
RegisterName("path", pathMapper(r)).
RegisterName("existingfile", existingFileMapper(r)).
RegisterName("existingdir", existingDirMapper(r)).
RegisterName("counter", counterMapper())
RegisterName("counter", counterMapper()).
RegisterName("filecontent", fileContentMapper(r)).
RegisterKind(reflect.Ptr, ptrMapper{r})
}
type boolMapper struct{}
@@ -270,14 +309,14 @@ func (boolMapper) Decode(ctx *DecodeContext, target reflect.Value) error {
target.SetBool(false)
default:
return errors.Errorf("bool value must be true, 1, yes, false, 0 or no but got %q", v)
return fmt.Errorf("bool value must be true, 1, yes, false, 0 or no but got %q", v)
}
case bool:
target.SetBool(v)
default:
return errors.Errorf("expected bool but got %q (%T)", token.Value, token.Value)
return fmt.Errorf("expected bool but got %q (%T)", token.Value, token.Value)
}
} else {
target.SetBool(true)
@@ -288,15 +327,23 @@ func (boolMapper) IsBool() bool { return true }
func durationDecoder() MapperFunc {
return func(ctx *DecodeContext, target reflect.Value) error {
var value string
if err := ctx.Scan.PopValueInto("duration", &value); err != nil {
t, err := ctx.Scan.PopValue("duration")
if err != nil {
return err
}
r, err := time.ParseDuration(value)
if err != nil {
return errors.Errorf("expected duration but got %q: %s", value, err)
var d time.Duration
switch v := t.Value.(type) {
case string:
d, err = time.ParseDuration(v)
if err != nil {
return fmt.Errorf("expected duration but got %q: %v", v, err)
}
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64:
d = reflect.ValueOf(v).Convert(reflect.TypeOf(time.Duration(0))).Interface().(time.Duration) //nolint: forcetypeassert
default:
return fmt.Errorf("expected duration but got %q", v)
}
target.Set(reflect.ValueOf(r))
target.Set(reflect.ValueOf(d))
return nil
}
}
@@ -320,7 +367,7 @@ func timeDecoder() MapperFunc {
}
}
func intDecoder(bits int) MapperFunc { // nolint: dupl
func intDecoder(bits int) MapperFunc { //nolint: dupl
return func(ctx *DecodeContext, target reflect.Value) error {
t, err := ctx.Scan.PopValue("int")
if err != nil {
@@ -331,22 +378,25 @@ func intDecoder(bits int) MapperFunc { // nolint: dupl
case string:
sv = v
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64:
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
sv = fmt.Sprintf("%v", v)
case float32, float64:
sv = fmt.Sprintf("%0.f", v)
default:
return errors.Errorf("expected an int but got %q (%T)", t, t.Value)
return fmt.Errorf("expected an int but got %q (%T)", t, t.Value)
}
n, err := strconv.ParseInt(sv, 10, bits)
n, err := strconv.ParseInt(sv, 0, bits)
if err != nil {
return errors.Errorf("expected a valid %d bit int but got %q", bits, sv)
return fmt.Errorf("expected a valid %d bit int but got %q", bits, sv)
}
target.SetInt(n)
return nil
}
}
func uintDecoder(bits int) MapperFunc { // nolint: dupl
func uintDecoder(bits int) MapperFunc { //nolint: dupl
return func(ctx *DecodeContext, target reflect.Value) error {
t, err := ctx.Scan.PopValue("uint")
if err != nil {
@@ -357,15 +407,18 @@ func uintDecoder(bits int) MapperFunc { // nolint: dupl
case string:
sv = v
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64:
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
sv = fmt.Sprintf("%v", v)
case float32, float64:
sv = fmt.Sprintf("%0.f", v)
default:
return errors.Errorf("expected an int but got %q (%T)", t, t.Value)
return fmt.Errorf("expected an int but got %q (%T)", t, t.Value)
}
n, err := strconv.ParseUint(sv, 10, bits)
n, err := strconv.ParseUint(sv, 0, bits)
if err != nil {
return errors.Errorf("expected a valid %d bit uint but got %q", bits, sv)
return fmt.Errorf("expected a valid %d bit uint but got %q", bits, sv)
}
target.SetUint(n)
return nil
@@ -382,7 +435,7 @@ func floatDecoder(bits int) MapperFunc {
case string:
n, err := strconv.ParseFloat(v, bits)
if err != nil {
return errors.Errorf("expected a float but got %q (%T)", t, t.Value)
return fmt.Errorf("expected a float but got %q (%T)", t, t.Value)
}
target.SetFloat(n)
@@ -393,10 +446,10 @@ func floatDecoder(bits int) MapperFunc {
target.SetFloat(v)
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
target.Set(reflect.ValueOf(v))
target.SetFloat(reflect.ValueOf(v).Convert(reflect.TypeOf(float64(0))).Float())
default:
return errors.Errorf("expected an int but got %q (%T)", t, t.Value)
return fmt.Errorf("expected a float but got %q (%T)", t, t.Value)
}
return nil
}
@@ -408,32 +461,32 @@ func mapDecoder(r *Registry) MapperFunc {
target.Set(reflect.MakeMap(target.Type()))
}
el := target.Type()
sep := ctx.Value.Tag.MapSep
mapsep := ctx.Value.Tag.MapSep
var childScanner *Scanner
if ctx.Value.Flag != nil {
t := ctx.Scan.Pop()
// If decoding a flag, we need an argument.
// If decoding a flag, we need an value.
if t.IsEOL() {
return errors.Errorf("unexpected EOL")
return fmt.Errorf("missing value, expecting \"<key>=<value>%c...\"", mapsep)
}
switch v := t.Value.(type) {
case string:
childScanner = Scan(SplitEscaped(v, sep)...)
childScanner = ScanAsType(t.Type, SplitEscaped(v, mapsep)...)
case []map[string]interface{}:
case []map[string]any:
for _, m := range v {
err := jsonTranscode(m, target.Addr().Interface())
if err != nil {
return errors.WithStack(err)
return err
}
}
return nil
case map[string]interface{}:
case map[string]any:
return jsonTranscode(v, target.Addr().Interface())
default:
return errors.Errorf("invalid map value %q (of type %T)", t, t.Value)
return fmt.Errorf("invalid map value %q (of type %T)", t, t.Value)
}
} else {
tokens := ctx.Scan.PopWhile(func(t Token) bool { return t.IsValue() })
@@ -447,7 +500,7 @@ func mapDecoder(r *Registry) MapperFunc {
}
parts := strings.SplitN(token, "=", 2)
if len(parts) != 2 {
return errors.Errorf("expected \"<key>=<value>\" but got %q", token)
return fmt.Errorf("expected \"<key>=<value>\" but got %q", token)
}
key, value := parts[0], parts[1]
@@ -455,23 +508,23 @@ func mapDecoder(r *Registry) MapperFunc {
if typ := ctx.Value.Tag.Type; typ != "" {
parts := strings.Split(typ, ":")
if len(parts) != 2 {
return errors.Errorf("type:\"\" on map field must be in the form \"[<keytype>]:[<valuetype>]\"")
return errors.New("type:\"\" on map field must be in the form \"[<keytype>]:[<valuetype>]\"")
}
keyTypeName, valueTypeName = parts[0], parts[1]
}
keyScanner := Scan(key)
keyScanner := ScanAsType(FlagValueToken, key)
keyDecoder := r.ForNamedType(keyTypeName, el.Key())
keyValue := reflect.New(el.Key()).Elem()
if err := keyDecoder.Decode(ctx.WithScanner(keyScanner), keyValue); err != nil {
return errors.Errorf("invalid map key %q", key)
return fmt.Errorf("invalid map key %q", key)
}
valueScanner := Scan(value)
valueScanner := ScanAsType(FlagValueToken, value)
valueDecoder := r.ForNamedType(valueTypeName, el.Elem())
valueValue := reflect.New(el.Elem()).Elem()
if err := valueDecoder.Decode(ctx.WithScanner(valueScanner), valueValue); err != nil {
return errors.Errorf("invalid map value %q", value)
return fmt.Errorf("invalid map value %q", value)
}
target.SetMapIndex(keyValue, valueValue)
@@ -487,19 +540,23 @@ func sliceDecoder(r *Registry) MapperFunc {
var childScanner *Scanner
if ctx.Value.Flag != nil {
t := ctx.Scan.Pop()
// If decoding a flag, we need an argument.
// If decoding a flag, we need a value.
tail := ""
if sep != -1 {
tail += string(sep) + "..."
}
if t.IsEOL() {
return errors.Errorf("unexpected EOL")
return fmt.Errorf("missing value, expecting \"<arg>%s\"", tail)
}
switch v := t.Value.(type) {
case string:
childScanner = Scan(SplitEscaped(v, sep)...)
childScanner = ScanAsType(t.Type, SplitEscaped(v, sep)...)
case []interface{}:
case []any:
return jsonTranscode(v, target.Addr().Interface())
default:
v = []interface{}{v}
v = []any{v}
return jsonTranscode(v, target.Addr().Interface())
}
} else {
@@ -508,13 +565,13 @@ func sliceDecoder(r *Registry) MapperFunc {
}
childDecoder := r.ForNamedType(ctx.Value.Tag.Type, el)
if childDecoder == nil {
return errors.Errorf("no mapper for element type of %s", target.Type())
return fmt.Errorf("no mapper for element type of %s", target.Type())
}
for !childScanner.Peek().IsEOL() {
childValue := reflect.New(el).Elem()
err := childDecoder.Decode(ctx.WithScanner(childScanner), childValue)
if err != nil {
return errors.WithStack(err)
return err
}
target.Set(reflect.Append(target, childValue))
}
@@ -527,33 +584,88 @@ func pathMapper(r *Registry) MapperFunc {
if target.Kind() == reflect.Slice {
return sliceDecoder(r)(ctx, target)
}
originalTarget := target
if target.Kind() == reflect.Ptr && target.Elem().Kind() == reflect.String {
if target.IsNil() {
return nil
}
target = target.Elem()
}
if target.Kind() != reflect.String {
return errors.Errorf("\"path\" type must be applied to a string not %s", target.Type())
return fmt.Errorf("\"path\" type must be applied to a string not %s", target.Type())
}
var path string
err := ctx.Scan.PopValueInto("file", &path)
if err != nil {
return err
}
path = ExpandPath(path)
// Skip if path with default is explicitly set to "". For the current directory use ".".
if ctx.Value.HasDefault && path == "" {
if originalTarget.Kind() == reflect.Ptr {
originalTarget.Set(reflect.Zero(originalTarget.Type()))
}
return nil
}
if path != "-" {
path = ExpandPath(path)
}
target.SetString(path)
return nil
}
}
func fileMapper(r *Registry) MapperFunc {
return func(ctx *DecodeContext, target reflect.Value) error {
if target.Kind() == reflect.Slice {
return sliceDecoder(r)(ctx, target)
}
var path string
err := ctx.Scan.PopValueInto("file", &path)
if err != nil {
return err
}
// Skip if value was already set to avoid opening the default file when an explicit value was provided
if ctx.Value.HasDefault && ctx.Value.Set {
return nil
}
var file *os.File
if path == "-" {
file = os.Stdin
} else {
path = ExpandPath(path)
file, err = os.Open(path) //nolint: gosec
if err != nil {
return err
}
}
target.Set(reflect.ValueOf(file))
return nil
}
}
func existingFileMapper(r *Registry) MapperFunc {
return func(ctx *DecodeContext, target reflect.Value) error {
if target.Kind() == reflect.Slice {
return sliceDecoder(r)(ctx, target)
}
if target.Kind() != reflect.String {
return errors.Errorf("\"existingfile\" type must be applied to a string not %s", target.Type())
return fmt.Errorf("\"existingfile\" type must be applied to a string not %s", target.Type())
}
var path string
err := ctx.Scan.PopValueInto("file", &path)
if err != nil {
return err
}
if !ctx.Value.Active || (ctx.Value.Set && ctx.Value.Target.Type() == target.Type()) {
// early return to avoid checking extra files that may not exist;
// this hack only works because the value provided on the cli is
// checked before the default value is checked (if default is set).
return nil
}
if path != "-" {
path = ExpandPath(path)
stat, err := os.Stat(path)
@@ -561,7 +673,7 @@ func existingFileMapper(r *Registry) MapperFunc {
return err
}
if stat.IsDir() {
return errors.Errorf("%q exists but is a directory", path)
return fmt.Errorf("%q exists but is a directory", path)
}
}
target.SetString(path)
@@ -575,26 +687,106 @@ func existingDirMapper(r *Registry) MapperFunc {
return sliceDecoder(r)(ctx, target)
}
if target.Kind() != reflect.String {
return errors.Errorf("\"existingdir\" must be applied to a string not %s", target.Type())
return fmt.Errorf("\"existingdir\" must be applied to a string not %s", target.Type())
}
var path string
err := ctx.Scan.PopValueInto("file", &path)
if err != nil {
return err
}
if !ctx.Value.Active || (ctx.Value.Set && ctx.Value.Target.Type() == target.Type()) {
// early return to avoid checking extra dirs that may not exist;
// this hack only works because the value provided on the cli is
// checked before the default value is checked (if default is set).
return nil
}
path = ExpandPath(path)
stat, err := os.Stat(path)
if err != nil {
return err
}
if !stat.IsDir() {
return errors.Errorf("%q exists but is not a directory", path)
return fmt.Errorf("%q exists but is not a directory", path)
}
target.SetString(path)
return nil
}
}
func fileContentMapper(r *Registry) MapperFunc {
return func(ctx *DecodeContext, target reflect.Value) error {
if target.Kind() != reflect.Slice || target.Type().Elem().Kind() != reflect.Uint8 {
return fmt.Errorf("\"filecontent\" must be applied to []byte not %s", target.Type())
}
var path string
err := ctx.Scan.PopValueInto("file", &path)
if err != nil {
return err
}
if !ctx.Value.Active || ctx.Value.Set {
// early return to avoid checking extra dirs that may not exist;
// this hack only works because the value provided on the cli is
// checked before the default value is checked (if default is set).
return nil
}
var data []byte
if path != "-" {
path = ExpandPath(path)
data, err = os.ReadFile(path) //nolint:gosec
} else {
data, err = io.ReadAll(os.Stdin)
}
if err != nil {
if info, statErr := os.Stat(path); statErr == nil && info.IsDir() {
return fmt.Errorf("%q exists but is a directory: %w", path, err)
}
return err
}
target.SetBytes(data)
return nil
}
}
type ptrMapper struct {
r *Registry
}
var _ BoolMapperExt = (*ptrMapper)(nil)
// IsBoolFromValue implements BoolMapperExt
func (p ptrMapper) IsBoolFromValue(target reflect.Value) bool {
elem := reflect.New(target.Type().Elem()).Elem()
nestedMapper := p.r.ForValue(elem)
if nestedMapper == nil {
return false
}
if bm, ok := nestedMapper.(BoolMapper); ok && bm.IsBool() {
return true
}
if bm, ok := nestedMapper.(BoolMapperExt); ok && bm.IsBoolFromValue(target) {
return true
}
return target.Kind() == reflect.Ptr && target.Type().Elem().Kind() == reflect.Bool
}
func (p ptrMapper) Decode(ctx *DecodeContext, target reflect.Value) error {
elem := reflect.New(target.Type().Elem()).Elem()
nestedMapper := p.r.ForValue(elem)
if nestedMapper == nil {
return fmt.Errorf("cannot find mapper for %v", target.Type().Elem().String())
}
err := nestedMapper.Decode(ctx, elem)
if err != nil {
return err
}
target.Set(elem.Addr())
return nil
}
func counterMapper() MapperFunc {
return func(ctx *DecodeContext, target reflect.Value) error {
if ctx.Scan.Peek().Type == FlagValueToken {
@@ -602,19 +794,32 @@ func counterMapper() MapperFunc {
if err != nil {
return err
}
var n int64
switch v := t.Value.(type) {
case string:
n, err := strconv.ParseInt(v, 10, 64)
n, err = strconv.ParseInt(v, 10, 64)
if err != nil {
return errors.Errorf("expected a counter but got %q (%T)", t, t.Value)
return fmt.Errorf("expected a counter but got %q (%T)", t, t.Value)
}
target.SetInt(n)
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
target.Set(reflect.ValueOf(v))
n = reflect.ValueOf(v).Convert(reflect.TypeOf(int64(0))).Int()
default:
return errors.Errorf("expected a counter but got %q (%T)", t, t.Value)
return fmt.Errorf("expected a counter but got %q (%T)", t, t.Value)
}
// Assign by the target's kind, like the increment path below, so a
// counter declared as a uint or float field doesn't panic.
switch target.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
target.SetInt(n)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
target.SetUint(uint64(n)) //nolint:gosec // a counter value is small and non-negative
case reflect.Float32, reflect.Float64:
target.SetFloat(float64(n))
default:
return fmt.Errorf("type:\"counter\" must be used with a numeric field")
}
return nil
}
@@ -630,7 +835,7 @@ func counterMapper() MapperFunc {
target.SetFloat(target.Float() + 1)
default:
return errors.Errorf("type:\"counter\" must be used with a numeric field")
return fmt.Errorf("type:\"counter\" must be used with a numeric field")
}
return nil
}
@@ -645,7 +850,7 @@ func urlMapper() MapperFunc {
}
url, err := url.Parse(urlStr)
if err != nil {
return errors.WithStack(err)
return err
}
target.Set(reflect.ValueOf(url))
return nil
@@ -656,19 +861,22 @@ func urlMapper() MapperFunc {
//
// It differs from strings.Split() in that the separator can exist in a field by escaping it with a \. eg.
//
// SplitEscaped(`hello\,there,bob`, ',') == []string{"hello,there", "bob"}
// SplitEscaped(`hello\,there,bob`, ',') == []string{"hello,there", "bob"}
func SplitEscaped(s string, sep rune) (out []string) {
if sep == -1 {
return []string{s}
}
escaped := false
token := ""
for _, ch := range s {
for i, ch := range s {
switch {
case escaped:
if ch != sep {
token += `\`
}
token += string(ch)
escaped = false
case ch == '\\':
case ch == '\\' && i < len(s)-1:
escaped = true
case ch == sep && !escaped:
out = append(out, token)
@@ -686,19 +894,46 @@ func SplitEscaped(s string, sep rune) (out []string) {
// JoinEscaped joins a slice of strings on sep, but also escapes any instances of sep in the fields with \. eg.
//
// JoinEscaped([]string{"hello,there", "bob"}, ',') == `hello\,there,bob`
// JoinEscaped([]string{"hello,there", "bob"}, ',') == `hello\,there,bob`
func JoinEscaped(s []string, sep rune) string {
escaped := []string{}
for _, e := range s {
escaped = append(escaped, strings.Replace(e, string(sep), `\`+string(sep), -1))
escaped = append(escaped, strings.ReplaceAll(e, string(sep), `\`+string(sep)))
}
return strings.Join(escaped, string(sep))
}
// NamedFileContentFlag is a flag value that loads a file's contents and filename into its value.
type NamedFileContentFlag struct {
Filename string
Contents []byte
}
func (f *NamedFileContentFlag) Decode(ctx *DecodeContext) error { //nolint: revive
var filename string
err := ctx.Scan.PopValueInto("filename", &filename)
if err != nil {
return err
}
// This allows unsetting of file content flags.
if filename == "" {
*f = NamedFileContentFlag{}
return nil
}
filename = ExpandPath(filename)
data, err := os.ReadFile(filename) //nolint: gosec
if err != nil {
return fmt.Errorf("failed to open %q: %v", filename, err)
}
f.Contents = data
f.Filename = filename
return nil
}
// FileContentFlag is a flag value that loads a file's contents into its value.
type FileContentFlag []byte
func (f *FileContentFlag) Decode(ctx *DecodeContext) error { // nolint: golint
func (f *FileContentFlag) Decode(ctx *DecodeContext) error { //nolint: revive
var filename string
err := ctx.Scan.PopValueInto("filename", &filename)
if err != nil {
@@ -710,18 +945,21 @@ func (f *FileContentFlag) Decode(ctx *DecodeContext) error { // nolint: golint
return nil
}
filename = ExpandPath(filename)
data, err := ioutil.ReadFile(filename) // nolint: gosec
data, err := os.ReadFile(filename) //nolint: gosec
if err != nil {
return errors.Errorf("failed to open %q: %s", filename, err)
return fmt.Errorf("failed to open %q: %v", filename, err)
}
*f = data
return nil
}
func jsonTranscode(in, out interface{}) error {
func jsonTranscode(in, out any) error {
data, err := json.Marshal(in)
if err != nil {
return errors.WithStack(err)
return err
}
return errors.Wrapf(json.Unmarshal(data, out), "%#v -> %T", in, out)
if err = json.Unmarshal(data, out); err != nil {
return fmt.Errorf("%#v -> %T: %v", in, out, err)
}
return nil
}
+155 -60
View File
@@ -7,8 +7,6 @@ import (
"reflect"
"strconv"
"strings"
"github.com/pkg/errors"
)
// A Visitable component in the model.
@@ -41,18 +39,22 @@ const (
// Node is a branch in the CLI. ie. a command or positional argument.
type Node struct {
Type NodeType
Parent *Node
Name string
Help string // Short help displayed in summaries.
Detail string // Detailed help displayed when describing command/arg alone.
Group string
Hidden bool
Flags []*Flag
Positional []*Positional
Children []*Node
Target reflect.Value // Pointer to the value in the grammar that this Node is associated with.
Tag *Tag
Type NodeType
Parent *Node
Name string
Help string // Short help displayed in summaries.
Detail string // Detailed help displayed when describing command/arg alone.
Group *Group
Hidden bool
Flags []*Flag
Positional []*Positional
Children []*Node
DefaultCmd *Node
Target reflect.Value // Pointer to the value in the grammar that this Node is associated with.
Tag *Tag
Aliases []string
Passthrough bool // Set to true to stop flag parsing when encountered.
Active bool // Denotes the node is part of an active branch in the CLI.
Argument *Value // Populated when Type is ArgumentNode.
}
@@ -67,7 +69,7 @@ func (n *Node) Leaf() bool {
// Find a command/argument/flag by pointer to its field.
//
// Returns nil if not found. Panics if ptr is not a pointer.
func (n *Node) Find(ptr interface{}) *Node {
func (n *Node) Find(ptr any) *Node {
key := reflect.ValueOf(ptr)
if key.Kind() != reflect.Ptr {
panic("expected a pointer")
@@ -97,6 +99,7 @@ func (n *Node) AllFlags(hide bool) (out [][]*Flag) {
group := []*Flag{}
for _, flag := range n.Flags {
if !hide || !flag.Hidden {
flag.Active = true
group = append(group, flag)
}
}
@@ -127,6 +130,20 @@ func (n *Node) Leaves(hide bool) (out []*Node) {
return
}
// Values returns the values directly attached to this node: its argument
// value, flag values and positionals.
func (n *Node) Values() []*Value {
values := []*Value{}
if n.Argument != nil {
values = append(values, n.Argument)
}
for _, flag := range n.Flags {
values = append(values, flag.Value)
}
values = append(values, n.Positional...)
return values
}
// Depth of the command from the application root.
func (n *Node) Depth() int {
depth := 0
@@ -145,14 +162,33 @@ func (n *Node) Summary() string {
summary += " " + flags
}
args := []string{}
optional := 0
for _, arg := range n.Positional {
args = append(args, arg.Summary())
argSummary := arg.Summary()
if arg.Tag.Optional {
optional++
argSummary = strings.TrimRight(argSummary, "]")
}
args = append(args, argSummary)
}
if len(args) != 0 {
summary += " " + strings.Join(args, " ")
summary += " " + strings.Join(args, " ") + strings.Repeat("]", optional)
} else if len(n.Children) > 0 {
summary += " <command>"
}
allFlags := n.Flags
if n.Parent != nil {
allFlags = append(allFlags, n.Parent.Flags...)
}
for _, flag := range allFlags {
if _, ok := flag.Target.Interface().(helpFlag); ok {
continue
}
if !flag.Required {
summary += " [flags]"
break
}
}
return summary
}
@@ -196,27 +232,48 @@ func (n *Node) Path() (out string) {
switch n.Type {
case CommandNode:
out += " " + n.Name
if len(n.Aliases) > 0 {
out += fmt.Sprintf(" (%s)", strings.Join(n.Aliases, ","))
}
case ArgumentNode:
out += " " + "<" + n.Name + ">"
default:
}
return strings.TrimSpace(out)
}
// ClosestGroup finds the first non-nil group in this node and its ancestors.
func (n *Node) ClosestGroup() *Group {
switch {
case n.Group != nil:
return n.Group
case n.Parent != nil:
return n.Parent.ClosestGroup()
default:
return nil
}
}
// A Value is either a flag or a variable positional argument.
type Value struct {
Flag *Flag // Nil if positional argument.
Name string
Help string
Default string
DefaultValue reflect.Value
Enum string
Mapper Mapper
Tag *Tag
Target reflect.Value
Required bool
Set bool // Set to true when this value is set through some mechanism.
Format string // Formatting directive, if applicable.
Position int // Position (for positional arguments).
Flag *Flag // Nil if positional argument.
Name string
Help string
OrigHelp string // Original help string, without interpolated variables.
HasDefault bool
Default string
DefaultValue reflect.Value
Enum string
Mapper Mapper
Tag *Tag
Target reflect.Value
Required bool
Set bool // Set to true when this value is set through some mechanism.
Format string // Formatting directive, if applicable.
Position int // Position (for positional arguments).
Passthrough bool // Deprecated: Use PassthroughMode instead. Set to true to stop flag parsing when encountered.
PassthroughMode PassthroughMode //
Active bool // Denotes the value is part of an active branch in the CLI.
}
// EnumMap returns a map of the enums in this value.
@@ -229,6 +286,16 @@ func (v *Value) EnumMap() map[string]bool {
return out
}
// EnumSlice returns a slice of the enums in this value.
func (v *Value) EnumSlice() []string {
parts := strings.Split(v.Enum, ",")
out := make([]string, len(parts))
for i, part := range parts {
out[i] = strings.TrimSpace(part)
}
return out
}
// ShortSummary returns a human-readable summary of the value, not including any placeholders/defaults.
func (v *Value) ShortSummary() string {
if v.Flag != nil {
@@ -279,27 +346,28 @@ func (v *Value) IsMap() bool {
// IsBool returns true if the underlying value is a boolean.
func (v *Value) IsBool() bool {
if m, ok := v.Mapper.(BoolMapperExt); ok && m.IsBoolFromValue(v.Target) {
return true
}
if m, ok := v.Mapper.(BoolMapper); ok && m.IsBool() {
return true
}
return v.Target.Kind() == reflect.Bool
}
// IsCounter returns true if the value is a counter.
func (v *Value) IsCounter() bool {
return v.Tag.Type == "counter"
}
// Parse tokens into value, parse, and validate, but do not write to the field.
func (v *Value) Parse(scan *Scanner, target reflect.Value) (err error) {
defer func() {
if rerr := recover(); rerr != nil {
switch rerr := rerr.(type) {
case Error:
err = errors.Wrap(rerr, v.ShortSummary())
default:
panic(fmt.Sprintf("mapper %T failed to apply to %s: %s", v.Mapper, v.Summary(), rerr))
}
}
}()
if target.Kind() == reflect.Ptr && target.IsNil() {
target.Set(reflect.New(target.Type().Elem()))
}
err = v.Mapper.Decode(&DecodeContext{Value: v, Scan: scan}, target)
if err != nil {
return errors.Wrap(err, v.ShortSummary())
return fmt.Errorf("%s: %w", v.ShortSummary(), err)
}
v.Set = true
return nil
@@ -326,17 +394,20 @@ func (v *Value) ApplyDefault() error {
// Does not include resolvers.
func (v *Value) Reset() error {
v.Target.Set(reflect.Zero(v.Target.Type()))
if v.Tag.Env != "" {
envar := os.Getenv(v.Tag.Env)
if envar != "" {
err := v.Parse(ScanFromTokens(Token{Type: FlagValueToken, Value: envar}), v.Target)
if err != nil {
return fmt.Errorf("%s (from envar %s=%q)", err, v.Tag.Env, envar)
if len(v.Tag.Envs) != 0 {
for _, env := range v.Tag.Envs {
envar, ok := os.LookupEnv(env)
// Parse the first non-empty ENV in the list
if ok {
err := v.Parse(ScanFromTokens(Token{Type: FlagValueToken, Value: envar}), v.Target)
if err != nil {
return fmt.Errorf("%s (from envar %s=%q)", err, env, envar)
}
return nil
}
return nil
}
}
if v.Default != "" {
if v.HasDefault {
return v.Parse(ScanFromTokens(Token{Type: FlagValueToken, Value: v.Default}), v.Target)
}
return nil
@@ -350,12 +421,15 @@ type Positional = Value
// A Flag represents a command-line flag.
type Flag struct {
*Value
Group string // Logical grouping when displaying. May also be used by configuration loaders to group options logically.
Xor string
Group *Group // Logical grouping when displaying. May also be used by configuration loaders to group options logically.
Xor []string
And []string
PlaceHolder string
Env string
Envs []string
Aliases []string
Short rune
Hidden bool
Negated bool
}
func (f *Flag) String() string {
@@ -363,7 +437,7 @@ func (f *Flag) String() string {
if f.Short != 0 {
out = fmt.Sprintf("-%c, %s", f.Short, out)
}
if !f.IsBool() {
if !f.IsBool() && !f.IsCounter() {
out += "=" + f.FormatPlaceHolder()
}
return out
@@ -371,28 +445,46 @@ func (f *Flag) String() string {
// FormatPlaceHolder formats the placeholder string for a Flag.
func (f *Flag) FormatPlaceHolder() string {
placeholderHelper, ok := f.Value.Mapper.(PlaceHolderProvider)
if ok {
return placeholderHelper.PlaceHolder(f)
}
tail := ""
if f.Value.IsSlice() && f.Value.Tag.Sep != -1 {
if f.Value.IsSlice() && f.Value.Tag.Sep != -1 && f.Tag.Type == "" {
tail += string(f.Value.Tag.Sep) + "..."
}
if f.Default != "" {
if f.PlaceHolder != "" {
return f.PlaceHolder + tail
}
if f.HasDefault {
if f.Value.Target.Kind() == reflect.String {
return strconv.Quote(f.Default) + tail
}
return f.Default + tail
}
if f.PlaceHolder != "" {
return f.PlaceHolder + tail
}
if f.Value.IsMap() {
if f.Value.Tag.MapSep != -1 {
if f.Value.Tag.MapSep != -1 && f.Tag.Type == "" {
tail = string(f.Value.Tag.MapSep) + "..."
}
return "KEY=VALUE" + tail
}
if f.Tag != nil && f.Tag.TypeName != "" {
return strings.ToUpper(dashedString(f.Tag.TypeName)) + tail
}
return strings.ToUpper(f.Name) + tail
}
// Group holds metadata about a command or flag group used when printing help.
type Group struct {
// Key is the `group` field tag value used to identify this group.
Key string
// Title is displayed above the grouped items.
Title string
// Description is optional and displayed under the Title when non empty.
// It can be used to introduce the group's purpose to the user.
Description string
}
// This is directly from the Go 1.13 source code.
func reflectValueIsZero(v reflect.Value) bool {
switch v.Kind() {
@@ -428,6 +520,9 @@ func reflectValueIsZero(v reflect.Value) bool {
default:
// This should never happens, but will act as a safeguard for
// later, as a default value doesn't makes sense here.
panic(&reflect.ValueError{"reflect.Value.IsZero", v.Kind()})
panic(&reflect.ValueError{
Method: "reflect.Value.IsZero",
Kind: v.Kind(),
})
}
}
+19
View File
@@ -0,0 +1,19 @@
package kong
// negatableDefault is a placeholder value for the Negatable tag to indicate
// the negated flag is --no-<flag-name>. This is needed as at the time of
// parsing a tag, the field's flag name is not yet known.
const negatableDefault = "_"
// negatableFlagName returns the name of the flag for a negatable field, or
// an empty string if the field is not negatable.
func negatableFlagName(name, negation string) string {
switch negation {
case "":
return ""
case negatableDefault:
return "--no-" + name
default:
return "--" + negation
}
}
+322 -44
View File
@@ -1,14 +1,15 @@
package kong
import (
"errors"
"fmt"
"io"
"os"
"os/user"
"path/filepath"
"reflect"
"regexp"
"strings"
"github.com/pkg/errors"
)
// An Option applies optional changes to the Kong application.
@@ -19,7 +20,7 @@ type Option interface {
// OptionFunc is function that adheres to the Option interface.
type OptionFunc func(k *Kong) error
func (o OptionFunc) Apply(k *Kong) error { return o(k) } // nolint: golint
func (o OptionFunc) Apply(k *Kong) error { return o(k) } //nolint: revive
// Vars sets the variables to use for interpolation into help strings and default values.
//
@@ -54,6 +55,61 @@ func Exit(exit func(int)) Option {
})
}
// WithHyphenPrefixedParameters enables or disables hyphen-prefixed parameters.
//
// These are disabled by default.
func WithHyphenPrefixedParameters(enable bool) Option {
return OptionFunc(func(k *Kong) error {
k.allowHyphenated = enable
return nil
})
}
type embedded struct {
strct any
tags []string
}
// Embed a struct into the root of the CLI.
//
// "strct" must be a pointer to a structure.
func Embed(strct any, tags ...string) Option {
t := reflect.TypeOf(strct)
if t.Kind() != reflect.Ptr || t.Elem().Kind() != reflect.Struct {
panic("kong: Embed() must be called with a pointer to a struct")
}
return OptionFunc(func(k *Kong) error {
k.embedded = append(k.embedded, embedded{strct, tags})
return nil
})
}
type dynamicCommand struct {
name string
help string
group string
tags []string
cmd any
}
// DynamicCommand registers a dynamically constructed command with the root of the CLI.
//
// This is useful for command-line structures that are extensible via user-provided plugins.
//
// "tags" is a list of extra tag strings to parse, in the form <key>:"<value>".
func DynamicCommand(name, help, group string, cmd any, tags ...string) Option {
return OptionFunc(func(k *Kong) error {
k.dynamicCommands = append(k.dynamicCommands, &dynamicCommand{
name: name,
help: help,
group: group,
cmd: cmd,
tags: tags,
})
return nil
})
}
// NoDefaultHelp disables the default help flags.
func NoDefaultHelp() Option {
return OptionFunc(func(k *Kong) error {
@@ -66,31 +122,59 @@ func NoDefaultHelp() Option {
// parsing occurs.
//
// This is useful for, e.g., adding short options to flags, updating help, etc.
func (k *Kong) PostBuild(fn func(*Kong) error) Option {
func PostBuild(fn func(*Kong) error) Option {
return OptionFunc(func(k *Kong) error {
k.postBuildOptions = append(k.postBuildOptions, OptionFunc(fn))
return nil
})
}
// WithBeforeReset registers a hook to run before fields values are reset to their defaults
// (as specified in the grammar) or to zero values.
func WithBeforeReset(fn any) Option {
return withHook("BeforeReset", fn)
}
// WithBeforeResolve registers a hook to run before resolvers are applied.
func WithBeforeResolve(fn any) Option {
return withHook("BeforeResolve", fn)
}
// WithBeforeApply registers a hook to run before command line arguments are applied to the grammar.
func WithBeforeApply(fn any) Option {
return withHook("BeforeApply", fn)
}
// WithAfterApply registers a hook to run after values are applied to the grammar and validated.
func WithAfterApply(fn any) Option {
return withHook("AfterApply", fn)
}
// withHook registers a named hook.
func withHook(name string, fn any) Option {
value := reflect.ValueOf(fn)
if value.Kind() != reflect.Func {
panic(fmt.Errorf("expected function, got %s", value.Type()))
}
return OptionFunc(func(k *Kong) error {
k.hooks[name] = append(k.hooks[name], value)
return nil
})
}
// Name overrides the application name.
func Name(name string) Option {
return OptionFunc(func(k *Kong) error {
k.postBuildOptions = append(k.postBuildOptions, OptionFunc(func(k *Kong) error {
k.Model.Name = name
return nil
}))
return PostBuild(func(k *Kong) error {
k.Model.Name = name
return nil
})
}
// Description sets the application description.
func Description(description string) Option {
return OptionFunc(func(k *Kong) error {
k.postBuildOptions = append(k.postBuildOptions, OptionFunc(func(k *Kong) error {
k.Model.Help = description
return nil
}))
return PostBuild(func(k *Kong) error {
k.Model.Help = description
return nil
})
}
@@ -112,7 +196,7 @@ func KindMapper(kind reflect.Kind, mapper Mapper) Option {
}
// ValueMapper registers a mapper to a field value.
func ValueMapper(ptr interface{}, mapper Mapper) Option {
func ValueMapper(ptr any, mapper Mapper) Option {
return OptionFunc(func(k *Kong) error {
k.registry.RegisterValue(ptr, mapper)
return nil
@@ -143,11 +227,11 @@ func Writers(stdout, stderr io.Writer) Option {
//
// There are two hook points:
//
// BeforeApply(...) error
// AfterApply(...) error
// BeforeApply(...) error
// AfterApply(...) error
//
// Called before validation/assignment, and immediately after validation/assignment, respectively.
func Bind(args ...interface{}) Option {
func Bind(args ...any) Option {
return OptionFunc(func(k *Kong) error {
k.bindings.add(args...)
return nil
@@ -156,37 +240,50 @@ func Bind(args ...interface{}) Option {
// BindTo allows binding of implementations to interfaces.
//
// BindTo(impl, (*iface)(nil))
func BindTo(impl, iface interface{}) Option {
// BindTo(impl, (*iface)(nil))
func BindTo(impl, iface any) Option {
return OptionFunc(func(k *Kong) error {
valueOf := reflect.ValueOf(impl)
k.bindings[reflect.TypeOf(iface).Elem()] = func() (reflect.Value, error) { return valueOf, nil }
k.bindings.addTo(impl, iface)
return nil
})
}
// BindToProvider allows binding of provider functions.
// BindFor allows binding of implementations to the compile-time type. It
// is equivalent to BindTo(v, (*T)(nil)), but can usually infer the
// correct type T.
func BindFor[T any](v T) Option {
return BindTo(v, (*T)(nil))
}
// BindToProvider binds an injected value to a provider function.
//
// The provider function must have one of the following signatures:
//
// func(...) (T, error)
// func(...) T
//
// Where arguments to the function are injected by Kong.
//
// This is useful when the Run() function of different commands require different values that may
// not all be initialisable from the main() function.
func BindToProvider(provider interface{}) Option {
func BindToProvider(provider any) Option {
return OptionFunc(func(k *Kong) error {
pv := reflect.ValueOf(provider)
t := pv.Type()
if t.Kind() != reflect.Func || t.NumIn() != 0 || t.NumOut() != 2 || t.Out(1) != reflect.TypeOf((*error)(nil)).Elem() {
return errors.Errorf("%T must be a function with the signature func()(T, error)", provider)
}
rt := pv.Type().Out(0)
k.bindings[rt] = func() (reflect.Value, error) {
out := pv.Call(nil)
errv := out[1]
var err error
if !errv.IsNil() {
err = errv.Interface().(error)
}
return out[0], err
}
return nil
return k.bindings.addProvider(provider, false /* singleton */)
})
}
// BindSingletonProvider binds an injected value to a provider function.
// The provider function must have the signature:
//
// func(...) (T, error)
// func(...) T
//
// Unlike [BindToProvider], the provider function will only be called
// at most once, and the result will be cached and reused
// across multiple recipients of the injected value.
func BindSingletonProvider(provider any) Option {
return OptionFunc(func(k *Kong) error {
return k.bindings.addProvider(provider, true /* singleton */)
})
}
@@ -198,7 +295,20 @@ func Help(help HelpPrinter) Option {
})
}
// ShortHelp configures the short usage message.
//
// It should be used together with kong.ShortUsageOnError() to display a
// custom short usage message on errors.
func ShortHelp(shortHelp HelpPrinter) Option {
return OptionFunc(func(k *Kong) error {
k.shortHelp = shortHelp
return nil
})
}
// HelpFormatter configures how the help text is formatted.
//
// Deprecated: Use ValueFormatter() instead.
func HelpFormatter(helpFormatter HelpValueFormatter) Option {
return OptionFunc(func(k *Kong) error {
k.helpFormatter = helpFormatter
@@ -206,6 +316,14 @@ func HelpFormatter(helpFormatter HelpValueFormatter) Option {
})
}
// ValueFormatter configures how the help text is formatted.
func ValueFormatter(helpFormatter HelpValueFormatter) Option {
return OptionFunc(func(k *Kong) error {
k.helpFormatter = helpFormatter
return nil
})
}
// ConfigureHelp sets the HelpOptions to use for printing help.
func ConfigureHelp(options HelpOptions) Option {
return OptionFunc(func(k *Kong) error {
@@ -214,10 +332,75 @@ func ConfigureHelp(options HelpOptions) Option {
})
}
// AutoGroup automatically assigns groups to flags.
func AutoGroup(format func(parent Visitable, flag *Flag) *Group) Option {
return PostBuild(func(kong *Kong) error {
parents := []Visitable{kong.Model}
return Visit(kong.Model, func(node Visitable, next Next) error {
if flag, ok := node.(*Flag); ok && flag.Group == nil {
flag.Group = format(parents[len(parents)-1], flag)
}
parents = append(parents, node)
defer func() { parents = parents[:len(parents)-1] }()
return next(nil)
})
})
}
// Groups associates `group` field tags with group metadata.
//
// This option is used to simplify Kong tags while providing
// rich group information such as title and optional description.
//
// Each key in the "groups" map corresponds to the value of a
// `group` Kong tag, while the first line of the value will be
// the title, and subsequent lines if any will be the description of
// the group.
//
// See also ExplicitGroups for a more structured alternative.
type Groups map[string]string
func (g Groups) Apply(k *Kong) error { //nolint: revive
for key, info := range g {
lines := strings.Split(info, "\n")
title := strings.TrimSpace(lines[0])
description := ""
if len(lines) > 1 {
description = strings.TrimSpace(strings.Join(lines[1:], "\n"))
}
k.groups = append(k.groups, Group{
Key: key,
Title: title,
Description: description,
})
}
return nil
}
// ExplicitGroups associates `group` field tags with their metadata.
//
// It can be used to provide a title or header to a command or flag group.
func ExplicitGroups(groups []Group) Option {
return OptionFunc(func(k *Kong) error {
k.groups = groups
return nil
})
}
// UsageOnError configures Kong to display context-sensitive usage if FatalIfErrorf is called with an error.
func UsageOnError() Option {
return OptionFunc(func(k *Kong) error {
k.usageOnError = true
k.usageOnError = fullUsage
return nil
})
}
// ShortUsageOnError configures Kong to display context-sensitive short
// usage if FatalIfErrorf is called with an error. The default short
// usage message can be overridden with kong.ShortHelp(...).
func ShortUsageOnError() Option {
return OptionFunc(func(k *Kong) error {
k.usageOnError = shortUsage
return nil
})
}
@@ -238,6 +421,31 @@ func Resolvers(resolvers ...Resolver) Option {
})
}
// IgnoreFields will cause kong.New() to skip field names that match any
// of the provided regex patterns. This is useful if you are not able to add a
// kong="-" struct tag to a struct/element before the call to New.
//
// Example: When referencing protoc generated structs, you will likely want to
// ignore/skip XXX_* fields.
func IgnoreFields(regexes ...string) Option {
return OptionFunc(func(k *Kong) error {
for _, r := range regexes {
if r == "" {
return errors.New("regex input cannot be empty")
}
re, err := regexp.Compile(r)
if err != nil {
return fmt.Errorf("unable to compile regex: %v", err)
}
k.ignoreFields = append(k.ignoreFields, re)
}
return nil
})
}
// ConfigurationLoader is a function that builds a resolver from a file.
type ConfigurationLoader func(r io.Reader) (Resolver, error)
@@ -252,12 +460,19 @@ func Configuration(loader ConfigurationLoader, paths ...string) Option {
return OptionFunc(func(k *Kong) error {
k.loader = loader
for _, path := range paths {
if _, err := os.Stat(ExpandPath(path)); os.IsNotExist(err) {
continue
f, err := os.Open(ExpandPath(path))
if err != nil {
if os.IsNotExist(err) || os.IsPermission(err) {
continue
}
return err
}
f.Close()
resolver, err := k.LoadConfig(path)
if err != nil {
return errors.Wrap(err, path)
return fmt.Errorf("%s: %v", path, err)
}
if resolver != nil {
k.resolvers = append(k.resolvers, resolver)
@@ -287,3 +502,66 @@ func ExpandPath(path string) string {
}
return abspath
}
func siftStrings(ss []string, filter func(s string) bool) []string {
i := 0
ss = append([]string(nil), ss...)
for _, s := range ss {
if filter(s) {
ss[i] = s
i++
}
}
return ss[0:i]
}
// DefaultEnvars option inits environment names for flags.
// The name will not generate if tag "env" is "-".
// Predefined environment variables are skipped.
//
// For example:
//
// --some.value -> PREFIX_SOME_VALUE
func DefaultEnvars(prefix string) Option {
processFlag := func(flag *Flag) {
switch env := flag.Envs; {
case flag.Name == "help":
return
case len(env) == 1 && env[0] == "-":
flag.Envs = nil
flag.Value.Tag.Envs = nil
return
case len(env) > 0:
return
}
replacer := strings.NewReplacer("-", "_", ".", "_")
names := append([]string{prefix}, camelCase(replacer.Replace(flag.Name))...)
names = siftStrings(names, func(s string) bool { return !(s == "_" || strings.TrimSpace(s) == "") })
name := strings.ToUpper(strings.Join(names, "_"))
flag.Envs = append(flag.Envs, name)
flag.Value.Tag.Envs = append(flag.Value.Tag.Envs, name)
}
var processNode func(node *Node)
processNode = func(node *Node) {
for _, flag := range node.Flags {
processFlag(flag)
}
for _, node := range node.Children {
processNode(node)
}
}
return PostBuild(func(k *Kong) error {
processNode(k.Model.Node)
return nil
})
}
// FlagNamer allows you to override the default kebab-case automated flag name generation.
func FlagNamer(namer func(fieldName string) string) Option {
return OptionFunc(func(k *Kong) error {
k.flagNamer = namer
return nil
})
}
+18
View File
@@ -0,0 +1,18 @@
{
$schema: "https://docs.renovatebot.com/renovate-schema.json",
extends: [
"config:recommended",
":semanticCommits",
":semanticCommitTypeAll(chore)",
":semanticCommitScope(deps)",
"group:allNonMajor",
"schedule:earlyMondays", // Run once a week.
],
packageRules: [
{
"matchPackageNames": ["golangci-lint"],
"matchManagers": ["hermit"],
"enabled": false
},
]
}
+29 -10
View File
@@ -14,36 +14,55 @@ type Resolver interface {
Validate(app *Application) error
// Resolve the value for a Flag.
Resolve(context *Context, parent *Path, flag *Flag) (interface{}, error)
Resolve(context *Context, parent *Path, flag *Flag) (any, error)
}
// ResolverFunc is a convenience type for non-validating Resolvers.
type ResolverFunc func(context *Context, parent *Path, flag *Flag) (interface{}, error)
type ResolverFunc func(context *Context, parent *Path, flag *Flag) (any, error)
var _ Resolver = ResolverFunc(nil)
func (r ResolverFunc) Resolve(context *Context, parent *Path, flag *Flag) (interface{}, error) { // nolint: golint
func (r ResolverFunc) Resolve(context *Context, parent *Path, flag *Flag) (any, error) { //nolint: revive
return r(context, parent, flag)
}
func (r ResolverFunc) Validate(app *Application) error { return nil } // nolint: golint
func (r ResolverFunc) Validate(app *Application) error { return nil } //nolint: revive
// JSON returns a Resolver that retrieves values from a JSON source.
//
// Hyphens in flag names are replaced with underscores.
// Flag names are used as JSON keys indirectly, by trying snake_case and camelCase variants.
func JSON(r io.Reader) (Resolver, error) {
values := map[string]interface{}{}
values := map[string]any{}
err := json.NewDecoder(r).Decode(&values)
if err != nil {
return nil, err
}
var f ResolverFunc = func(context *Context, parent *Path, flag *Flag) (interface{}, error) {
name := strings.Replace(flag.Name, "-", "_", -1)
var f ResolverFunc = func(context *Context, parent *Path, flag *Flag) (any, error) {
name := strings.ReplaceAll(flag.Name, "-", "_")
snakeCaseName := snakeCase(flag.Name)
raw, ok := values[name]
if !ok {
return nil, nil
if ok {
return raw, nil
} else if raw, ok = values[snakeCaseName]; ok {
return raw, nil
}
raw = values
for _, part := range strings.Split(name, ".") {
if values, ok := raw.(map[string]any); ok {
raw, ok = values[part]
if !ok {
return nil, nil
}
} else {
return nil, nil
}
}
return raw, nil
}
return f, nil
}
func snakeCase(name string) string {
name = strings.Join(strings.Split(strings.Title(name), "-"), "") //nolint:staticcheck // Unicode punctuation not an issue
return strings.ToLower(name[:1]) + name[1:]
}
+32 -13
View File
@@ -41,7 +41,7 @@ func (t TokenType) String() string {
// Token created by Scanner.
type Token struct {
Value interface{}
Value any
Type TokenType
}
@@ -82,7 +82,7 @@ func (t Token) InferredType() TokenType {
return t.Type
}
if v, ok := t.Value.(string); ok {
if strings.HasPrefix(v, "--") { // nolint: gocritic
if strings.HasPrefix(v, "--") { //nolint: gocritic
return FlagToken
} else if v == "-" {
return PositionalArgumentToken
@@ -109,18 +109,24 @@ func (t Token) IsValue() bool {
//
// For example, the token "--foo=bar" will be split into the following by the parser:
//
// [{FlagToken, "foo"}, {FlagValueToken, "bar"}]
// [{FlagToken, "foo"}, {FlagValueToken, "bar"}]
type Scanner struct {
args []Token
allowHyphenated bool
args []Token
}
// ScanAsType creates a new Scanner from args with the given type.
func ScanAsType(ttype TokenType, args ...string) *Scanner {
s := &Scanner{}
for _, arg := range args {
s.args = append(s.args, Token{Value: arg, Type: ttype})
}
return s
}
// Scan creates a new Scanner from args with untyped tokens.
func Scan(args ...string) *Scanner {
s := &Scanner{}
for _, arg := range args {
s.args = append(s.args, Token{Value: arg})
}
return s
return ScanAsType(UntypedToken, args...)
}
// ScanFromTokens creates a new Scanner from a slice of tokens.
@@ -128,6 +134,14 @@ func ScanFromTokens(tokens ...Token) *Scanner {
return &Scanner{args: tokens}
}
// AllowHyphenPrefixedParameters enables or disables hyphen-prefixed flag parameters on this Scanner.
//
// Disabled by default.
func (s *Scanner) AllowHyphenPrefixedParameters(enable bool) *Scanner {
s.allowHyphenated = enable
return s
}
// Len returns the number of input arguments.
func (s *Scanner) Len() int {
return len(s.args)
@@ -157,7 +171,7 @@ func (e *expectedError) Error() string {
// "context" is used to assist the user if the value can not be popped, eg. "expected <context> value but got <type>"
func (s *Scanner) PopValue(context string) (Token, error) {
t := s.Pop()
if !t.IsValue() {
if !s.allowHyphenated && !t.IsValue() {
return t, &expectedError{context, t}
}
return t, nil
@@ -166,7 +180,7 @@ func (s *Scanner) PopValue(context string) (Token, error) {
// PopValueInto pops a value token into target or returns an error.
//
// "context" is used to assist the user if the value can not be popped, eg. "expected <context> value but got <type>"
func (s *Scanner) PopValueInto(context string, target interface{}) error {
func (s *Scanner) PopValueInto(context string, target any) error {
t, err := s.PopValue(context)
if err != nil {
return err
@@ -198,14 +212,19 @@ func (s *Scanner) Peek() Token {
return s.args[0]
}
// PeekAll remaining tokens
func (s *Scanner) PeekAll() []Token {
return s.args
}
// Push an untyped Token onto the front of the Scanner.
func (s *Scanner) Push(arg interface{}) *Scanner {
func (s *Scanner) Push(arg any) *Scanner {
s.PushToken(Token{Value: arg})
return s
}
// PushTyped pushes a typed token onto the front of the Scanner.
func (s *Scanner) PushTyped(arg interface{}, typ TokenType) *Scanner {
func (s *Scanner) PushTyped(arg any, typ TokenType) *Scanner {
s.PushToken(Token{Value: arg, Type: typ})
return s
}
+263 -58
View File
@@ -1,6 +1,7 @@
package kong
import (
"errors"
"fmt"
"reflect"
"strconv"
@@ -8,54 +9,101 @@ import (
"unicode/utf8"
)
// PassthroughMode indicates how parameters are passed through when "passthrough" is set.
type PassthroughMode int
const (
// PassThroughModeNone indicates passthrough mode is disabled.
PassThroughModeNone PassthroughMode = iota
// PassThroughModeAll indicates that all parameters, including flags, are passed through. It is the default.
PassThroughModeAll
// PassThroughModePartial will validate flags until the first positional argument is encountered, then pass through all remaining positional arguments.
PassThroughModePartial
)
// Tag represents the parsed state of Kong tags in a struct field tag.
type Tag struct {
Ignored bool // Field is ignored by Kong. ie. kong:"-"
Cmd bool
Arg bool
Required bool
Optional bool
Name string
Help string
Type string
Default string
Format string
PlaceHolder string
Env string
Short rune
Hidden bool
Sep rune
MapSep rune
Enum string
Group string
Xor string
Vars Vars
Prefix string // Optional prefix on anonymous structs. All sub-flags will have this prefix.
Embed bool
Ignored bool // Field is ignored by Kong. ie. kong:"-"
Cmd bool
Arg bool
Required bool
Optional bool
Name string
Help string
Type string
TypeName string
HasDefault bool
Default string
Format string
PlaceHolder string
Envs []string
Short rune
Hidden bool
Sep rune
MapSep rune
Enum string
Group string
Xor []string
And []string
Vars Vars
Prefix string // Optional prefix on anonymous structs. All sub-flags will have this prefix.
EnvPrefix string
XorPrefix string // Optional prefix on XOR/AND groups.
Embed bool
Aliases []string
Negatable string
Passthrough bool // Deprecated: use PassthroughMode instead.
PassthroughMode PassthroughMode
// Storage for all tag keys for arbitrary lookups.
items map[string][]string
}
type tagChars struct {
sep, quote, assign rune
func (t *Tag) String() string {
out := []string{}
for key, list := range t.items {
for _, value := range list {
out = append(out, fmt.Sprintf("%s:%q", key, value))
}
}
return strings.Join(out, " ")
}
var kongChars = tagChars{sep: ',', quote: '\'', assign: '='}
var bareChars = tagChars{sep: ' ', quote: '"', assign: ':'}
type tagChars struct {
sep, quote, assign rune
needsUnquote bool
}
func parseTagItems(tagString string, chr tagChars) map[string][]string {
var kongChars = tagChars{sep: ',', quote: '\'', assign: '=', needsUnquote: false}
var bareChars = tagChars{sep: ' ', quote: '"', assign: ':', needsUnquote: true}
//nolint:gocyclo
func parseTagItems(tagString string, chr tagChars) (map[string][]string, error) {
d := map[string][]string{}
key := []rune{}
value := []rune{}
quotes := false
inKey := true
add := func() {
d[string(key)] = append(d[string(key)], string(value))
add := func() error {
// Bare tags are quoted, therefore we need to unquote them in the same fashion reflect.Lookup() (implicitly)
// unquotes "kong tags".
s := string(value)
if chr.needsUnquote && s != "" {
if unquoted, err := strconv.Unquote(fmt.Sprintf(`"%s"`, s)); err == nil {
s = unquoted
} else {
return fmt.Errorf("unquoting tag value `%s`: %w", s, err)
}
}
d[string(key)] = append(d[string(key)], s)
key = []rune{}
value = []rune{}
inKey = true
return nil
}
runes := []rune(tagString)
@@ -69,7 +117,10 @@ func parseTagItems(tagString string, chr tagChars) map[string][]string {
eof = true
}
if !quotes && r == chr.sep {
add()
if err := add(); err != nil {
return nil, err
}
continue
}
if r == chr.assign && inKey {
@@ -79,6 +130,12 @@ func parseTagItems(tagString string, chr tagChars) map[string][]string {
if r == '\\' {
if next == chr.quote {
idx++
// We need to keep the backslashes, otherwise subsequent unquoting cannot work
if chr.needsUnquote {
value = append(value, r)
}
r = chr.quote
}
} else if r == chr.quote {
@@ -87,11 +144,10 @@ func parseTagItems(tagString string, chr tagChars) map[string][]string {
if next == chr.sep || eof {
continue
}
fail("%v has an unexpected char at pos %v", tagString, idx)
} else {
quotes = true
continue
return nil, fmt.Errorf("%v has an unexpected char at pos %v", tagString, idx)
}
quotes = true
continue
}
if inKey {
key = append(key, r)
@@ -100,77 +156,194 @@ func parseTagItems(tagString string, chr tagChars) map[string][]string {
}
}
if quotes {
fail("%v is not quoted properly", tagString)
return nil, fmt.Errorf("%v is not quoted properly", tagString)
}
add()
if err := add(); err != nil {
return nil, err
}
return d
return d, nil
}
func getTagInfo(ft reflect.StructField) (string, tagChars) {
s, ok := ft.Tag.Lookup("kong")
if ok {
return s, kongChars
func parseStructTagItems(tag reflect.StructTag) (map[string][]string, error) {
items, err := parseTagItems(string(tag), bareChars)
if err != nil {
return nil, err
}
delete(items, "kong")
return string(ft.Tag), bareChars
kongTag, ok := tag.Lookup("kong")
if !ok {
return items, nil
}
kongItems, err := parseTagItems(kongTag, kongChars)
if err != nil {
return nil, err
}
for key, value := range kongItems {
items[key] = append(value, items[key]...)
}
return items, nil
}
func newEmptyTag() *Tag {
return &Tag{items: map[string][]string{}}
}
func parseTag(fv reflect.Value, ft reflect.StructField) *Tag {
func tagSplitFn(r rune) bool {
return r == ',' || r == ' '
}
func parseTagString(s string) (*Tag, error) {
items, err := parseTagItems(s, bareChars)
if err != nil {
return nil, err
}
t := &Tag{
items: items,
}
err = hydrateTag(t, nil)
if err != nil {
return nil, fmt.Errorf("%s: %s", s, err)
}
return t, nil
}
func parseTag(parent reflect.Value, ft reflect.StructField) (*Tag, error) {
if ft.Tag.Get("kong") == "-" {
t := newEmptyTag()
t.Ignored = true
return t
return t, nil
}
items := map[string][]string{}
// First use a [Signature] if present
signatureTag, ok := maybeGetSignature(ft.Type)
if ok {
signatureItems, err := parseStructTagItems(signatureTag)
if err != nil {
return nil, err
}
items = signatureItems
}
// Next overlay the field's tags.
fieldItems, err := parseStructTagItems(ft.Tag)
if err != nil {
return nil, err
}
for key, value := range fieldItems {
// Prepend field tag values
items[key] = append(value, items[key]...)
}
t := &Tag{
items: parseTagItems(getTagInfo(ft)),
items: items,
}
err = hydrateTag(t, ft.Type)
if err != nil {
return nil, failField(parent, ft, "%s", err)
}
return t, nil
}
func hydrateTag(t *Tag, typ reflect.Type) error { //nolint: gocyclo
var typeName string
var isBool bool
var isBoolPtr bool
if typ != nil {
typeName = typ.Name()
isBool = typ.Kind() == reflect.Bool
isBoolPtr = typ.Kind() == reflect.Ptr && typ.Elem().Kind() == reflect.Bool
}
var err error
t.Cmd = t.Has("cmd")
t.Arg = t.Has("arg")
required := t.Has("required")
optional := t.Has("optional")
if required && optional {
fail("can't specify both required and optional")
return fmt.Errorf("can't specify both required and optional")
}
t.Required = required
t.Optional = optional
t.HasDefault = t.Has("default")
t.Default = t.Get("default")
// Arguments with defaults are always optional.
if t.Arg && t.Default != "" {
if t.Arg && t.HasDefault {
t.Optional = true
} else if t.Arg && !optional { // Arguments are required unless explicitly made optional.
t.Required = true
}
t.Name = t.Get("name")
t.Help = t.Get("help")
t.Type = t.Get("type")
t.Env = t.Get("env")
t.Short, _ = t.GetRune("short")
t.TypeName = typeName
for _, env := range t.GetAll("env") {
t.Envs = append(t.Envs, strings.FieldsFunc(env, tagSplitFn)...)
}
t.Short, err = t.GetRune("short")
if err != nil && t.Get("short") != "" {
return fmt.Errorf("invalid short flag name %q: %s", t.Get("short"), err)
}
t.Hidden = t.Has("hidden")
t.Format = t.Get("format")
t.Sep, _ = t.GetSep("sep", ',')
t.MapSep, _ = t.GetSep("mapsep", ';')
t.Group = t.Get("group")
t.Xor = t.Get("xor")
for _, xor := range t.GetAll("xor") {
t.Xor = append(t.Xor, strings.FieldsFunc(xor, tagSplitFn)...)
}
for _, and := range t.GetAll("and") {
t.And = append(t.And, strings.FieldsFunc(and, tagSplitFn)...)
}
t.Prefix = t.Get("prefix")
t.EnvPrefix = t.Get("envprefix")
t.XorPrefix = t.Get("xorprefix")
t.Embed = t.Has("embed")
if t.Has("negatable") {
if !isBool && !isBoolPtr {
return fmt.Errorf("negatable can only be set on booleans")
}
negatable := t.Get("negatable")
if negatable == "" {
negatable = negatableDefault // placeholder for default negation of --no-<flag>
}
t.Negatable = negatable
}
aliases := t.Get("aliases")
if len(aliases) > 0 {
t.Aliases = append(t.Aliases, strings.FieldsFunc(aliases, tagSplitFn)...)
}
t.Vars = Vars{}
for _, set := range t.GetAll("set") {
parts := strings.SplitN(set, "=", 2)
if len(parts) == 0 {
fail("set should be in the form key=value but got %q", set)
return fmt.Errorf("set should be in the form key=value but got %q", set)
}
t.Vars[parts[0]] = parts[1]
}
t.PlaceHolder = t.Get("placeholder")
if t.PlaceHolder == "" {
t.PlaceHolder = strings.ToUpper(dashedString(fv.Type().Name()))
}
t.Enum = t.Get("enum")
return t
scalarType := typ == nil || !(typ.Kind() == reflect.Slice || typ.Kind() == reflect.Map || typ.Kind() == reflect.Ptr)
if t.Enum != "" && !(t.Required || t.HasDefault) && scalarType {
return fmt.Errorf("enum value is only valid if it is either required or has a valid default value")
}
passthrough := t.Has("passthrough")
if passthrough && !t.Arg && !t.Cmd {
return fmt.Errorf("passthrough only makes sense for positional arguments or commands")
}
t.Passthrough = passthrough
if t.Passthrough {
passthroughMode := t.Get("passthrough")
switch passthroughMode {
case "partial":
t.PassthroughMode = PassThroughModePartial
case "all", "":
t.PassthroughMode = PassThroughModeAll
default:
return fmt.Errorf("invalid passthrough mode %q, must be one of 'partial' or 'all'", passthroughMode)
}
}
return nil
}
// Has returns true if the tag contained the given key.
@@ -212,9 +385,10 @@ func (t *Tag) GetInt(k string) (int64, error) {
// GetRune parses the given tag as a rune.
func (t *Tag) GetRune(k string) (rune, error) {
r, _ := utf8.DecodeRuneInString(t.Get(k))
if r == utf8.RuneError {
return 0, fmt.Errorf("%v has a rune error", t.Get(k))
value := t.Get(k)
r, size := utf8.DecodeRuneInString(value)
if r == utf8.RuneError || size < len(value) {
return 0, errors.New("invalid rune")
}
return r, nil
}
@@ -238,3 +412,34 @@ func (t *Tag) GetSep(k string, dflt rune) (rune, error) {
}
return r, nil
}
// Signature allows flags, args and commands to supply a default set of tags,
// that can be overridden by the field itself.
type Signature interface {
// Signature returns default tags for the flag, arg or command.
//
// eg. `name:"migrate" help:"Run migrations" aliases:"mig,mg"`.
Signature() string
}
var signatureOverrideType = reflect.TypeOf((*Signature)(nil)).Elem()
func maybeGetSignature(t reflect.Type) (reflect.StructTag, bool) {
ut := t
if ut.Kind() == reflect.Pointer {
ut = ut.Elem()
}
ptr := reflect.New(ut)
var sig string
for _, v := range []reflect.Value{ptr, ptr.Elem()} {
if v.Type().Implements(signatureOverrideType) {
sig = v.Interface().(Signature).Signature() //nolint:forcetypeassert
break
}
}
sig = strings.TrimSpace(sig)
if sig == "" {
return "", false
}
return reflect.StructTag(sig), true
}
+25 -3
View File
@@ -2,6 +2,8 @@ package kong
import (
"fmt"
"os"
"reflect"
)
// ConfigFlag uses the configured (via kong.Configuration(loader)) configuration loader to load configuration
@@ -15,7 +17,7 @@ func (c ConfigFlag) BeforeResolve(kong *Kong, ctx *Context, trace *Path) error {
if kong.loader == nil {
return fmt.Errorf("kong must be configured with kong.Configuration(...)")
}
path := string(ctx.FlagValue(trace.Flag).(ConfigFlag))
path := string(ctx.FlagValue(trace.Flag).(ConfigFlag)) //nolint
resolver, err := kong.LoadConfig(path)
if err != nil {
return err
@@ -27,9 +29,29 @@ func (c ConfigFlag) BeforeResolve(kong *Kong, ctx *Context, trace *Path) error {
// VersionFlag is a flag type that can be used to display a version number, stored in the "version" variable.
type VersionFlag bool
// BeforeApply writes the version variable and terminates with a 0 exit status.
func (v VersionFlag) BeforeApply(app *Kong, vars Vars) error {
// BeforeReset writes the version variable and terminates with a 0 exit status.
func (v VersionFlag) BeforeReset(app *Kong, vars Vars) error {
fmt.Fprintln(app.Stdout, vars["version"])
app.Exit(0)
return nil
}
// ChangeDirFlag changes the current working directory to a path specified by a flag
// early in the parsing process, changing how other flags resolve relative paths.
//
// Use this flag to provide a "git -C" like functionality.
//
// It is not compatible with custom named decoders, e.g., existingdir.
type ChangeDirFlag string
// Decode is used to create a side effect of changing the current working directory.
func (c ChangeDirFlag) Decode(ctx *DecodeContext) error {
var path string
err := ctx.Scan.PopValueInto("string", &path)
if err != nil {
return err
}
path = ExpandPath(path)
ctx.Value.Target.Set(reflect.ValueOf(ChangeDirFlag(path)))
return os.Chdir(path)
}
-3
View File
@@ -1,3 +0,0 @@
module github.com/lane-c-wagner/go-password-validator
go 1.15
-24
View File
@@ -1,24 +0,0 @@
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
# Folders
_obj
_test
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe
*.test
*.prof
-15
View File
@@ -1,15 +0,0 @@
language: go
go_import_path: github.com/pkg/errors
go:
- 1.4.x
- 1.5.x
- 1.6.x
- 1.7.x
- 1.8.x
- 1.9.x
- 1.10.x
- 1.11.x
- tip
script:
- go test -v ./...
-23
View File
@@ -1,23 +0,0 @@
Copyright (c) 2015, Dave Cheney <dave@cheney.net>
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-52
View File
@@ -1,52 +0,0 @@
# errors [![Travis-CI](https://travis-ci.org/pkg/errors.svg)](https://travis-ci.org/pkg/errors) [![AppVeyor](https://ci.appveyor.com/api/projects/status/b98mptawhudj53ep/branch/master?svg=true)](https://ci.appveyor.com/project/davecheney/errors/branch/master) [![GoDoc](https://godoc.org/github.com/pkg/errors?status.svg)](http://godoc.org/github.com/pkg/errors) [![Report card](https://goreportcard.com/badge/github.com/pkg/errors)](https://goreportcard.com/report/github.com/pkg/errors) [![Sourcegraph](https://sourcegraph.com/github.com/pkg/errors/-/badge.svg)](https://sourcegraph.com/github.com/pkg/errors?badge)
Package errors provides simple error handling primitives.
`go get github.com/pkg/errors`
The traditional error handling idiom in Go is roughly akin to
```go
if err != nil {
return err
}
```
which applied recursively up the call stack results in error reports without context or debugging information. The errors package allows programmers to add context to the failure path in their code in a way that does not destroy the original value of the error.
## Adding context to an error
The errors.Wrap function returns a new error that adds context to the original error. For example
```go
_, err := ioutil.ReadAll(r)
if err != nil {
return errors.Wrap(err, "read failed")
}
```
## Retrieving the cause of an error
Using `errors.Wrap` constructs a stack of errors, adding context to the preceding error. Depending on the nature of the error it may be necessary to reverse the operation of errors.Wrap to retrieve the original error for inspection. Any error value which implements this interface can be inspected by `errors.Cause`.
```go
type causer interface {
Cause() error
}
```
`errors.Cause` will recursively retrieve the topmost error which does not implement `causer`, which is assumed to be the original cause. For example:
```go
switch err := errors.Cause(err).(type) {
case *MyError:
// handle specifically
default:
// unknown error
}
```
[Read the package documentation for more information](https://godoc.org/github.com/pkg/errors).
## Contributing
We welcome pull requests, bug fixes and issue reports. With that said, the bar for adding new symbols to this package is intentionally set high.
Before proposing a change, please discuss your change by raising an issue.
## License
BSD-2-Clause
-32
View File
@@ -1,32 +0,0 @@
version: build-{build}.{branch}
clone_folder: C:\gopath\src\github.com\pkg\errors
shallow_clone: true # for startup speed
environment:
GOPATH: C:\gopath
platform:
- x64
# http://www.appveyor.com/docs/installed-software
install:
# some helpful output for debugging builds
- go version
- go env
# pre-installed MinGW at C:\MinGW is 32bit only
# but MSYS2 at C:\msys64 has mingw64
- set PATH=C:\msys64\mingw64\bin;%PATH%
- gcc --version
- g++ --version
build_script:
- go install -v ./...
test_script:
- set PATH=C:\gopath\bin;%PATH%
- go test -v ./...
#artifacts:
# - path: '%GOPATH%\bin\*.exe'
deploy: off
-282
View File
@@ -1,282 +0,0 @@
// Package errors provides simple error handling primitives.
//
// The traditional error handling idiom in Go is roughly akin to
//
// if err != nil {
// return err
// }
//
// which when applied recursively up the call stack results in error reports
// without context or debugging information. The errors package allows
// programmers to add context to the failure path in their code in a way
// that does not destroy the original value of the error.
//
// Adding context to an error
//
// The errors.Wrap function returns a new error that adds context to the
// original error by recording a stack trace at the point Wrap is called,
// together with the supplied message. For example
//
// _, err := ioutil.ReadAll(r)
// if err != nil {
// return errors.Wrap(err, "read failed")
// }
//
// If additional control is required, the errors.WithStack and
// errors.WithMessage functions destructure errors.Wrap into its component
// operations: annotating an error with a stack trace and with a message,
// respectively.
//
// Retrieving the cause of an error
//
// Using errors.Wrap constructs a stack of errors, adding context to the
// preceding error. Depending on the nature of the error it may be necessary
// to reverse the operation of errors.Wrap to retrieve the original error
// for inspection. Any error value which implements this interface
//
// type causer interface {
// Cause() error
// }
//
// can be inspected by errors.Cause. errors.Cause will recursively retrieve
// the topmost error that does not implement causer, which is assumed to be
// the original cause. For example:
//
// switch err := errors.Cause(err).(type) {
// case *MyError:
// // handle specifically
// default:
// // unknown error
// }
//
// Although the causer interface is not exported by this package, it is
// considered a part of its stable public interface.
//
// Formatted printing of errors
//
// All error values returned from this package implement fmt.Formatter and can
// be formatted by the fmt package. The following verbs are supported:
//
// %s print the error. If the error has a Cause it will be
// printed recursively.
// %v see %s
// %+v extended format. Each Frame of the error's StackTrace will
// be printed in detail.
//
// Retrieving the stack trace of an error or wrapper
//
// New, Errorf, Wrap, and Wrapf record a stack trace at the point they are
// invoked. This information can be retrieved with the following interface:
//
// type stackTracer interface {
// StackTrace() errors.StackTrace
// }
//
// The returned errors.StackTrace type is defined as
//
// type StackTrace []Frame
//
// The Frame type represents a call site in the stack trace. Frame supports
// the fmt.Formatter interface that can be used for printing information about
// the stack trace of this error. For example:
//
// if err, ok := err.(stackTracer); ok {
// for _, f := range err.StackTrace() {
// fmt.Printf("%+s:%d", f)
// }
// }
//
// Although the stackTracer interface is not exported by this package, it is
// considered a part of its stable public interface.
//
// See the documentation for Frame.Format for more details.
package errors
import (
"fmt"
"io"
)
// New returns an error with the supplied message.
// New also records the stack trace at the point it was called.
func New(message string) error {
return &fundamental{
msg: message,
stack: callers(),
}
}
// Errorf formats according to a format specifier and returns the string
// as a value that satisfies error.
// Errorf also records the stack trace at the point it was called.
func Errorf(format string, args ...interface{}) error {
return &fundamental{
msg: fmt.Sprintf(format, args...),
stack: callers(),
}
}
// fundamental is an error that has a message and a stack, but no caller.
type fundamental struct {
msg string
*stack
}
func (f *fundamental) Error() string { return f.msg }
func (f *fundamental) Format(s fmt.State, verb rune) {
switch verb {
case 'v':
if s.Flag('+') {
io.WriteString(s, f.msg)
f.stack.Format(s, verb)
return
}
fallthrough
case 's':
io.WriteString(s, f.msg)
case 'q':
fmt.Fprintf(s, "%q", f.msg)
}
}
// WithStack annotates err with a stack trace at the point WithStack was called.
// If err is nil, WithStack returns nil.
func WithStack(err error) error {
if err == nil {
return nil
}
return &withStack{
err,
callers(),
}
}
type withStack struct {
error
*stack
}
func (w *withStack) Cause() error { return w.error }
func (w *withStack) Format(s fmt.State, verb rune) {
switch verb {
case 'v':
if s.Flag('+') {
fmt.Fprintf(s, "%+v", w.Cause())
w.stack.Format(s, verb)
return
}
fallthrough
case 's':
io.WriteString(s, w.Error())
case 'q':
fmt.Fprintf(s, "%q", w.Error())
}
}
// Wrap returns an error annotating err with a stack trace
// at the point Wrap is called, and the supplied message.
// If err is nil, Wrap returns nil.
func Wrap(err error, message string) error {
if err == nil {
return nil
}
err = &withMessage{
cause: err,
msg: message,
}
return &withStack{
err,
callers(),
}
}
// Wrapf returns an error annotating err with a stack trace
// at the point Wrapf is called, and the format specifier.
// If err is nil, Wrapf returns nil.
func Wrapf(err error, format string, args ...interface{}) error {
if err == nil {
return nil
}
err = &withMessage{
cause: err,
msg: fmt.Sprintf(format, args...),
}
return &withStack{
err,
callers(),
}
}
// WithMessage annotates err with a new message.
// If err is nil, WithMessage returns nil.
func WithMessage(err error, message string) error {
if err == nil {
return nil
}
return &withMessage{
cause: err,
msg: message,
}
}
// WithMessagef annotates err with the format specifier.
// If err is nil, WithMessagef returns nil.
func WithMessagef(err error, format string, args ...interface{}) error {
if err == nil {
return nil
}
return &withMessage{
cause: err,
msg: fmt.Sprintf(format, args...),
}
}
type withMessage struct {
cause error
msg string
}
func (w *withMessage) Error() string { return w.msg + ": " + w.cause.Error() }
func (w *withMessage) Cause() error { return w.cause }
func (w *withMessage) Format(s fmt.State, verb rune) {
switch verb {
case 'v':
if s.Flag('+') {
fmt.Fprintf(s, "%+v\n", w.Cause())
io.WriteString(s, w.msg)
return
}
fallthrough
case 's', 'q':
io.WriteString(s, w.Error())
}
}
// Cause returns the underlying cause of the error, if possible.
// An error value has a cause if it implements the following
// interface:
//
// type causer interface {
// Cause() error
// }
//
// If the error does not implement Cause, the original error will
// be returned. If the error is nil, nil will be returned without further
// investigation.
func Cause(err error) error {
type causer interface {
Cause() error
}
for err != nil {
cause, ok := err.(causer)
if !ok {
break
}
err = cause.Cause()
}
return err
}
-147
View File
@@ -1,147 +0,0 @@
package errors
import (
"fmt"
"io"
"path"
"runtime"
"strings"
)
// Frame represents a program counter inside a stack frame.
type Frame uintptr
// pc returns the program counter for this frame;
// multiple frames may have the same PC value.
func (f Frame) pc() uintptr { return uintptr(f) - 1 }
// file returns the full path to the file that contains the
// function for this Frame's pc.
func (f Frame) file() string {
fn := runtime.FuncForPC(f.pc())
if fn == nil {
return "unknown"
}
file, _ := fn.FileLine(f.pc())
return file
}
// line returns the line number of source code of the
// function for this Frame's pc.
func (f Frame) line() int {
fn := runtime.FuncForPC(f.pc())
if fn == nil {
return 0
}
_, line := fn.FileLine(f.pc())
return line
}
// Format formats the frame according to the fmt.Formatter interface.
//
// %s source file
// %d source line
// %n function name
// %v equivalent to %s:%d
//
// Format accepts flags that alter the printing of some verbs, as follows:
//
// %+s function name and path of source file relative to the compile time
// GOPATH separated by \n\t (<funcname>\n\t<path>)
// %+v equivalent to %+s:%d
func (f Frame) Format(s fmt.State, verb rune) {
switch verb {
case 's':
switch {
case s.Flag('+'):
pc := f.pc()
fn := runtime.FuncForPC(pc)
if fn == nil {
io.WriteString(s, "unknown")
} else {
file, _ := fn.FileLine(pc)
fmt.Fprintf(s, "%s\n\t%s", fn.Name(), file)
}
default:
io.WriteString(s, path.Base(f.file()))
}
case 'd':
fmt.Fprintf(s, "%d", f.line())
case 'n':
name := runtime.FuncForPC(f.pc()).Name()
io.WriteString(s, funcname(name))
case 'v':
f.Format(s, 's')
io.WriteString(s, ":")
f.Format(s, 'd')
}
}
// StackTrace is stack of Frames from innermost (newest) to outermost (oldest).
type StackTrace []Frame
// Format formats the stack of Frames according to the fmt.Formatter interface.
//
// %s lists source files for each Frame in the stack
// %v lists the source file and line number for each Frame in the stack
//
// Format accepts flags that alter the printing of some verbs, as follows:
//
// %+v Prints filename, function, and line number for each Frame in the stack.
func (st StackTrace) Format(s fmt.State, verb rune) {
switch verb {
case 'v':
switch {
case s.Flag('+'):
for _, f := range st {
fmt.Fprintf(s, "\n%+v", f)
}
case s.Flag('#'):
fmt.Fprintf(s, "%#v", []Frame(st))
default:
fmt.Fprintf(s, "%v", []Frame(st))
}
case 's':
fmt.Fprintf(s, "%s", []Frame(st))
}
}
// stack represents a stack of program counters.
type stack []uintptr
func (s *stack) Format(st fmt.State, verb rune) {
switch verb {
case 'v':
switch {
case st.Flag('+'):
for _, pc := range *s {
f := Frame(pc)
fmt.Fprintf(st, "\n%+v", f)
}
}
}
}
func (s *stack) StackTrace() StackTrace {
f := make([]Frame, len(*s))
for i := 0; i < len(f); i++ {
f[i] = Frame((*s)[i])
}
return f
}
func callers() *stack {
const depth = 32
var pcs [depth]uintptr
n := runtime.Callers(3, pcs[:])
var st stack = pcs[0:n]
return &st
}
// funcname removes the path prefix component of a function's name reported by func.Name().
func funcname(name string) string {
i := strings.LastIndex(name, "/")
name = name[i+1:]
i = strings.Index(name, ".")
return name[i+1:]
}
+20 -12
View File
@@ -1,18 +1,18 @@
// Package password provides a library for generating high-entropy random
// password strings via the crypto/rand package.
//
// res, err := Generate(64, 10, 10, false, false)
// if err != nil {
// log.Fatal(err)
// }
// log.Printf(res)
// res, err := Generate(64, 10, 10, false, false)
// if err != nil {
// log.Fatal(err)
// }
// log.Print(res)
//
// Most functions are safe for concurrent use.
package password
import (
"crypto/rand"
"errors"
"fmt"
"io"
"math/big"
"strings"
@@ -46,19 +46,23 @@ const (
var (
// ErrExceedsTotalLength is the error returned with the number of digits and
// symbols is greater than the total length.
ErrExceedsTotalLength = errors.New("number of digits and symbols must be less than total length")
ErrExceedsTotalLength = fmt.Errorf("number of digits and symbols must be less than total length")
// ErrLettersExceedsAvailable is the error returned with the number of letters
// exceeds the number of available letters and repeats are not allowed.
ErrLettersExceedsAvailable = errors.New("number of letters exceeds available letters and repeats are not allowed")
ErrLettersExceedsAvailable = fmt.Errorf("number of letters exceeds available letters and repeats are not allowed")
// ErrDigitsExceedsAvailable is the error returned with the number of digits
// exceeds the number of available digits and repeats are not allowed.
ErrDigitsExceedsAvailable = errors.New("number of digits exceeds available digits and repeats are not allowed")
ErrDigitsExceedsAvailable = fmt.Errorf("number of digits exceeds available digits and repeats are not allowed")
// ErrSymbolsExceedsAvailable is the error returned with the number of symbols
// exceeds the number of available symbols and repeats are not allowed.
ErrSymbolsExceedsAvailable = errors.New("number of symbols exceeds available symbols and repeats are not allowed")
ErrSymbolsExceedsAvailable = fmt.Errorf("number of symbols exceeds available symbols and repeats are not allowed")
// ErrNegativeInput is the error returned when the number of digits or symbols
// is negative.
ErrNegativeInput = fmt.Errorf("number of digits and symbols must not be negative")
)
// Generator is the stateful generator which can be used to customize the list
@@ -128,6 +132,10 @@ func NewGenerator(i *GeneratorInput) (*Generator, error) {
// The algorithm is fast, but it's not designed to be performant; it favors
// entropy over speed. This function is safe for concurrent use.
func (g *Generator) Generate(length, numDigits, numSymbols int, noUpper, allowRepeat bool) (string, error) {
if numDigits < 0 || numSymbols < 0 {
return "", ErrNegativeInput
}
letters := g.lowerLetters
if !noUpper {
letters += g.upperLetters
@@ -245,7 +253,7 @@ func randomInsert(reader io.Reader, s, val string) (string, error) {
n, err := rand.Int(reader, big.NewInt(int64(len(s)+1)))
if err != nil {
return "", err
return "", fmt.Errorf("failed to generate random integer: %w", err)
}
i := n.Int64()
return s[0:i] + val + s[i:], nil
@@ -255,7 +263,7 @@ func randomInsert(reader io.Reader, s, val string) (string, error) {
func randomElement(reader io.Reader, s string) (string, error) {
n, err := rand.Int(reader, big.NewInt(int64(len(s))))
if err != nil {
return "", err
return "", fmt.Errorf("failed to generate random integer: %w", err)
}
return string(s[n.Int64()]), nil
}
-1
View File
@@ -1 +0,0 @@
language: go
+15 -199
View File
@@ -1,202 +1,18 @@
Copyright (c) 2020 Josh Baker
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+45
View File
@@ -0,0 +1,45 @@
# B-tree Path Hints
I use a thing I call path hints in my B-tree [C](https://github.com/tidwall/btree.c) and [Go](https://github.com/tidwall/btree) implementations. It's a search optimization.
## The B-tree
A standard [B-tree](https://en.wikipedia.org/wiki/B-tree) is an ordered tree-based data structure that stores its items in nodes. The B-tree has a single root node, which may have children nodes, and those children nodes may also have children nodes.
<img width="322" alt="image" src="https://user-images.githubusercontent.com/1156077/127664015-14ca38bb-1a3b-4d2f-80ff-27be0bd3d886.png">
Searching for items in a B-tree is fast. [O(log N)](https://en.wikipedia.org/wiki/Big_O_notation) to be exact.
This is because the [binary search algorithm](https://en.wikipedia.org/wiki/Binary_search_algorithm) is used.
A binary search works by first comparing the item at the middle-most index of the root node with the target item.
If the middle item is greater than the target item, then it divides the node in two and does the binary search on the left part of the node. If the middle is less, it searches the right part. And so on. If the target item is found, then the search stop. If the item is not found, then the search is passed to the child node at the appropriate index. This traversal terminates when item is found or there are no more child nodes.
<img width="600" alt="image" src="https://user-images.githubusercontent.com/1156077/127664822-6ab4f8f6-8ab5-477e-8e17-f52346f02819.png">
## The Path
Each index is a component of the path to the item (or where the item should be stored, if it does not exist in the tree).
Take the first example image. The item 9 is at path “1/0”. The item 16 is at path “1”. The item 21 is at path “2/1”. The item 5 is at path “0/2”.
## The Path Hint
A Path Hint is a predefined path that is provided to B-tree operations. It’s just a hint that says, “Hey B-tree, instead of starting your binary search with the middle index, start with what I provide you. My path may be wrong, and if so please provide me with the correct path so I get it right the next time.”
I’ve found using path hints can lead to a little performance increase of 150% - 300%. This is because in real-world cases the items that I’m working with are usually nearby each other in the tree.
Take for example inserting a group of timeseries points. They may often be received as chucks of near-contiguous items.
Or, I'm sequentially inserting an ordered group of rows somewhere in the middle of a table.
Or, I have a Redis-style key/value store, where the keys look have the common structure “user:98512:name”, “user:98512:email”, and I want to update a bunch of values for specified user.
Using a path hint may help to avoid the unnecessary binary searching in each of these examples.
While I may see a 3x boost in when the path hint is right on, I'll only see around 5% decrease when the path hint is totally wrong.
## Using a Path Hint
All of the functions that take in a path hint argument mutate the path hint argument.
For single-threaded programs, it’s possible to use one shared path hint per B-tree for the life of the program.
For multi-threaded programs, I find it best to use one path hint per B-tree , per thread.
For server-client programs, one path hint per B-tree, per client should suffice.
+394 -50
View File
@@ -1,23 +1,232 @@
BTree implementation for Go
===========================
# btree
![Travis CI Build Status](https://api.travis-ci.org/tidwall/btree.svg?branch=master)
[![GoDoc](https://godoc.org/github.com/tidwall/btree?status.svg)](https://godoc.org/github.com/tidwall/btree)
This package provides an in-memory B-Tree implementation for Go, useful as
an ordered, mutable data structure.
An efficient [B-tree](https://en.wikipedia.org/wiki/B-tree) implementation in Go.
This is a fork of the wonderful [google/btree](https://github.com/google/btree) package. It's has all the same great features and adds a few more.
## Features
- Descend* functions for iterating backwards.
- Iteration performance boost.
- User defined context.
- Support for [Generics](#generics) (Go 1.18+).
- `Map` and `Set` types for ordered key-value maps and sets,
- Fast bulk loading for pre-ordered data using the `Load()` method.
- `Copy()` method with copy-on-write support.
- Thread-safe operations.
- [Path hinting](PATH_HINT.md) optimization for operations with nearby keys.
User defined context
--------------------
This is a great new feature that allows for entering the same item into multiple B-trees, and each B-tree have a different ordering formula.
## Using
For example:
To start using this package, install Go and run:
```sh
$ go get github.com/tidwall/btree
```
## B-tree types
This package includes the following types of B-trees:
- [`btree.Map`](#btreemap):
A fast B-tree for storing ordered key value pairs.
Go 1.18+
- [`btree.Set`](#btreeset):
Like `Map`, but only for storing keys.
Go 1.18+
- [`btree.BTreeG`](#btreegeneric):
A feature-rich B-tree for storing data using a custom comparator.
Go 1.18+
- [`btree.BTree`](#btreebtree):
Like `BTreeG` but uses the `interface{}` type for data. Backwards compatible.
Go 1.16+
### btree.Map
```go
// Basic
Set(key, value) // insert or replace an item
Get(key, value) // get an existing item
Delete(key) // delete an item
Len() // return the number of items in the map
// Iteration
Scan(iter) // scan items in ascending order
Reverse(iter) // scan items in descending order
Ascend(key, iter) // scan items in ascending order that are >= to key
Descend(key, iter) // scan items in descending order that are <= to key.
Iter() // returns a read-only iterator for for-loops.
// Array-like operations
GetAt(index) // returns the item at index
DeleteAt(index) // deletes the item at index
// Bulk-loading
Load(key, value) // load presorted items into tree
```
#### Example
```go
package main
import (
"fmt"
"github.com/tidwall/btree"
)
func main() {
// create a map
var users btree.Map[string, string]
// add some users
users.Set("user:4", "Andrea")
users.Set("user:6", "Andy")
users.Set("user:2", "Andy")
users.Set("user:1", "Jane")
users.Set("user:5", "Janet")
users.Set("user:3", "Steve")
// Iterate over the maps and print each user
users.Scan(func(key, value string) bool {
fmt.Printf("%s %s\n", key, value)
return true
})
fmt.Printf("\n")
// Delete a couple
users.Delete("user:5")
users.Delete("user:1")
// print the map again
users.Scan(func(key, value string) bool {
fmt.Printf("%s %s\n", key, value)
return true
})
fmt.Printf("\n")
// Output:
// user:1 Jane
// user:2 Andy
// user:3 Steve
// user:4 Andrea
// user:5 Janet
// user:6 Andy
//
// user:2 Andy
// user:3 Steve
// user:4 Andrea
// user:6 Andy
}
```
### btree.Set
```go
// Basic
Insert(key) // insert an item
Contains(key) // test if item exists
Delete(key) // delete an item
Len() // return the number of items in the set
// Iteration
Scan(iter) // scan items in ascending order
Reverse(iter) // scan items in descending order
Ascend(key, iter) // scan items in ascending order that are >= to key
Descend(key, iter) // scan items in descending order that are <= to key.
Iter() // returns a read-only iterator for for-loops.
// Array-like operations
GetAt(index) // returns the item at index
DeleteAt(index) // deletes the item at index
// Bulk-loading
Load(key) // load presorted item into tree
```
#### Example
```go
package main
import (
"fmt"
"github.com/tidwall/btree"
)
func main() {
// create a set
var names btree.Set[string]
// add some names
names.Insert("Jane")
names.Insert("Andrea")
names.Insert("Steve")
names.Insert("Andy")
names.Insert("Janet")
names.Insert("Andy")
// Iterate over the maps and print each user
names.Scan(func(key string) bool {
fmt.Printf("%s\n", key)
return true
})
fmt.Printf("\n")
// Delete a couple
names.Delete("Steve")
names.Delete("Andy")
// print the map again
names.Scan(func(key string) bool {
fmt.Printf("%s\n", key)
return true
})
fmt.Printf("\n")
// Output:
// Andrea
// Andy
// Jane
// Janet
// Steve
//
// Andrea
// Jane
// Janet
}
```
### btree.BTreeG
```go
// Basic
Set(item) // insert or replace an item
Get(item) // get an existing item
Delete(item) // delete an item
Len() // return the number of items in the btree
// Iteration
Scan(iter) // scan items in ascending order
Reverse(iter) // scan items in descending order
Ascend(key, iter) // scan items in ascending order that are >= to key
Descend(key, iter) // scan items in descending order that are <= to key.
Iter() // returns a read-only iterator for for-loops.
// Array-like operations
GetAt(index) // returns the item at index
DeleteAt(index) // deletes the item at index
// Bulk-loading
Load(item) // load presorted items into tree
// Path hinting
SetHint(item, *hint) // insert or replace an existing item
GetHint(item, *hint) // get an existing item
DeleteHint(item, *hint) // delete an item
// Copy-on-write
Copy() // copy the btree
```
#### Example
```go
package main
@@ -32,30 +241,154 @@ type Item struct {
Key, Val string
}
func (i1 *Item) Less(item btree.Item, ctx interface{}) bool {
i2 := item.(*Item)
switch tag := ctx.(type) {
case string:
if tag == "vals" {
if i1.Val < i2.Val {
return true
} else if i1.Val > i2.Val {
return false
}
// Both vals are equal so we should fall though
// and let the key comparison take over.
}
// byKeys is a comparison function that compares item keys and returns true
// when a is less than b.
func byKeys(a, b Item) bool {
return a.Key < b.Key
}
// byVals is a comparison function that compares item values and returns true
// when a is less than b.
func byVals(a, b Item) bool {
if a.Val < b.Val {
return true
}
return i1.Key < i2.Key
if a.Val > b.Val {
return false
}
// Both vals are equal so we should fall though
// and let the key comparison take over.
return byKeys(a, b)
}
func main() {
// Create a tree for keys and a tree for values.
// The "keys" tree will be sorted on the Keys field.
// The "values" tree will be sorted on the Values field.
keys := btree.New(16, "keys")
vals := btree.New(16, "vals")
keys := btree.NewBTreeG[Item](byKeys)
vals := btree.NewBTreeG[Item](byVals)
// Create some items.
users := []Item{
Item{Key: "user:1", Val: "Jane"},
Item{Key: "user:2", Val: "Andy"},
Item{Key: "user:3", Val: "Steve"},
Item{Key: "user:4", Val: "Andrea"},
Item{Key: "user:5", Val: "Janet"},
Item{Key: "user:6", Val: "Andy"},
}
// Insert each user into both trees
for _, user := range users {
keys.Set(user)
vals.Set(user)
}
// Iterate over each user in the key tree
keys.Scan(func(item Item) bool {
fmt.Printf("%s %s\n", item.Key, item.Val)
return true
})
fmt.Printf("\n")
// Iterate over each user in the val tree
vals.Scan(func(item Item) bool {
fmt.Printf("%s %s\n", item.Key, item.Val)
return true
})
// Output:
// user:1 Jane
// user:2 Andy
// user:3 Steve
// user:4 Andrea
// user:5 Janet
// user:6 Andy
//
// user:4 Andrea
// user:2 Andy
// user:6 Andy
// user:1 Jane
// user:5 Janet
// user:3 Steve
}
```
### btree.BTree
```go
// Basic
Set(item) // insert or replace an item
Get(item) // get an existing item
Delete(item) // delete an item
Len() // return the number of items in the btree
// Iteration
Scan(iter) // scan items in ascending order
Reverse(iter) // scan items in descending order
Ascend(key, iter) // scan items in ascending order that are >= to key
Descend(key, iter) // scan items in descending order that are <= to key.
Iter() // returns a read-only iterator for for-loops.
// Array-like operations
GetAt(index) // returns the item at index
DeleteAt(index) // deletes the item at index
// Bulk-loading
Load(item) // load presorted items into tree
// Path hinting
SetHint(item, *hint) // insert or replace an existing item
GetHint(item, *hint) // get an existing item
DeleteHint(item, *hint) // delete an item
// Copy-on-write
Copy() // copy the btree
```
#### Example
```go
package main
import (
"fmt"
"github.com/tidwall/btree"
)
type Item struct {
Key, Val string
}
// byKeys is a comparison function that compares item keys and returns true
// when a is less than b.
func byKeys(a, b interface{}) bool {
i1, i2 := a.(*Item), b.(*Item)
return i1.Key < i2.Key
}
// byVals is a comparison function that compares item values and returns true
// when a is less than b.
func byVals(a, b interface{}) bool {
i1, i2 := a.(*Item), b.(*Item)
if i1.Val < i2.Val {
return true
}
if i1.Val > i2.Val {
return false
}
// Both vals are equal so we should fall though
// and let the key comparison take over.
return byKeys(a, b)
}
func main() {
// Create a tree for keys and a tree for values.
// The "keys" tree will be sorted on the Keys field.
// The "values" tree will be sorted on the Values field.
keys := btree.New(byKeys)
vals := btree.New(byVals)
// Create some items.
users := []*Item{
@@ -69,12 +402,12 @@ func main() {
// Insert each user into both trees
for _, user := range users {
keys.ReplaceOrInsert(user)
vals.ReplaceOrInsert(user)
keys.Set(user)
vals.Set(user)
}
// Iterate over each user in the key tree
keys.Ascend(func(item btree.Item) bool {
keys.Ascend(nil, func(item interface{}) bool {
kvi := item.(*Item)
fmt.Printf("%s %s\n", kvi.Key, kvi.Val)
return true
@@ -82,26 +415,37 @@ func main() {
fmt.Printf("\n")
// Iterate over each user in the val tree
vals.Ascend(func(item btree.Item) bool {
vals.Ascend(nil, func(item interface{}) bool {
kvi := item.(*Item)
fmt.Printf("%s %s\n", kvi.Key, kvi.Val)
return true
})
// Output:
// user:1 Jane
// user:2 Andy
// user:3 Steve
// user:4 Andrea
// user:5 Janet
// user:6 Andy
//
// user:4 Andrea
// user:2 Andy
// user:6 Andy
// user:1 Jane
// user:5 Janet
// user:3 Steve
}
// Should see the results
/*
user:1 Jane
user:2 Andy
user:3 Steve
user:4 Andrea
user:5 Janet
user:6 Andy
user:4 Andrea
user:2 Andy
user:6 Andy
user:1 Jane
user:3 Steve
*/
```
## Performance
See [tidwall/btree-benchmark](https://github.com/tidwall/btree-benchmark) for benchmark numbers.
## Contact
Josh Baker [@tidwall](http://twitter.com/tidwall)
## License
Source code is available under the MIT [License](/LICENSE).
+221 -926
View File
File diff suppressed because it is too large Load Diff
+1303
View File
File diff suppressed because it is too large Load Diff
+1056
View File
File diff suppressed because it is too large Load Diff
+168
View File
@@ -0,0 +1,168 @@
package btree
type Set[K ordered] struct {
base Map[K, struct{}]
}
// Copy
func (tr *Set[K]) Copy() *Set[K] {
tr2 := new(Set[K])
tr2.base = *tr.base.Copy()
return tr2
}
// Insert an item
func (tr *Set[K]) Insert(key K) {
tr.base.Set(key, struct{}{})
}
func (tr *Set[K]) Scan(iter func(key K) bool) {
tr.base.Scan(func(key K, value struct{}) bool {
return iter(key)
})
}
// Get a value for key
func (tr *Set[K]) Contains(key K) bool {
_, ok := tr.base.Get(key)
return ok
}
// Len returns the number of items in the tree
func (tr *Set[K]) Len() int {
return tr.base.Len()
}
// Delete an item
func (tr *Set[K]) Delete(key K) {
tr.base.Delete(key)
}
// Ascend the tree within the range [pivot, last]
// Pass nil for pivot to scan all item in ascending order
// Return false to stop iterating
func (tr *Set[K]) Ascend(pivot K, iter func(key K) bool) {
tr.base.Ascend(pivot, func(key K, value struct{}) bool {
return iter(key)
})
}
func (tr *Set[K]) Reverse(iter func(key K) bool) {
tr.base.Reverse(func(key K, value struct{}) bool {
return iter(key)
})
}
// Descend the tree within the range [pivot, first]
// Pass nil for pivot to scan all item in descending order
// Return false to stop iterating
func (tr *Set[K]) Descend(pivot K, iter func(key K) bool) {
tr.base.Descend(pivot, func(key K, value struct{}) bool {
return iter(key)
})
}
// Load is for bulk loading pre-sorted items
func (tr *Set[K]) Load(key K) {
tr.base.Load(key, struct{}{})
}
// Min returns the minimum item in tree.
// Returns nil if the treex has no items.
func (tr *Set[K]) Min() (K, bool) {
key, _, ok := tr.base.Min()
return key, ok
}
// Max returns the maximum item in tree.
// Returns nil if the tree has no items.
func (tr *Set[K]) Max() (K, bool) {
key, _, ok := tr.base.Max()
return key, ok
}
// PopMin removes the minimum item in tree and returns it.
// Returns nil if the tree has no items.
func (tr *Set[K]) PopMin() (K, bool) {
key, _, ok := tr.base.PopMin()
return key, ok
}
// PopMax removes the maximum item in tree and returns it.
// Returns nil if the tree has no items.
func (tr *Set[K]) PopMax() (K, bool) {
key, _, ok := tr.base.PopMax()
return key, ok
}
// GetAt returns the value at index.
// Return nil if the tree is empty or the index is out of bounds.
func (tr *Set[K]) GetAt(index int) (K, bool) {
key, _, ok := tr.base.GetAt(index)
return key, ok
}
// DeleteAt deletes the item at index.
// Return nil if the tree is empty or the index is out of bounds.
func (tr *Set[K]) DeleteAt(index int) (K, bool) {
key, _, ok := tr.base.DeleteAt(index)
return key, ok
}
// Height returns the height of the tree.
// Returns zero if tree has no items.
func (tr *Set[K]) Height() int {
return tr.base.Height()
}
// SetIter represents an iterator for btree.Set
type SetIter[K ordered] struct {
base MapIter[K, struct{}]
}
// Iter returns a read-only iterator.
func (tr *Set[K]) Iter() SetIter[K] {
return SetIter[K]{tr.base.Iter()}
}
// Seek to item greater-or-equal-to key.
// Returns false if there was no item found.
func (iter *SetIter[K]) Seek(key K) bool {
return iter.base.Seek(key)
}
// First moves iterator to first item in tree.
// Returns false if the tree is empty.
func (iter *SetIter[K]) First() bool {
return iter.base.First()
}
// Last moves iterator to last item in tree.
// Returns false if the tree is empty.
func (iter *SetIter[K]) Last() bool {
return iter.base.Last()
}
// Next moves iterator to the next item in iterator.
// Returns false if the tree is empty or the iterator is at the end of
// the tree.
func (iter *SetIter[K]) Next() bool {
return iter.base.Next()
}
// Prev moves iterator to the previous item in iterator.
// Returns false if the tree is empty or the iterator is at the beginning of
// the tree.
func (iter *SetIter[K]) Prev() bool {
return iter.base.Prev()
}
// Key returns the current iterator item key.
func (iter *SetIter[K]) Key() K {
return iter.base.Key()
}
// Keys returns all the keys in order.
func (tr *Set[K]) Keys() []K {
return tr.base.Keys()
}
-1
View File
@@ -1 +0,0 @@
language: go
+7 -8
View File
@@ -3,10 +3,8 @@
src="logo.png"
width="307" height="150" border="0" alt="BuntDB">
<br>
<a href="https://travis-ci.org/tidwall/buntdb"><img src="https://img.shields.io/travis/tidwall/buntdb.svg?style=flat-square" alt="Build Status"></a>
<a href="http://gocover.io/github.com/tidwall/buntdb"><img src="https://img.shields.io/badge/coverage-95%25-brightgreen.svg?style=flat-square" alt="Code Coverage"></a>
<a href="https://goreportcard.com/report/github.com/tidwall/buntdb"><img src="https://goreportcard.com/badge/github.com/tidwall/buntdb?style=flat-square" alt="Go Report Card"></a>
<a href="https://godoc.org/github.com/tidwall/buntdb"><img src="https://img.shields.io/badge/api-reference-blue.svg?style=flat-square" alt="GoDoc"></a>
<a href="https://godoc.org/github.com/tidwall/buntdb"><img src="https://img.shields.io/badge/go-documentation-blue.svg?style=flat-square" alt="Godoc"></a>
<a href="https://github.com/tidwall/buntdb/blob/master/LICENSE"><img src="https://img.shields.io/github/license/tidwall/buntdb.svg?style=flat-square" alt="LICENSE"></a>
</p>
BuntDB is a low-level, in-memory, key/value store in pure Go.
@@ -29,7 +27,6 @@ Features
- Flexible [iteration](#iterating) of data; ascending, descending, and ranges
- [Durable append-only file](#append-only-file) format for persistence
- Option to evict old items with an [expiration](#data-expiration) TTL
- Tight codebase, under 2K loc using the `cloc` command
- ACID semantics with locking [transactions](#transactions) that support rollbacks
@@ -140,6 +137,7 @@ All keys/value pairs are ordered in the database by the key. To iterate over the
err := db.View(func(tx *buntdb.Tx) error {
err := tx.Ascend("", func(key, value string) bool {
fmt.Printf("key: %s, value: %s\n", key, value)
return true // continue iteration
})
return err
})
@@ -458,8 +456,9 @@ Any index can be put in descending order by wrapping it's less function with `bu
```go
db.CreateIndex("last_name_age", "*",
buntdb.IndexJSON("name.last"),
buntdb.Desc(buntdb.IndexJSON("age")))
buntdb.IndexJSON("name.last"),
buntdb.Desc(buntdb.IndexJSON("age")),
)
```
This will create a multi value index where the last name is ascending and the age is descending.
@@ -576,7 +575,7 @@ var config buntdb.Config
if err := db.ReadConfig(&config); err != nil{
log.Fatal(err)
}
if err := db.WriteConfig(config); err != nil{
if err := db.SetConfig(config); err != nil{
log.Fatal(err)
}
```
+338 -111
View File
@@ -1,14 +1,16 @@
// Package buntdb implements a low-level in-memory key/value store in pure Go.
// It persists to disk, is ACID compliant, and uses locking for multiple
// readers and a single writer. Bunt is ideal for projects that need
// a dependable database, and favor speed over data size.
// readers and a single writer. Bunt is ideal for projects that need a
// dependable database, and favor speed over data size.
package buntdb
import (
"bufio"
"errors"
"fmt"
"io"
"os"
"runtime"
"sort"
"strconv"
"strings"
@@ -19,7 +21,7 @@ import (
"github.com/tidwall/gjson"
"github.com/tidwall/grect"
"github.com/tidwall/match"
"github.com/tidwall/rtree"
"github.com/tidwall/rtred"
)
var (
@@ -60,6 +62,8 @@ var (
ErrTxIterating = errors.New("tx is iterating")
)
const useAbsEx = true
// DB represents a collection of key-value pairs that persist on disk.
// Transactions are used for all forms of data access to the DB.
type DB struct {
@@ -69,7 +73,7 @@ type DB struct {
keys *btree.BTree // a tree of all item ordered by key
exps *btree.BTree // a tree of items ordered by expiration
idxs map[string]*index // the index trees.
exmgr bool // indicates that expires manager is running.
insIdxs []*index // a reuse buffer for gathering indexes
flushes int // a count of the number of disk flushes
closed bool // set when the database has been closed
config Config // the database configuration
@@ -122,10 +126,11 @@ type Config struct {
// has been expired.
OnExpired func(keys []string)
// OnExpiredSync will be called inside the same transaction that is performing
// the deletion of expired items. If OnExpired is present then this callback
// will not be called. If this callback is present, then the deletion of the
// timeed-out item is the explicit responsibility of this callback.
// OnExpiredSync will be called inside the same transaction that is
// performing the deletion of expired items. If OnExpired is present then
// this callback will not be called. If this callback is present, then the
// deletion of the timeed-out item is the explicit responsibility of this
// callback.
OnExpiredSync func(key, value string, tx *Tx) error
}
@@ -134,16 +139,13 @@ type exctx struct {
db *DB
}
// Default number of btree degrees
const btreeDegrees = 64
// Open opens a database at the provided path.
// If the file does not exist then it will be created automatically.
func Open(path string) (*DB, error) {
db := &DB{}
// initialize trees and indexes
db.keys = btree.New(btreeDegrees, nil)
db.exps = btree.New(btreeDegrees, &exctx{db})
db.keys = btreeNew(lessCtx(nil))
db.exps = btreeNew(lessCtx(&exctx{db}))
db.idxs = make(map[string]*index)
// initialize default configuration
db.config = Config{
@@ -203,10 +205,11 @@ func (db *DB) Save(wr io.Writer) error {
defer db.mu.RUnlock()
// use a buffered writer and flush every 4MB
var buf []byte
now := time.Now()
// iterated through every item in the database and write to the buffer
db.keys.Ascend(func(item btree.Item) bool {
btreeAscend(db.keys, func(item interface{}) bool {
dbi := item.(*dbItem)
buf = dbi.writeSetTo(buf)
buf = dbi.writeSetTo(buf, now)
if len(buf) > 1024*1024*4 {
// flush when buffer is over 4MB
_, err = wr.Write(buf)
@@ -240,14 +243,15 @@ func (db *DB) Load(rd io.Reader) error {
// cannot load into databases that persist to disk
return ErrPersistenceActive
}
return db.readLoad(rd, time.Now())
_, err := db.readLoad(rd, time.Now())
return err
}
// index represents a b-tree or r-tree index and also acts as the
// b-tree/r-tree context for itself.
type index struct {
btr *btree.BTree // contains the items
rtr *rtree.RTree // contains the items
rtr *rtred.RTree // contains the items
name string // name of the index
pattern string // a required key pattern
less func(a, b string) bool // less comparison function
@@ -285,10 +289,10 @@ func (idx *index) clearCopy() *index {
}
// initialize with empty trees
if nidx.less != nil {
nidx.btr = btree.New(btreeDegrees, nidx)
nidx.btr = btreeNew(lessCtx(nidx))
}
if nidx.rect != nil {
nidx.rtr = rtree.New(nidx)
nidx.rtr = rtred.New(nidx)
}
return nidx
}
@@ -297,20 +301,20 @@ func (idx *index) clearCopy() *index {
func (idx *index) rebuild() {
// initialize trees
if idx.less != nil {
idx.btr = btree.New(btreeDegrees, idx)
idx.btr = btreeNew(lessCtx(idx))
}
if idx.rect != nil {
idx.rtr = rtree.New(idx)
idx.rtr = rtred.New(idx)
}
// iterate through all keys and fill the index
idx.db.keys.Ascend(func(item btree.Item) bool {
btreeAscend(idx.db.keys, func(item interface{}) bool {
dbi := item.(*dbItem)
if !idx.match(dbi.key) {
// does not match the pattern, conintue
// does not match the pattern, continue
return true
}
if idx.less != nil {
idx.btr.ReplaceOrInsert(dbi)
idx.btr.Set(dbi)
}
if idx.rect != nil {
idx.rtr.Insert(dbi)
@@ -456,16 +460,23 @@ func (db *DB) SetConfig(config Config) error {
// will be replaced with the new one, and return the previous item.
func (db *DB) insertIntoDatabase(item *dbItem) *dbItem {
var pdbi *dbItem
prev := db.keys.ReplaceOrInsert(item)
// Generate a list of indexes that this item will be inserted in to.
idxs := db.insIdxs
for _, idx := range db.idxs {
if idx.match(item.key) {
idxs = append(idxs, idx)
}
}
prev := db.keys.Set(item)
if prev != nil {
// A previous item was removed from the keys tree. Let's
// fully delete this item from all indexes.
pdbi = prev.(*dbItem)
if pdbi.opts != nil && pdbi.opts.ex {
// Remove it from the exipres tree.
// Remove it from the expires tree.
db.exps.Delete(pdbi)
}
for _, idx := range db.idxs {
for _, idx := range idxs {
if idx.btr != nil {
// Remove it from the btree index.
idx.btr.Delete(pdbi)
@@ -479,21 +490,22 @@ func (db *DB) insertIntoDatabase(item *dbItem) *dbItem {
if item.opts != nil && item.opts.ex {
// The new item has eviction options. Add it to the
// expires tree
db.exps.ReplaceOrInsert(item)
db.exps.Set(item)
}
for _, idx := range db.idxs {
if !idx.match(item.key) {
continue
}
for i, idx := range idxs {
if idx.btr != nil {
// Add new item to btree index.
idx.btr.ReplaceOrInsert(item)
idx.btr.Set(item)
}
if idx.rtr != nil {
// Add new item to rtree index.
idx.rtr.Insert(item)
}
// clear the index
idxs[i] = nil
}
// reuse the index list slice
db.insIdxs = idxs[:0]
// we must return the previous item to the caller.
return pdbi
}
@@ -514,6 +526,9 @@ func (db *DB) deleteFromDatabase(item *dbItem) *dbItem {
db.exps.Delete(pdbi)
}
for _, idx := range db.idxs {
if !idx.match(pdbi.key) {
continue
}
if idx.btr != nil {
// Remove it from the btree index.
idx.btr.Delete(pdbi)
@@ -557,9 +572,9 @@ func (db *DB) backgroundManager() {
}
}
// produce a list of expired items that need removing
db.exps.AscendLessThan(&dbItem{
btreeAscendLessThan(db.exps, &dbItem{
opts: &dbItemOpts{ex: true, exat: time.Now()},
}, func(item btree.Item) bool {
}, func(item interface{}) bool {
expired = append(expired, item.(*dbItem))
return true
})
@@ -674,8 +689,9 @@ func (db *DB) Shrink() error {
}
done = true
var n int
db.keys.AscendGreaterOrEqual(&dbItem{key: pivot},
func(item btree.Item) bool {
now := time.Now()
btreeAscendGreaterOrEqual(db.keys, &dbItem{key: pivot},
func(item interface{}) bool {
dbi := item.(*dbItem)
// 1000 items or 64MB buffer
if n > 1000 || len(buf) > 64*1024*1024 {
@@ -683,7 +699,7 @@ func (db *DB) Shrink() error {
done = false
return false
}
buf = dbi.writeSetTo(buf)
buf = dbi.writeSetTo(buf, now)
n++
return true
},
@@ -737,13 +753,13 @@ func (db *DB) Shrink() error {
if err := db.file.Close(); err != nil {
return err
}
// Any failures below here is really bad. So just panic.
if err := os.Rename(tmpname, fname); err != nil {
panic(err)
// Any failures below here are really bad. So just panic.
if err := renameFile(tmpname, fname); err != nil {
panicErr(err)
}
db.file, err = os.OpenFile(fname, os.O_CREATE|os.O_RDWR, 0666)
if err != nil {
panic(err)
panicErr(err)
}
pos, err := db.file.Seek(0, 2)
if err != nil {
@@ -754,46 +770,81 @@ func (db *DB) Shrink() error {
}()
}
var errValidEOF = errors.New("valid eof")
func panicErr(err error) error {
panic(fmt.Errorf("buntdb: %w", err))
}
func renameFile(src, dest string) error {
var err error
if err = os.Rename(src, dest); err != nil {
if runtime.GOOS == "windows" {
if err = os.Remove(dest); err == nil {
err = os.Rename(src, dest)
}
}
}
return err
}
// readLoad reads from the reader and loads commands into the database.
// modTime is the modified time of the reader, should be no greater than
// the current time.Now().
func (db *DB) readLoad(rd io.Reader, modTime time.Time) error {
// Returns the number of bytes of the last command read and the error if any.
func (db *DB) readLoad(rd io.Reader, modTime time.Time) (n int64, err error) {
defer func() {
if err == io.EOF {
err = io.ErrUnexpectedEOF
}
}()
totalSize := int64(0)
data := make([]byte, 4096)
parts := make([]string, 0, 8)
r := bufio.NewReader(rd)
for {
// peek at the first byte. If it's a 'nul' control character then
// ignore it and move to the next byte.
c, err := r.ReadByte()
if err != nil {
if err == io.EOF {
err = nil
}
return totalSize, err
}
if c == 0 {
// ignore nul control characters
n += 1
continue
}
if err := r.UnreadByte(); err != nil {
return totalSize, err
}
// read a single command.
// first we should read the number of parts that the of the command
cmdByteSize := int64(0)
line, err := r.ReadBytes('\n')
if err != nil {
if len(line) > 0 {
// got an eof but also data. this should be an unexpected eof.
return io.ErrUnexpectedEOF
}
if err == io.EOF {
break
}
return err
return totalSize, err
}
if line[0] != '*' {
return ErrInvalid
return totalSize, ErrInvalid
}
cmdByteSize += int64(len(line))
// convert the string number to and int
var n int
if len(line) == 4 && line[len(line)-2] == '\r' {
if line[1] < '0' || line[1] > '9' {
return ErrInvalid
return totalSize, ErrInvalid
}
n = int(line[1] - '0')
} else {
if len(line) < 5 || line[len(line)-2] != '\r' {
return ErrInvalid
return totalSize, ErrInvalid
}
for i := 1; i < len(line)-2; i++ {
if line[i] < '0' || line[i] > '9' {
return ErrInvalid
return totalSize, ErrInvalid
}
n = n*10 + int(line[i]-'0')
}
@@ -804,25 +855,26 @@ func (db *DB) readLoad(rd io.Reader, modTime time.Time) error {
// read the number of bytes of the part.
line, err := r.ReadBytes('\n')
if err != nil {
return err
return totalSize, err
}
if line[0] != '$' {
return ErrInvalid
return totalSize, ErrInvalid
}
cmdByteSize += int64(len(line))
// convert the string number to and int
var n int
if len(line) == 4 && line[len(line)-2] == '\r' {
if line[1] < '0' || line[1] > '9' {
return ErrInvalid
return totalSize, ErrInvalid
}
n = int(line[1] - '0')
} else {
if len(line) < 5 || line[len(line)-2] != '\r' {
return ErrInvalid
return totalSize, ErrInvalid
}
for i := 1; i < len(line)-2; i++ {
if line[i] < '0' || line[i] > '9' {
return ErrInvalid
return totalSize, ErrInvalid
}
n = n*10 + int(line[i]-'0')
}
@@ -836,67 +888,79 @@ func (db *DB) readLoad(rd io.Reader, modTime time.Time) error {
data = make([]byte, dataln)
}
if _, err = io.ReadFull(r, data[:n+2]); err != nil {
return err
return totalSize, err
}
if data[n] != '\r' || data[n+1] != '\n' {
return ErrInvalid
return totalSize, ErrInvalid
}
// copy string
parts = append(parts, string(data[:n]))
cmdByteSize += int64(n + 2)
}
// finished reading the command
if len(parts) == 0 {
continue
}
if (parts[0][0] == 's' || parts[0][1] == 'S') &&
if (parts[0][0] == 's' || parts[0][0] == 'S') &&
(parts[0][1] == 'e' || parts[0][1] == 'E') &&
(parts[0][2] == 't' || parts[0][2] == 'T') {
// SET
if len(parts) < 3 || len(parts) == 4 || len(parts) > 5 {
return ErrInvalid
return totalSize, ErrInvalid
}
if len(parts) == 5 {
if strings.ToLower(parts[3]) != "ex" {
return ErrInvalid
arg := strings.ToLower(parts[3])
if arg != "ex" && arg != "ae" {
return totalSize, ErrInvalid
}
ex, err := strconv.ParseInt(parts[4], 10, 64)
if err != nil {
return err
return totalSize, err
}
var exat time.Time
now := time.Now()
dur := (time.Duration(ex) * time.Second) - now.Sub(modTime)
if dur > 0 {
if arg == "ex" {
dur := (time.Duration(ex) * time.Second) - now.Sub(modTime)
exat = now.Add(dur)
} else {
exat = time.Unix(ex, 0)
}
if exat.After(now) {
db.insertIntoDatabase(&dbItem{
key: parts[1],
val: parts[2],
opts: &dbItemOpts{
ex: true,
exat: now.Add(dur),
exat: exat,
},
})
} else {
db.deleteFromDatabase(&dbItem{
key: parts[1],
})
}
} else {
db.insertIntoDatabase(&dbItem{key: parts[1], val: parts[2]})
}
} else if (parts[0][0] == 'd' || parts[0][1] == 'D') &&
} else if (parts[0][0] == 'd' || parts[0][0] == 'D') &&
(parts[0][1] == 'e' || parts[0][1] == 'E') &&
(parts[0][2] == 'l' || parts[0][2] == 'L') {
// DEL
if len(parts) != 2 {
return ErrInvalid
return totalSize, ErrInvalid
}
db.deleteFromDatabase(&dbItem{key: parts[1]})
} else if (parts[0][0] == 'f' || parts[0][1] == 'F') &&
} else if (parts[0][0] == 'f' || parts[0][0] == 'F') &&
strings.ToLower(parts[0]) == "flushdb" {
db.keys = btree.New(btreeDegrees, nil)
db.exps = btree.New(btreeDegrees, &exctx{db})
db.keys = btreeNew(lessCtx(nil))
db.exps = btreeNew(lessCtx(&exctx{db}))
db.idxs = make(map[string]*index)
} else {
return ErrInvalid
return totalSize, ErrInvalid
}
totalSize += cmdByteSize
}
return nil
}
// load reads entries from the append only database file and fills the database.
@@ -909,14 +973,29 @@ func (db *DB) load() error {
if err != nil {
return err
}
if err := db.readLoad(db.file, fi.ModTime()); err != nil {
return err
}
pos, err := db.file.Seek(0, 2)
n, err := db.readLoad(db.file, fi.ModTime())
if err != nil {
if err == io.ErrUnexpectedEOF {
// The db file has ended mid-command, which is allowed but the
// data file should be truncated to the end of the last valid
// command
if err := db.file.Truncate(n); err != nil {
return err
}
} else {
return err
}
}
if _, err := db.file.Seek(n, 0); err != nil {
return err
}
db.lastaofsz = int(pos)
var estaofsz int
db.keys.Walk(func(items []interface{}) {
for _, v := range items {
estaofsz += v.(*dbItem).estAOFSetSize()
}
})
db.lastaofsz += estaofsz
return nil
}
@@ -1025,8 +1104,8 @@ func (tx *Tx) DeleteAll() error {
}
// now reset the live database trees
tx.db.keys = btree.New(btreeDegrees, nil)
tx.db.exps = btree.New(btreeDegrees, &exctx{tx.db})
tx.db.keys = btreeNew(lessCtx(nil))
tx.db.exps = btreeNew(lessCtx(&exctx{tx.db}))
tx.db.idxs = make(map[string]*index)
// finally re-create the indexes
@@ -1136,18 +1215,37 @@ func (tx *Tx) Commit() error {
if tx.wc.rbkeys != nil {
tx.db.buf = append(tx.db.buf, "*1\r\n$7\r\nflushdb\r\n"...)
}
now := time.Now()
// Each committed record is written to disk
for key, item := range tx.wc.commitItems {
if item == nil {
tx.db.buf = (&dbItem{key: key}).writeDeleteTo(tx.db.buf)
} else {
tx.db.buf = item.writeSetTo(tx.db.buf)
tx.db.buf = item.writeSetTo(tx.db.buf, now)
}
}
// Flushing the buffer only once per transaction.
// If this operation fails then the write did failed and we must
// rollback.
if _, err = tx.db.file.Write(tx.db.buf); err != nil {
var n int
n, err = tx.db.file.Write(tx.db.buf)
if err != nil {
if n > 0 {
// There was a partial write to disk.
// We are possibly out of disk space.
// Delete the partially written bytes from the data file by
// seeking to the previously known position and performing
// a truncate operation.
// At this point a syscall failure is fatal and the process
// should be killed to avoid corrupting the file.
pos, err := tx.db.file.Seek(-int64(n), 1)
if err != nil {
panicErr(err)
}
if err := tx.db.file.Truncate(pos); err != nil {
panicErr(err)
}
}
tx.rollbackInner()
}
if tx.db.config.SyncPolicy == Always {
@@ -1196,16 +1294,59 @@ type dbItem struct {
keyless bool // keyless item for scanning
}
// estIntSize returns the string representions size.
// Has the same result as len(strconv.Itoa(x)).
func estIntSize(x int) int {
n := 1
if x < 0 {
n++
x *= -1
}
for x >= 10 {
n++
x /= 10
}
return n
}
func estArraySize(count int) int {
return 1 + estIntSize(count) + 2
}
func estBulkStringSize(s string) int {
return 1 + estIntSize(len(s)) + 2 + len(s) + 2
}
// estAOFSetSize returns an estimated number of bytes that this item will use
// when stored in the aof file.
func (dbi *dbItem) estAOFSetSize() int {
var n int
if dbi.opts != nil && dbi.opts.ex {
n += estArraySize(5)
n += estBulkStringSize("set")
n += estBulkStringSize(dbi.key)
n += estBulkStringSize(dbi.val)
n += estBulkStringSize("ex")
n += estBulkStringSize("99") // estimate two byte bulk string
} else {
n += estArraySize(3)
n += estBulkStringSize("set")
n += estBulkStringSize(dbi.key)
n += estBulkStringSize(dbi.val)
}
return n
}
func appendArray(buf []byte, count int) []byte {
buf = append(buf, '*')
buf = append(buf, strconv.FormatInt(int64(count), 10)...)
buf = strconv.AppendInt(buf, int64(count), 10)
buf = append(buf, '\r', '\n')
return buf
}
func appendBulkString(buf []byte, s string) []byte {
buf = append(buf, '$')
buf = append(buf, strconv.FormatInt(int64(len(s)), 10)...)
buf = strconv.AppendInt(buf, int64(len(s)), 10)
buf = append(buf, '\r', '\n')
buf = append(buf, s...)
buf = append(buf, '\r', '\n')
@@ -1213,15 +1354,21 @@ func appendBulkString(buf []byte, s string) []byte {
}
// writeSetTo writes an item as a single SET record to the a bufio Writer.
func (dbi *dbItem) writeSetTo(buf []byte) []byte {
func (dbi *dbItem) writeSetTo(buf []byte, now time.Time) []byte {
if dbi.opts != nil && dbi.opts.ex {
ex := dbi.opts.exat.Sub(time.Now()) / time.Second
buf = appendArray(buf, 5)
buf = appendBulkString(buf, "set")
buf = appendBulkString(buf, dbi.key)
buf = appendBulkString(buf, dbi.val)
buf = appendBulkString(buf, "ex")
buf = appendBulkString(buf, strconv.FormatUint(uint64(ex), 10))
if useAbsEx {
ex := dbi.opts.exat.Unix()
buf = appendBulkString(buf, "ae")
buf = appendBulkString(buf, strconv.FormatUint(uint64(ex), 10))
} else {
ex := dbi.opts.exat.Sub(now) / time.Second
buf = appendBulkString(buf, "ex")
buf = appendBulkString(buf, strconv.FormatUint(uint64(ex), 10))
}
} else {
buf = appendArray(buf, 3)
buf = appendBulkString(buf, "set")
@@ -1264,8 +1411,7 @@ func (dbi *dbItem) expiresAt() time.Time {
// to note that the ctx parameter is used to help with determine which
// formula to use on an item. Each b-tree should use a different ctx when
// sharing the same item.
func (dbi *dbItem) Less(item btree.Item, ctx interface{}) bool {
dbi2 := item.(*dbItem)
func (dbi *dbItem) Less(dbi2 *dbItem, ctx interface{}) bool {
switch ctx := ctx.(type) {
case *exctx:
// The expires b-tree formula
@@ -1295,6 +1441,12 @@ func (dbi *dbItem) Less(item btree.Item, ctx interface{}) bool {
return dbi.key < dbi2.key
}
func lessCtx(ctx interface{}) func(a, b interface{}) bool {
return func(a, b interface{}) bool {
return a.(*dbItem).Less(b.(*dbItem), ctx)
}
}
// Rect converts a string to a rectangle.
// An invalid rectangle will cause a panic.
func (dbi *dbItem) Rect(ctx interface{}) (min, max []float64) {
@@ -1384,7 +1536,9 @@ func (tx *Tx) Set(key, value string, opts *SetOptions) (previousValue string,
// create a rollback entry with a nil value. A nil value indicates
// that the entry should be deleted on rollback. When the value is
// *not* nil, that means the entry should be reverted.
tx.wc.rollbackItems[key] = nil
if _, ok := tx.wc.rollbackItems[key]; !ok {
tx.wc.rollbackItems[key] = nil
}
} else {
// A previous item already exists in the database. Let's create a
// rollback entry with the item as the value. We need to check the
@@ -1475,7 +1629,7 @@ func (tx *Tx) TTL(key string) (time.Duration, error) {
} else if item.opts == nil || !item.opts.ex {
return -1, nil
}
dur := item.opts.exat.Sub(time.Now())
dur := time.Until(item.opts.exat)
if dur < 0 {
return 0, ErrNotFound
}
@@ -1498,8 +1652,11 @@ func (tx *Tx) scan(desc, gt, lt bool, index, start, stop string,
return ErrTxClosed
}
// wrap a btree specific iterator around the user-defined iterator.
iter := func(item btree.Item) bool {
iter := func(item interface{}) bool {
dbi := item.(*dbItem)
if dbi.expired() {
return true
}
return iterator(dbi.key, dbi.val)
}
var tr *btree.BTree
@@ -1542,26 +1699,26 @@ func (tx *Tx) scan(desc, gt, lt bool, index, start, stop string,
if desc {
if gt {
if lt {
tr.DescendRange(itemA, itemB, iter)
btreeDescendRange(tr, itemA, itemB, iter)
} else {
tr.DescendGreaterThan(itemA, iter)
btreeDescendGreaterThan(tr, itemA, iter)
}
} else if lt {
tr.DescendLessOrEqual(itemA, iter)
btreeDescendLessOrEqual(tr, itemA, iter)
} else {
tr.Descend(iter)
btreeDescend(tr, iter)
}
} else {
if gt {
if lt {
tr.AscendRange(itemA, itemB, iter)
btreeAscendRange(tr, itemA, itemB, iter)
} else {
tr.AscendGreaterOrEqual(itemA, iter)
btreeAscendGreaterOrEqual(tr, itemA, iter)
}
} else if lt {
tr.AscendLessThan(itemA, iter)
btreeAscendLessThan(tr, itemA, iter)
} else {
tr.Ascend(iter)
btreeAscend(tr, iter)
}
}
return nil
@@ -1816,7 +1973,7 @@ func (tx *Tx) Nearby(index, bounds string,
return nil
}
// // wrap a rtree specific iterator around the user-defined iterator.
iter := func(item rtree.Item, dist float64) bool {
iter := func(item rtred.Item, dist float64) bool {
dbi := item.(*dbItem)
return iterator(dbi.key, dbi.val, dist)
}
@@ -1854,7 +2011,7 @@ func (tx *Tx) Intersects(index, bounds string,
return nil
}
// wrap a rtree specific iterator around the user-defined iterator.
iter := func(item rtree.Item) bool {
iter := func(item rtred.Item) bool {
dbi := item.(*dbItem)
return iterator(dbi.key, dbi.val)
}
@@ -2014,7 +2171,8 @@ func (tx *Tx) createIndex(name string, pattern string,
if tx.wc.rbkeys == nil {
// store the index in the rollback map.
if _, ok := tx.wc.rollbackIndexes[name]; !ok {
// we use nil to indicate that the index should be removed upon rollback.
// we use nil to indicate that the index should be removed upon
// rollback.
tx.wc.rollbackIndexes[name] = nil
}
}
@@ -2044,8 +2202,8 @@ func (tx *Tx) DropIndex(name string) error {
if tx.wc.rbkeys == nil {
// store the index in the rollback map.
if _, ok := tx.wc.rollbackIndexes[name]; !ok {
// we use a non-nil copy of the index without the data to indicate that the
// index should be rebuilt upon rollback.
// we use a non-nil copy of the index without the data to indicate
// that the index should be rebuilt upon rollback.
tx.wc.rollbackIndexes[name] = idx.clearCopy()
}
}
@@ -2181,3 +2339,72 @@ func IndexJSONCaseSensitive(path string) func(a, b string) bool {
func Desc(less func(a, b string) bool) func(a, b string) bool {
return func(a, b string) bool { return less(b, a) }
}
//// Wrappers around btree Ascend/Descend
func bLT(tr *btree.BTree, a, b interface{}) bool { return tr.Less(a, b) }
func bGT(tr *btree.BTree, a, b interface{}) bool { return tr.Less(b, a) }
// func bLTE(tr *btree.BTree, a, b interface{}) bool { return !tr.Less(b, a) }
// func bGTE(tr *btree.BTree, a, b interface{}) bool { return !tr.Less(a, b) }
// Ascend
func btreeAscend(tr *btree.BTree, iter func(item interface{}) bool) {
tr.Ascend(nil, iter)
}
func btreeAscendLessThan(tr *btree.BTree, pivot interface{},
iter func(item interface{}) bool,
) {
tr.Ascend(nil, func(item interface{}) bool {
return bLT(tr, item, pivot) && iter(item)
})
}
func btreeAscendGreaterOrEqual(tr *btree.BTree, pivot interface{},
iter func(item interface{}) bool,
) {
tr.Ascend(pivot, iter)
}
func btreeAscendRange(tr *btree.BTree, greaterOrEqual, lessThan interface{},
iter func(item interface{}) bool,
) {
tr.Ascend(greaterOrEqual, func(item interface{}) bool {
return bLT(tr, item, lessThan) && iter(item)
})
}
// Descend
func btreeDescend(tr *btree.BTree, iter func(item interface{}) bool) {
tr.Descend(nil, iter)
}
func btreeDescendGreaterThan(tr *btree.BTree, pivot interface{},
iter func(item interface{}) bool,
) {
tr.Descend(nil, func(item interface{}) bool {
return bGT(tr, item, pivot) && iter(item)
})
}
func btreeDescendRange(tr *btree.BTree, lessOrEqual, greaterThan interface{},
iter func(item interface{}) bool,
) {
tr.Descend(lessOrEqual, func(item interface{}) bool {
return bGT(tr, item, greaterThan) && iter(item)
})
}
func btreeDescendLessOrEqual(tr *btree.BTree, pivot interface{},
iter func(item interface{}) bool,
) {
tr.Descend(pivot, iter)
}
func btreeNew(less func(a, b interface{}) bool) *btree.BTree {
// Using NewNonConcurrent because we're managing our own locks.
return btree.NewNonConcurrent(less)
}
-12
View File
@@ -1,12 +0,0 @@
module github.com/tidwall/buntdb
go 1.13
require (
github.com/tidwall/btree v0.0.0-20191029221954-400434d76274
github.com/tidwall/gjson v1.3.4
github.com/tidwall/grect v0.0.0-20161006141115-ba9a043346eb
github.com/tidwall/match v1.0.1
github.com/tidwall/rtree v0.0.0-20180113144539-6cd427091e0e
github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563 // indirect
)
-14
View File
@@ -1,14 +0,0 @@
github.com/tidwall/btree v0.0.0-20191029221954-400434d76274 h1:G6Z6HvJuPjG6XfNGi/feOATzeJrfgTNJY+rGrHbA04E=
github.com/tidwall/btree v0.0.0-20191029221954-400434d76274/go.mod h1:huei1BkDWJ3/sLXmO+bsCNELL+Bp2Kks9OLyQFkzvA8=
github.com/tidwall/gjson v1.3.4 h1:On5waDnyKKk3SWE4EthbjjirAWXp43xx5cKCUZY1eZw=
github.com/tidwall/gjson v1.3.4/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls=
github.com/tidwall/grect v0.0.0-20161006141115-ba9a043346eb h1:5NSYaAdrnblKByzd7XByQEJVT8+9v0W/tIY0Oo4OwrE=
github.com/tidwall/grect v0.0.0-20161006141115-ba9a043346eb/go.mod h1:lKYYLFIr9OIgdgrtgkZ9zgRxRdvPYsExnYBsEAd8W5M=
github.com/tidwall/match v1.0.1 h1:PnKP62LPNxHKTwvHHZZzdOAOCtsJTjo6dZLCwpKm5xc=
github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E=
github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/tidwall/rtree v0.0.0-20180113144539-6cd427091e0e h1:+NL1GDIUOKxVfbp2KoJQD9cTQ6dyP2co9q4yzmT9FZo=
github.com/tidwall/rtree v0.0.0-20180113144539-6cd427091e0e/go.mod h1:/h+UnNGt0IhNNJLkGikcdcJqm66zGD/uJGMRxK/9+Ao=
github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563 h1:Otn9S136ELckZ3KKDyCkxapfufrqDqwmGjcHfAyXRrE=
github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563/go.mod h1:mLqSmt7Dv/CNneF2wfcChfN1rvapyQr01LGKnKex0DQ=
-1
View File
@@ -1 +0,0 @@
language: go
+38 -32
View File
@@ -3,13 +3,12 @@
src="logo.png"
width="240" height="78" border="0" alt="GJSON">
<br>
<a href="https://travis-ci.org/tidwall/gjson"><img src="https://img.shields.io/travis/tidwall/gjson.svg?style=flat-square" alt="Build Status"></a>
<a href="https://godoc.org/github.com/tidwall/gjson"><img src="https://img.shields.io/badge/api-reference-blue.svg?style=flat-square" alt="GoDoc"></a>
<a href="http://tidwall.com/gjson-play"><img src="https://img.shields.io/badge/%F0%9F%8F%90-playground-9900cc.svg?style=flat-square" alt="GJSON Playground"></a>
<a href="https://tidwall.com/gjson-play"><img src="https://img.shields.io/badge/%F0%9F%8F%90-playground-9900cc.svg?style=flat-square" alt="GJSON Playground"></a>
<a href="SYNTAX.md"><img src="https://img.shields.io/badge/{}-syntax-33aa33.svg?style=flat-square" alt="GJSON Syntax"></a>
</p>
<p align="center">get json values quickly</a></p>
GJSON is a Go package that provides a [fast](#performance) and [simple](#get-a-value) way to get values from a json document.
@@ -17,6 +16,10 @@ It has features such as [one line retrieval](#get-a-value), [dot notation paths]
Also check out [SJSON](https://github.com/tidwall/sjson) for modifying json, and the [JJ](https://github.com/tidwall/jj) command line tool.
This README is a quick overview of how to use GJSON, for more information check out [GJSON Syntax](SYNTAX.md).
GJSON is also available for [Python](https://github.com/volans-/gjson-py) and [Rust](https://github.com/tidwall/gjson.rs)
Getting Started
===============
@@ -126,11 +129,12 @@ nil, for JSON null
To directly access the value:
```go
result.Type // can be String, Number, True, False, Null, or JSON
result.Str // holds the string
result.Num // holds the float64 number
result.Raw // holds the raw json
result.Index // index of raw value in original json, zero means index unknown
result.Type // can be String, Number, True, False, Null, or JSON
result.Str // holds the string
result.Num // holds the float64 number
result.Raw // holds the raw json
result.Index // index of raw value in original json, zero means index unknown
result.Indexes // indexes of all the elements that match on a path containing the '#' query character.
```
There are a variety of handy functions that work on a result:
@@ -153,10 +157,6 @@ result.Less(token Result, caseSensitive bool) bool
The `result.Value()` function returns an `interface{}` which requires type assertion and is one of the following Go types:
The `result.Array()` function returns back an array of values.
If the result represents a non-existent value, then an empty array will be returned.
If the result is not a JSON array, the return value will be an array containing one result.
```go
boolean >> bool
number >> float64
@@ -166,6 +166,10 @@ array >> []interface{}
object >> map[string]interface{}
```
The `result.Array()` function returns back an array of values.
If the result represents a non-existent value, then an empty array will be returned.
If the result is not a JSON array, the return value will be an array containing one result.
### 64-bit integers
The `result.Int()` and `result.Uint()` calls are capable of reading all 64 bits, allowing for large JSON integers.
@@ -193,11 +197,20 @@ we'll get `children` array and reverse the order:
"children|@reverse|0" >> "Jack"
```
There are currently three built-in modifiers:
There are currently the following built-in modifiers:
- `@reverse`: Reverse an array or the members of an object.
- `@ugly`: Remove all whitespace from a json document.
- `@pretty`: Make the json document more human readable.
- `@this`: Returns the current element. It can be used to retrieve the root element.
- `@valid`: Ensure the json document is valid.
- `@flatten`: Flattens an array.
- `@join`: Joins multiple objects into a single object.
- `@keys`: Returns an array of keys for an object.
- `@values`: Returns an array of values for an object.
- `@tostr`: Converts json to a string. Wraps a json string.
- `@fromstr`: Converts a string from json. Unwraps a json string.
- `@group`: Groups arrays of objects. See [e4fc67c](https://github.com/tidwall/gjson/commit/e4fc67c92aeebf2089fabc7872f010e340d105db).
### Modifier arguments
@@ -432,14 +445,15 @@ Benchmarks of GJSON alongside [encoding/json](https://golang.org/pkg/encoding/js
and [json-iterator](https://github.com/json-iterator/go)
```
BenchmarkGJSONGet-8 3000000 372 ns/op 0 B/op 0 allocs/op
BenchmarkGJSONUnmarshalMap-8 900000 4154 ns/op 1920 B/op 26 allocs/op
BenchmarkJSONUnmarshalMap-8 600000 9019 ns/op 3048 B/op 69 allocs/op
BenchmarkJSONDecoder-8 300000 14120 ns/op 4224 B/op 184 allocs/op
BenchmarkFFJSONLexer-8 1500000 3111 ns/op 896 B/op 8 allocs/op
BenchmarkEasyJSONLexer-8 3000000 887 ns/op 613 B/op 6 allocs/op
BenchmarkJSONParserGet-8 3000000 499 ns/op 21 B/op 0 allocs/op
BenchmarkJSONIterator-8 3000000 812 ns/op 544 B/op 9 allocs/op
BenchmarkGJSONGet-16 11644512 311 ns/op 0 B/op 0 allocs/op
BenchmarkGJSONUnmarshalMap-16 1122678 3094 ns/op 1920 B/op 26 allocs/op
BenchmarkJSONUnmarshalMap-16 516681 6810 ns/op 2944 B/op 69 allocs/op
BenchmarkJSONUnmarshalStruct-16 697053 5400 ns/op 928 B/op 13 allocs/op
BenchmarkJSONDecoder-16 330450 10217 ns/op 3845 B/op 160 allocs/op
BenchmarkFFJSONLexer-16 1424979 2585 ns/op 880 B/op 8 allocs/op
BenchmarkEasyJSONLexer-16 3000000 729 ns/op 501 B/op 5 allocs/op
BenchmarkJSONParserGet-16 3000000 366 ns/op 21 B/op 0 allocs/op
BenchmarkJSONIterator-16 3000000 869 ns/op 693 B/op 14 allocs/op
```
JSON document used:
@@ -472,7 +486,7 @@ JSON document used:
}
```
Each operation was rotated though one of the following search paths:
Each operation was rotated through one of the following search paths:
```
widget.window.name
@@ -480,12 +494,4 @@ widget.image.hOffset
widget.text.onMouseUp
```
*These benchmarks were run on a MacBook Pro 15" 2.8 GHz Intel Core i7 using Go 1.8 and can be be found [here](https://github.com/tidwall/gjson-benchmarks).*
## Contact
Josh Baker [@tidwall](http://twitter.com/tidwall)
## License
GJSON source code is available under the MIT [License](/LICENSE).
*These benchmarks were run on a MacBook Pro 16" 2.4 GHz Intel Core i9 using Go 1.17 and can be found [here](https://github.com/tidwall/gjson-benchmarks).*
+84 -7
View File
@@ -13,16 +13,16 @@ This document is designed to explain the structure of a GJSON Path through examp
- [Dot vs Pipe](#dot-vs-pipe)
- [Modifiers](#modifiers)
- [Multipaths](#multipaths)
- [Literals](#literals)
The definitive implemenation is [github.com/tidwall/gjson](https://github.com/tidwall/gjson).
Use the [GJSON Playground](https://gjson.dev) to experiment with the syntax online.
## Path structure
A GJSON Path is intended to be easily expressed as a series of components seperated by a `.` character.
Along with `.` character, there are a few more that have special meaning, including `|`, `#`, `@`, `\`, `*`, and `?`.
Along with `.` character, there are a few more that have special meaning, including `|`, `#`, `@`, `\`, `*`, `!`, and `?`.
## Example
@@ -77,6 +77,21 @@ Special purpose characters, such as `.`, `*`, and `?` can be escaped with `\`.
fav\.movie "Deer Hunter"
```
You'll also need to make sure that the `\` character is correctly escaped when hardcoding a path in your source code.
```go
// Go
val := gjson.Get(json, "fav\\.movie") // must escape the slash
val := gjson.Get(json, `fav\.movie`) // no need to escape the slash
```
```rust
// Rust
let val = gjson::get(json, "fav\\.movie") // must escape the slash
let val = gjson::get(json, r#"fav\.movie"#) // no need to escape the slash
```
### Arrays
The `#` character allows for digging into JSON Arrays.
@@ -120,6 +135,37 @@ changed in v1.3.0 as to avoid confusion with the new [multipath](#multipaths)
syntax. For backwards compatibility, `#[...]` will continue to work until the
next major release.*
The `~` (tilde) operator will convert a value to a boolean before comparison.
For example, using the following JSON:
```json
{
"vals": [
{ "a": 1, "b": true },
{ "a": 2, "b": true },
{ "a": 3, "b": false },
{ "a": 4, "b": "0" },
{ "a": 5, "b": 0 },
{ "a": 6, "b": "1" },
{ "a": 7, "b": 1 },
{ "a": 8, "b": "true" },
{ "a": 9, "b": false },
{ "a": 10, "b": null },
{ "a": 11 }
]
}
```
You can now query for all true(ish) or false(ish) values:
```
vals.#(b==~true)#.a >> [1,2,6,7,8]
vals.#(b==~false)#.a >> [3,4,5,9,10,11]
```
The last value which was non-existent is treated as `false`
### Dot vs Pipe
The `.` is standard separator, but it's also possible to use a `|`.
@@ -181,11 +227,20 @@ children.@reverse ["Jack","Alex","Sara"]
children.@reverse.0 "Jack"
```
There are currently three built-in modifiers:
There are currently the following built-in modifiers:
- `@reverse`: Reverse an array or the members of an object.
- `@ugly`: Remove all whitespace from JSON.
- `@pretty`: Make the JSON more human readable.
- `@this`: Returns the current element. It can be used to retrieve the root element.
- `@valid`: Ensure the json document is valid.
- `@flatten`: Flattens an array.
- `@join`: Joins multiple objects into a single object.
- `@keys`: Returns an array of keys for an object.
- `@values`: Returns an array of values for an object.
- `@tostr`: Converts json to a string. Wraps a json string.
- `@fromstr`: Converts a string from json. Unwraps a json string.
- `@group`: Groups arrays of objects. See [e4fc67c](https://github.com/tidwall/gjson/commit/e4fc67c92aeebf2089fabc7872f010e340d105db).
#### Modifier arguments
@@ -236,13 +291,15 @@ gjson.AddModifier("case", func(json, arg string) string {
"children.@case:lower.@reverse" ["jack","alex","sara"]
```
*Note: Custom modifiers are not yet available in the Rust version*
### Multipaths
Starting with v1.3.0, GJSON added the ability to join multiple paths together
to form new documents. Wrapping comma-separated paths between `{...}` or
`[...]` will result in a new array or object, respectively.
to form new documents. Wrapping comma-separated paths between `[...]` or
`{...}` will result in a new array or object, respectively.
For example, using the given multipath
For example, using the given multipath:
```
{name.first,age,"the_murphys":friends.#(last="Murphy")#.first}
@@ -258,8 +315,28 @@ determined, then "_" is used.
This results in
```
```json
{"first":"Tom","age":37,"the_murphys":["Dale","Jane"]}
```
### Literals
Starting with v1.12.0, GJSON added support of json literals, which provides a way for constructing static blocks of json. This is can be particularly useful when constructing a new json document using [multipaths](#multipaths).
A json literal begins with the '!' declaration character.
For example, using the given multipath:
```
{name.first,age,"company":!"Happysoft","employed":!true}
```
Here we selected the first name and age. Then add two new fields, "company" and "employed".
This results in
```json
{"first":"Tom","age":37,"company":"Happysoft","employed":true}
```
*See issue [#249](https://github.com/tidwall/gjson/issues/249) for additional context on JSON Literals.*
+964 -431
View File
File diff suppressed because it is too large Load Diff
-18
View File
@@ -1,18 +0,0 @@
//+build appengine js
package gjson
func getBytes(json []byte, path string) Result {
return Get(string(json), path)
}
func fillIndex(json string, c *parseContext) {
// noop. Use zero for the Index value.
}
func stringBytes(s string) []byte {
return []byte(s)
}
func bytesString(b []byte) string {
return string(b)
}
-81
View File
@@ -1,81 +0,0 @@
//+build !appengine
//+build !js
package gjson
import (
"reflect"
"unsafe"
)
// getBytes casts the input json bytes to a string and safely returns the
// results as uniquely allocated data. This operation is intended to minimize
// copies and allocations for the large json string->[]byte.
func getBytes(json []byte, path string) Result {
var result Result
if json != nil {
// unsafe cast to string
result = Get(*(*string)(unsafe.Pointer(&json)), path)
// safely get the string headers
rawhi := *(*reflect.StringHeader)(unsafe.Pointer(&result.Raw))
strhi := *(*reflect.StringHeader)(unsafe.Pointer(&result.Str))
// create byte slice headers
rawh := reflect.SliceHeader{Data: rawhi.Data, Len: rawhi.Len}
strh := reflect.SliceHeader{Data: strhi.Data, Len: strhi.Len}
if strh.Data == 0 {
// str is nil
if rawh.Data == 0 {
// raw is nil
result.Raw = ""
} else {
// raw has data, safely copy the slice header to a string
result.Raw = string(*(*[]byte)(unsafe.Pointer(&rawh)))
}
result.Str = ""
} else if rawh.Data == 0 {
// raw is nil
result.Raw = ""
// str has data, safely copy the slice header to a string
result.Str = string(*(*[]byte)(unsafe.Pointer(&strh)))
} else if strh.Data >= rawh.Data &&
int(strh.Data)+strh.Len <= int(rawh.Data)+rawh.Len {
// Str is a substring of Raw.
start := int(strh.Data - rawh.Data)
// safely copy the raw slice header
result.Raw = string(*(*[]byte)(unsafe.Pointer(&rawh)))
// substring the raw
result.Str = result.Raw[start : start+strh.Len]
} else {
// safely copy both the raw and str slice headers to strings
result.Raw = string(*(*[]byte)(unsafe.Pointer(&rawh)))
result.Str = string(*(*[]byte)(unsafe.Pointer(&strh)))
}
}
return result
}
// fillIndex finds the position of Raw data and assigns it to the Index field
// of the resulting value. If the position cannot be found then Index zero is
// used instead.
func fillIndex(json string, c *parseContext) {
if len(c.value.Raw) > 0 && !c.calcd {
jhdr := *(*reflect.StringHeader)(unsafe.Pointer(&json))
rhdr := *(*reflect.StringHeader)(unsafe.Pointer(&(c.value.Raw)))
c.value.Index = int(rhdr.Data - jhdr.Data)
if c.value.Index < 0 || c.value.Index >= len(json) {
c.value.Index = 0
}
}
}
func stringBytes(s string) []byte {
return *(*[]byte)(unsafe.Pointer(&reflect.SliceHeader{
Data: (*reflect.StringHeader)(unsafe.Pointer(&s)).Data,
Len: len(s),
Cap: len(s),
}))
}
func bytesString(b []byte) string {
return *(*string)(unsafe.Pointer(&b))
}
-8
View File
@@ -1,8 +0,0 @@
module github.com/tidwall/gjson
go 1.12
require (
github.com/tidwall/match v1.0.1
github.com/tidwall/pretty v1.0.0
)
-4
View File
@@ -1,4 +0,0 @@
github.com/tidwall/match v1.0.1 h1:PnKP62LPNxHKTwvHHZZzdOAOCtsJTjo6dZLCwpKm5xc=
github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E=
github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
-1
View File
@@ -1 +0,0 @@
language: go
+9 -12
View File
@@ -1,20 +1,17 @@
Match
=====
<a href="https://travis-ci.org/tidwall/match"><img src="https://img.shields.io/travis/tidwall/match.svg?style=flat-square" alt="Build Status"></a>
<a href="https://godoc.org/github.com/tidwall/match"><img src="https://img.shields.io/badge/api-reference-blue.svg?style=flat-square" alt="GoDoc"></a>
# Match
[![GoDoc](https://godoc.org/github.com/tidwall/match?status.svg)](https://godoc.org/github.com/tidwall/match)
Match is a very simple pattern matcher where '*' matches on any
number characters and '?' matches on any one character.
Installing
----------
## Installing
```
go get -u github.com/tidwall/match
```
Example
-------
## Example
```go
match.Match("hello", "*llo")
@@ -23,10 +20,10 @@ match.Match("hello", "h*o")
```
Contact
-------
## Contact
Josh Baker [@tidwall](http://twitter.com/tidwall)
License
-------
## License
Redcon source code is available under the MIT [License](/LICENSE).
+153 -97
View File
@@ -1,12 +1,14 @@
// Match provides a simple pattern matcher with unicode support.
// Package match provides a simple pattern matcher with unicode support.
package match
import "unicode/utf8"
import (
"unicode/utf8"
)
// Match returns true if str matches pattern. This is a very
// simple wildcard match where '*' matches on any number characters
// and '?' matches on any one character.
//
// pattern:
// { term }
// term:
@@ -19,113 +21,167 @@ func Match(str, pattern string) bool {
if pattern == "*" {
return true
}
return deepMatch(str, pattern)
}
func deepMatch(str, pattern string) bool {
for len(pattern) > 0 {
if pattern[0] > 0x7f {
return deepMatchRune(str, pattern)
}
switch pattern[0] {
default:
if len(str) == 0 {
return false
}
if str[0] > 0x7f {
return deepMatchRune(str, pattern)
}
if str[0] != pattern[0] {
return false
}
case '?':
if len(str) == 0 {
return false
}
case '*':
return deepMatch(str, pattern[1:]) ||
(len(str) > 0 && deepMatch(str[1:], pattern))
}
str = str[1:]
pattern = pattern[1:]
}
return len(str) == 0 && len(pattern) == 0
return match(str, pattern, 0, nil, -1) == rMatch
}
func deepMatchRune(str, pattern string) bool {
var sr, pr rune
var srsz, prsz int
// MatchLimit is the same as Match but will limit the complexity of the match
// operation. This is to avoid long running matches, specifically to avoid ReDos
// attacks from arbritary inputs.
//
// How it works:
// The underlying match routine is recursive and may call itself when it
// encounters a sandwiched wildcard pattern, such as: `user:*:name`.
// Everytime it calls itself a counter is incremented.
// The operation is stopped when counter > maxcomp*len(str).
func MatchLimit(str, pattern string, maxcomp int) (matched, stopped bool) {
if pattern == "*" {
return true, false
}
counter := 0
r := match(str, pattern, len(str), &counter, maxcomp)
if r == rStop {
return false, true
}
return r == rMatch, false
}
// read the first rune ahead of time
if len(str) > 0 {
if str[0] > 0x7f {
sr, srsz = utf8.DecodeRuneInString(str)
} else {
sr, srsz = rune(str[0]), 1
type result int
const (
rNoMatch result = iota
rMatch
rStop
)
func match(str, pat string, slen int, counter *int, maxcomp int) result {
// check complexity limit
if maxcomp > -1 {
if *counter > slen*maxcomp {
return rStop
}
} else {
sr, srsz = utf8.RuneError, 0
*counter++
}
if len(pattern) > 0 {
if pattern[0] > 0x7f {
pr, prsz = utf8.DecodeRuneInString(pattern)
} else {
pr, prsz = rune(pattern[0]), 1
for len(pat) > 0 {
var wild bool
pc, ps := rune(pat[0]), 1
if pc > 0x7f {
pc, ps = utf8.DecodeRuneInString(pat)
}
} else {
pr, prsz = utf8.RuneError, 0
}
// done reading
for pr != utf8.RuneError {
switch pr {
default:
if srsz == utf8.RuneError {
return false
}
if sr != pr {
return false
}
case '?':
if srsz == utf8.RuneError {
return false
}
case '*':
return deepMatchRune(str, pattern[prsz:]) ||
(srsz > 0 && deepMatchRune(str[srsz:], pattern))
}
str = str[srsz:]
pattern = pattern[prsz:]
// read the next runes
var sc rune
var ss int
if len(str) > 0 {
if str[0] > 0x7f {
sr, srsz = utf8.DecodeRuneInString(str)
} else {
sr, srsz = rune(str[0]), 1
sc, ss = rune(str[0]), 1
if sc > 0x7f {
sc, ss = utf8.DecodeRuneInString(str)
}
} else {
sr, srsz = utf8.RuneError, 0
}
if len(pattern) > 0 {
if pattern[0] > 0x7f {
pr, prsz = utf8.DecodeRuneInString(pattern)
} else {
pr, prsz = rune(pattern[0]), 1
switch pc {
case '?':
if ss == 0 {
return rNoMatch
}
case '*':
// Ignore repeating stars.
for len(pat) > 1 && pat[1] == '*' {
pat = pat[1:]
}
} else {
pr, prsz = utf8.RuneError, 0
}
// done reading
}
return srsz == 0 && prsz == 0
// If this star is the last character then it must be a match.
if len(pat) == 1 {
return rMatch
}
// Match and trim any non-wildcard suffix characters.
var ok bool
str, pat, ok = matchTrimSuffix(str, pat)
if !ok {
return rNoMatch
}
// Check for single star again.
if len(pat) == 1 {
return rMatch
}
// Perform recursive wildcard search.
r := match(str, pat[1:], slen, counter, maxcomp)
if r != rNoMatch {
return r
}
if len(str) == 0 {
return rNoMatch
}
wild = true
default:
if ss == 0 {
return rNoMatch
}
if pc == '\\' {
pat = pat[ps:]
pc, ps = utf8.DecodeRuneInString(pat)
if ps == 0 {
return rNoMatch
}
}
if sc != pc {
return rNoMatch
}
}
str = str[ss:]
if !wild {
pat = pat[ps:]
}
}
if len(str) == 0 {
return rMatch
}
return rNoMatch
}
var maxRuneBytes = func() []byte {
b := make([]byte, 4)
if utf8.EncodeRune(b, '\U0010FFFF') != 4 {
panic("invalid rune encoding")
// matchTrimSuffix matches and trims any non-wildcard suffix characters.
// Returns the trimed string and pattern.
//
// This is called because the pattern contains extra data after the wildcard
// star. Here we compare any suffix characters in the pattern to the suffix of
// the target string. Basically a reverse match that stops when a wildcard
// character is reached. This is a little trickier than a forward match because
// we need to evaluate an escaped character in reverse.
//
// Any matched characters will be trimmed from both the target
// string and the pattern.
func matchTrimSuffix(str, pat string) (string, string, bool) {
// It's expected that the pattern has at least two bytes and the first byte
// is a wildcard star '*'
match := true
for len(str) > 0 && len(pat) > 1 {
pc, ps := utf8.DecodeLastRuneInString(pat)
var esc bool
for i := 0; ; i++ {
if pat[len(pat)-ps-i-1] != '\\' {
if i&1 == 1 {
esc = true
ps++
}
break
}
}
if pc == '*' && !esc {
match = true
break
}
sc, ss := utf8.DecodeLastRuneInString(str)
if !((pc == '?' && !esc) || pc == sc) {
match = false
break
}
str = str[:len(str)-ss]
pat = pat[:len(pat)-ps]
}
return b
}()
return str, pat, match
}
var maxRuneBytes = [...]byte{244, 143, 191, 191}
// Allowable parses the pattern and determines the minimum and maximum allowable
// values that the pattern can represent.
@@ -146,7 +202,7 @@ func Allowable(pattern string) (min, max string) {
}
if pattern[i] == '?' {
minb = append(minb, 0)
maxb = append(maxb, maxRuneBytes...)
maxb = append(maxb, maxRuneBytes[:]...)
} else {
minb = append(minb, pattern[i])
maxb = append(maxb, pattern[i])
-1
View File
@@ -1 +0,0 @@
language: go
+8 -10
View File
@@ -1,8 +1,6 @@
# Pretty
[![Build Status](https://img.shields.io/travis/tidwall/pretty.svg?style=flat-square)](https://travis-ci.org/tidwall/prettty)
[![Coverage Status](https://img.shields.io/badge/coverage-100%25-brightgreen.svg?style=flat-square)](http://gocover.io/github.com/tidwall/pretty)
[![GoDoc](https://img.shields.io/badge/api-reference-blue.svg?style=flat-square)](https://godoc.org/github.com/tidwall/pretty)
[![GoDoc](https://img.shields.io/badge/api-reference-blue.svg?style=flat-square)](https://pkg.go.dev/github.com/tidwall/pretty)
Pretty is a Go package that provides [fast](#performance) methods for formatting JSON for human readability, or to compact JSON for smaller payloads.
@@ -81,7 +79,6 @@ Will format the json to:
{"name":{"first":"Tom","last":"Anderson"},"age":37,"children":["Sara","Alex","Jack"],"fav.movie":"Deer Hunter","friends":[{"first":"Janet","last":"Murphy","age":44}]}```
```
## Customized output
There's a `PrettyOptions(json, opts)` function which allows for customizing the output with the following options:
@@ -106,14 +103,15 @@ type Options struct {
Benchmarks of Pretty alongside the builtin `encoding/json` Indent/Compact methods.
```
BenchmarkPretty-8 1000000 1283 ns/op 720 B/op 2 allocs/op
BenchmarkUgly-8 3000000 426 ns/op 240 B/op 1 allocs/op
BenchmarkUglyInPlace-8 5000000 340 ns/op 0 B/op 0 allocs/op
BenchmarkJSONIndent-8 300000 4628 ns/op 1069 B/op 4 allocs/op
BenchmarkJSONCompact-8 1000000 2469 ns/op 758 B/op 4 allocs/op
BenchmarkPretty-16 1000000 1034 ns/op 720 B/op 2 allocs/op
BenchmarkPrettySortKeys-16 586797 1983 ns/op 2848 B/op 14 allocs/op
BenchmarkUgly-16 4652365 254 ns/op 240 B/op 1 allocs/op
BenchmarkUglyInPlace-16 6481233 183 ns/op 0 B/op 0 allocs/op
BenchmarkJSONIndent-16 450654 2687 ns/op 1221 B/op 0 allocs/op
BenchmarkJSONCompact-16 685111 1699 ns/op 442 B/op 0 allocs/op
```
*These benchmarks were run on a MacBook Pro 15" 2.8 GHz Intel Core i7 using Go 1.7.*
*These benchmarks were run on a MacBook Pro 2.4 GHz 8-Core Intel Core i9.*
## Contact
Josh Baker [@tidwall](http://twitter.com/tidwall)
+272 -30
View File
@@ -1,7 +1,10 @@
package pretty
import (
"bytes"
"encoding/json"
"sort"
"strconv"
)
// Options is Pretty options
@@ -84,6 +87,14 @@ func ugly(dst, src []byte) []byte {
return dst
}
func isNaNOrInf(src []byte) bool {
return src[0] == 'i' || //Inf
src[0] == 'I' || // inf
src[0] == '+' || // +Inf
src[0] == 'N' || // Nan
(src[0] == 'n' && len(src) > 1 && src[1] != 'u') // nan
}
func appendPrettyAny(buf, json []byte, i int, pretty bool, width int, prefix, indent string, sortkeys bool, tabs, nl, max int) ([]byte, int, int, bool) {
for ; i < len(json); i++ {
if json[i] <= ' ' {
@@ -92,7 +103,8 @@ func appendPrettyAny(buf, json []byte, i int, pretty bool, width int, prefix, in
if json[i] == '"' {
return appendPrettyString(buf, json, i, nl)
}
if (json[i] >= '0' && json[i] <= '9') || json[i] == '-' {
if (json[i] >= '0' && json[i] <= '9') || json[i] == '-' || isNaNOrInf(json[i:]) {
return appendPrettyNumber(buf, json, i, nl)
}
if json[i] == '{' {
@@ -118,25 +130,121 @@ type pair struct {
vstart, vend int
}
type byKey struct {
type byKeyVal struct {
sorted bool
json []byte
buf []byte
pairs []pair
}
func (arr *byKey) Len() int {
func (arr *byKeyVal) Len() int {
return len(arr.pairs)
}
func (arr *byKey) Less(i, j int) bool {
key1 := arr.json[arr.pairs[i].kstart+1 : arr.pairs[i].kend-1]
key2 := arr.json[arr.pairs[j].kstart+1 : arr.pairs[j].kend-1]
return string(key1) < string(key2)
func (arr *byKeyVal) Less(i, j int) bool {
if arr.isLess(i, j, byKey) {
return true
}
if arr.isLess(j, i, byKey) {
return false
}
return arr.isLess(i, j, byVal)
}
func (arr *byKey) Swap(i, j int) {
func (arr *byKeyVal) Swap(i, j int) {
arr.pairs[i], arr.pairs[j] = arr.pairs[j], arr.pairs[i]
arr.sorted = true
}
type byKind int
const (
byKey byKind = 0
byVal byKind = 1
)
type jtype int
const (
jnull jtype = iota
jfalse
jnumber
jstring
jtrue
jjson
)
func getjtype(v []byte) jtype {
if len(v) == 0 {
return jnull
}
switch v[0] {
case '"':
return jstring
case 'f':
return jfalse
case 't':
return jtrue
case 'n':
return jnull
case '[', '{':
return jjson
default:
return jnumber
}
}
func (arr *byKeyVal) isLess(i, j int, kind byKind) bool {
k1 := arr.json[arr.pairs[i].kstart:arr.pairs[i].kend]
k2 := arr.json[arr.pairs[j].kstart:arr.pairs[j].kend]
var v1, v2 []byte
if kind == byKey {
v1 = k1
v2 = k2
} else {
v1 = bytes.TrimSpace(arr.buf[arr.pairs[i].vstart:arr.pairs[i].vend])
v2 = bytes.TrimSpace(arr.buf[arr.pairs[j].vstart:arr.pairs[j].vend])
if len(v1) >= len(k1)+1 {
v1 = bytes.TrimSpace(v1[len(k1)+1:])
}
if len(v2) >= len(k2)+1 {
v2 = bytes.TrimSpace(v2[len(k2)+1:])
}
}
t1 := getjtype(v1)
t2 := getjtype(v2)
if t1 < t2 {
return true
}
if t1 > t2 {
return false
}
if t1 == jstring {
s1 := parsestr(v1)
s2 := parsestr(v2)
return string(s1) < string(s2)
}
if t1 == jnumber {
n1, _ := strconv.ParseFloat(string(v1), 64)
n2, _ := strconv.ParseFloat(string(v2), 64)
return n1 < n2
}
return string(v1) < string(v2)
}
func parsestr(s []byte) []byte {
for i := 1; i < len(s); i++ {
if s[i] == '\\' {
var str string
json.Unmarshal(s, &str)
return []byte(str)
}
if s[i] == '"' {
return s[1:i]
}
}
return nil
}
func appendPrettyObject(buf, json []byte, i int, open, close byte, pretty bool, width int, prefix, indent string, sortkeys bool, tabs, nl, max int) ([]byte, int, int, bool) {
var ok bool
if width > 0 {
@@ -174,7 +282,11 @@ func appendPrettyObject(buf, json []byte, i int, open, close byte, pretty bool,
}
if n > 0 {
nl = len(buf)
buf = append(buf, '\n')
if buf[nl-1] == ' ' {
buf[nl-1] = '\n'
} else {
buf = append(buf, '\n')
}
}
if buf[len(buf)-1] != open {
buf = appendTabs(buf, prefix, indent, tabs)
@@ -193,7 +305,11 @@ func appendPrettyObject(buf, json []byte, i int, open, close byte, pretty bool,
var p pair
if pretty {
nl = len(buf)
buf = append(buf, '\n')
if buf[nl-1] == ' ' {
buf[nl-1] = '\n'
} else {
buf = append(buf, '\n')
}
if open == '{' && sortkeys {
p.kstart = i
p.vstart = len(buf)
@@ -235,8 +351,8 @@ func sortPairs(json, buf []byte, pairs []pair) []byte {
}
vstart := pairs[0].vstart
vend := pairs[len(pairs)-1].vend
arr := byKey{false, json, pairs}
sort.Sort(&arr)
arr := byKeyVal{false, json, buf, pairs}
sort.Stable(&arr)
if !arr.sorted {
return buf
}
@@ -305,6 +421,7 @@ func appendTabs(buf []byte, prefix, indent string, tabs int) []byte {
type Style struct {
Key, String, Number [2]string
True, False, Null [2]string
Escape [2]string
Append func(dst []byte, c byte) []byte
}
@@ -318,21 +435,26 @@ func hexp(p byte) byte {
}
// TerminalStyle is for terminals
var TerminalStyle = &Style{
Key: [2]string{"\x1B[94m", "\x1B[0m"},
String: [2]string{"\x1B[92m", "\x1B[0m"},
Number: [2]string{"\x1B[93m", "\x1B[0m"},
True: [2]string{"\x1B[96m", "\x1B[0m"},
False: [2]string{"\x1B[96m", "\x1B[0m"},
Null: [2]string{"\x1B[91m", "\x1B[0m"},
Append: func(dst []byte, c byte) []byte {
if c < ' ' && (c != '\r' && c != '\n' && c != '\t' && c != '\v') {
dst = append(dst, "\\u00"...)
dst = append(dst, hexp((c>>4)&0xF))
return append(dst, hexp((c)&0xF))
}
return append(dst, c)
},
var TerminalStyle *Style
func init() {
TerminalStyle = &Style{
Key: [2]string{"\x1B[94m", "\x1B[0m"},
String: [2]string{"\x1B[92m", "\x1B[0m"},
Number: [2]string{"\x1B[93m", "\x1B[0m"},
True: [2]string{"\x1B[96m", "\x1B[0m"},
False: [2]string{"\x1B[96m", "\x1B[0m"},
Null: [2]string{"\x1B[91m", "\x1B[0m"},
Escape: [2]string{"\x1B[35m", "\x1B[0m"},
Append: func(dst []byte, c byte) []byte {
if c < ' ' && (c != '\r' && c != '\n' && c != '\t' && c != '\v') {
dst = append(dst, "\\u00"...)
dst = append(dst, hexp((c>>4)&0xF))
return append(dst, hexp((c)&0xF))
}
return append(dst, c)
},
}
}
// Color will colorize the json. The style parma is used for customizing
@@ -363,8 +485,39 @@ func Color(src []byte, style *Style) []byte {
dst = append(dst, style.String[0]...)
}
dst = apnd(dst, '"')
esc := false
uesc := 0
for i = i + 1; i < len(src); i++ {
dst = apnd(dst, src[i])
if src[i] == '\\' {
if key {
dst = append(dst, style.Key[1]...)
} else {
dst = append(dst, style.String[1]...)
}
dst = append(dst, style.Escape[0]...)
dst = apnd(dst, src[i])
esc = true
if i+1 < len(src) && src[i+1] == 'u' {
uesc = 5
} else {
uesc = 1
}
} else if esc {
dst = apnd(dst, src[i])
if uesc == 1 {
esc = false
dst = append(dst, style.Escape[1]...)
if key {
dst = append(dst, style.Key[0]...)
} else {
dst = append(dst, style.String[0]...)
}
} else {
uesc--
}
} else {
dst = apnd(dst, src[i])
}
if src[i] == '"' {
j := i - 1
for ; ; j-- {
@@ -377,7 +530,9 @@ func Color(src []byte, style *Style) []byte {
}
}
}
if key {
if esc {
dst = append(dst, style.Escape[1]...)
} else if key {
dst = append(dst, style.Key[1]...)
} else {
dst = append(dst, style.String[1]...)
@@ -393,7 +548,7 @@ func Color(src []byte, style *Style) []byte {
dst = apnd(dst, src[i])
} else {
var kind byte
if (src[i] >= '0' && src[i] <= '9') || src[i] == '-' {
if (src[i] >= '0' && src[i] <= '9') || src[i] == '-' || isNaNOrInf(src[i:]) {
kind = '0'
dst = append(dst, style.Number[0]...)
} else if src[i] == 't' {
@@ -430,3 +585,90 @@ func Color(src []byte, style *Style) []byte {
}
return dst
}
// Spec strips out comments and trailing commas and convert the input to a
// valid JSON per the official spec: https://tools.ietf.org/html/rfc8259
//
// The resulting JSON will always be the same length as the input and it will
// include all of the same line breaks at matching offsets. This is to ensure
// the result can be later processed by a external parser and that that
// parser will report messages or errors with the correct offsets.
func Spec(src []byte) []byte {
return spec(src, nil)
}
// SpecInPlace is the same as Spec, but this method reuses the input json
// buffer to avoid allocations. Do not use the original bytes slice upon return.
func SpecInPlace(src []byte) []byte {
return spec(src, src)
}
func spec(src, dst []byte) []byte {
dst = dst[:0]
for i := 0; i < len(src); i++ {
if src[i] == '/' {
if i < len(src)-1 {
if src[i+1] == '/' {
dst = append(dst, ' ', ' ')
i += 2
for ; i < len(src); i++ {
if src[i] == '\n' {
dst = append(dst, '\n')
break
} else if src[i] == '\t' || src[i] == '\r' {
dst = append(dst, src[i])
} else {
dst = append(dst, ' ')
}
}
continue
}
if src[i+1] == '*' {
dst = append(dst, ' ', ' ')
i += 2
for ; i < len(src)-1; i++ {
if src[i] == '*' && src[i+1] == '/' {
dst = append(dst, ' ', ' ')
i++
break
} else if src[i] == '\n' || src[i] == '\t' ||
src[i] == '\r' {
dst = append(dst, src[i])
} else {
dst = append(dst, ' ')
}
}
continue
}
}
}
dst = append(dst, src[i])
if src[i] == '"' {
for i = i + 1; i < len(src); i++ {
dst = append(dst, src[i])
if src[i] == '"' {
j := i - 1
for ; ; j-- {
if src[j] != '\\' {
break
}
}
if (j-i)%2 != 0 {
break
}
}
}
} else if src[i] == '}' || src[i] == ']' {
for j := len(dst) - 2; j >= 0; j-- {
if dst[j] <= ' ' {
continue
}
if dst[j] == ',' {
dst[j] = ' '
}
break
}
}
}
return dst
}
@@ -1,7 +1,6 @@
RTree implementation for Go
===========================
[![Build Status](https://travis-ci.org/tidwall/rtree.svg?branch=master)](https://travis-ci.org/tidwall/rtree)
[![GoDoc](https://godoc.org/github.com/tidwall/rtree?status.svg)](https://godoc.org/github.com/tidwall/rtree)
This package provides an in-memory R-Tree implementation for Go, useful as a spatial data structure.
@@ -183,6 +183,7 @@ type treeItem struct {
item interface{}
}
//go:nocheckptr
func (item *treeItem) unsafeNode() *treeNode {
return (*treeNode)(unsafe.Pointer(item))
}
@@ -1,10 +1,10 @@
package rtree
package rtred
import (
"math"
"sync"
"github.com/tidwall/rtree/base"
"github.com/tidwall/rtred/base"
)
type Iterator func(item Item) bool
-1
View File
@@ -1 +0,0 @@
language: go
-3
View File
@@ -1,3 +0,0 @@
# This source code refers to The Go Authors for copyright purposes.
# The master list of authors is in the main Go distribution,
# visible at https://tip.golang.org/AUTHORS.
-3
View File
@@ -1,3 +0,0 @@
# This source code was written by the Go contributors.
# The master list of contributors is in the main Go distribution,
# visible at https://tip.golang.org/CONTRIBUTORS.
+2 -2
View File
@@ -1,4 +1,4 @@
Copyright (c) 2009 The Go Authors. All rights reserved.
Copyright 2009 The Go Authors.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
@@ -10,7 +10,7 @@ notice, this list of conditions and the following disclaimer.
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
* Neither the name of Google LLC nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
+11 -2
View File
@@ -4,7 +4,7 @@
// Package bcrypt implements Provos and Mazières's bcrypt adaptive hashing
// algorithm. See http://www.usenix.org/event/usenix99/provos/provos.pdf
package bcrypt // import "golang.org/x/crypto/bcrypt"
package bcrypt
// The code is a port of Provos and Mazières's C implementation.
import (
@@ -50,7 +50,7 @@ func (ih InvalidHashPrefixError) Error() string {
type InvalidCostError int
func (ic InvalidCostError) Error() string {
return fmt.Sprintf("crypto/bcrypt: cost %d is outside allowed range (%d,%d)", int(ic), int(MinCost), int(MaxCost))
return fmt.Sprintf("crypto/bcrypt: cost %d is outside allowed inclusive range %d..%d", int(ic), MinCost, MaxCost)
}
const (
@@ -82,11 +82,20 @@ type hashed struct {
minor byte
}
// ErrPasswordTooLong is returned when the password passed to
// GenerateFromPassword is too long (i.e. > 72 bytes).
var ErrPasswordTooLong = errors.New("bcrypt: password length exceeds 72 bytes")
// GenerateFromPassword returns the bcrypt hash of the password at the given
// cost. If the cost given is less than MinCost, the cost will be set to
// DefaultCost, instead. Use CompareHashAndPassword, as defined in this package,
// to compare the returned hashed password with its cleartext version.
// GenerateFromPassword does not accept passwords longer than 72 bytes, which
// is the longest password bcrypt will operate on.
func GenerateFromPassword(password []byte, cost int) ([]byte, error) {
if len(password) > 72 {
return nil, ErrPasswordTooLong
}
p, err := newFromPassword(password, cost)
if err != nil {
return nil, err
+1 -1
View File
@@ -11,7 +11,7 @@
// Deprecated: any new system should use AES (from crypto/aes, if necessary in
// an AEAD mode like crypto/cipher.NewGCM) or XChaCha20-Poly1305 (from
// golang.org/x/crypto/chacha20poly1305).
package blowfish // import "golang.org/x/crypto/blowfish"
package blowfish
// The code is a port of Bruce Schneier's C implementation.
// See https://www.schneier.com/blowfish.html.
+39 -950
View File
File diff suppressed because it is too large Load Diff
-10
View File
@@ -1,10 +0,0 @@
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package terminal
import "golang.org/x/sys/unix"
const ioctlReadTermios = unix.TCGETS
const ioctlWriteTermios = unix.TCSETS
-58
View File
@@ -1,58 +0,0 @@
// Copyright 2016 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package terminal provides support functions for dealing with terminals, as
// commonly found on UNIX systems.
//
// Putting a terminal into raw mode is the most common requirement:
//
// oldState, err := terminal.MakeRaw(0)
// if err != nil {
// panic(err)
// }
// defer terminal.Restore(0, oldState)
package terminal
import (
"fmt"
"runtime"
)
type State struct{}
// IsTerminal returns whether the given file descriptor is a terminal.
func IsTerminal(fd int) bool {
return false
}
// MakeRaw put the terminal connected to the given file descriptor into raw
// mode and returns the previous state of the terminal so that it can be
// restored.
func MakeRaw(fd int) (*State, error) {
return nil, fmt.Errorf("terminal: MakeRaw not implemented on %s/%s", runtime.GOOS, runtime.GOARCH)
}
// GetState returns the current state of a terminal which may be useful to
// restore the terminal after a signal.
func GetState(fd int) (*State, error) {
return nil, fmt.Errorf("terminal: GetState not implemented on %s/%s", runtime.GOOS, runtime.GOARCH)
}
// Restore restores the terminal connected to the given file descriptor to a
// previous state.
func Restore(fd int, state *State) error {
return fmt.Errorf("terminal: Restore not implemented on %s/%s", runtime.GOOS, runtime.GOARCH)
}
// GetSize returns the dimensions of the given terminal.
func GetSize(fd int) (width, height int, err error) {
return 0, 0, fmt.Errorf("terminal: GetSize not implemented on %s/%s", runtime.GOOS, runtime.GOARCH)
}
// ReadPassword reads a line of input from a terminal without local echo. This
// is commonly used for inputting passwords and other sensitive data. The slice
// returned does not include the \n.
func ReadPassword(fd int) ([]byte, error) {
return nil, fmt.Errorf("terminal: ReadPassword not implemented on %s/%s", runtime.GOOS, runtime.GOARCH)
}
-124
View File
@@ -1,124 +0,0 @@
// Copyright 2015 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build solaris
package terminal // import "golang.org/x/crypto/ssh/terminal"
import (
"golang.org/x/sys/unix"
"io"
"syscall"
)
// State contains the state of a terminal.
type State struct {
termios unix.Termios
}
// IsTerminal returns whether the given file descriptor is a terminal.
func IsTerminal(fd int) bool {
_, err := unix.IoctlGetTermio(fd, unix.TCGETA)
return err == nil
}
// ReadPassword reads a line of input from a terminal without local echo. This
// is commonly used for inputting passwords and other sensitive data. The slice
// returned does not include the \n.
func ReadPassword(fd int) ([]byte, error) {
// see also: http://src.illumos.org/source/xref/illumos-gate/usr/src/lib/libast/common/uwin/getpass.c
val, err := unix.IoctlGetTermios(fd, unix.TCGETS)
if err != nil {
return nil, err
}
oldState := *val
newState := oldState
newState.Lflag &^= syscall.ECHO
newState.Lflag |= syscall.ICANON | syscall.ISIG
newState.Iflag |= syscall.ICRNL
err = unix.IoctlSetTermios(fd, unix.TCSETS, &newState)
if err != nil {
return nil, err
}
defer unix.IoctlSetTermios(fd, unix.TCSETS, &oldState)
var buf [16]byte
var ret []byte
for {
n, err := syscall.Read(fd, buf[:])
if err != nil {
return nil, err
}
if n == 0 {
if len(ret) == 0 {
return nil, io.EOF
}
break
}
if buf[n-1] == '\n' {
n--
}
ret = append(ret, buf[:n]...)
if n < len(buf) {
break
}
}
return ret, nil
}
// MakeRaw puts the terminal connected to the given file descriptor into raw
// mode and returns the previous state of the terminal so that it can be
// restored.
// see http://cr.illumos.org/~webrev/andy_js/1060/
func MakeRaw(fd int) (*State, error) {
termios, err := unix.IoctlGetTermios(fd, unix.TCGETS)
if err != nil {
return nil, err
}
oldState := State{termios: *termios}
termios.Iflag &^= unix.IGNBRK | unix.BRKINT | unix.PARMRK | unix.ISTRIP | unix.INLCR | unix.IGNCR | unix.ICRNL | unix.IXON
termios.Oflag &^= unix.OPOST
termios.Lflag &^= unix.ECHO | unix.ECHONL | unix.ICANON | unix.ISIG | unix.IEXTEN
termios.Cflag &^= unix.CSIZE | unix.PARENB
termios.Cflag |= unix.CS8
termios.Cc[unix.VMIN] = 1
termios.Cc[unix.VTIME] = 0
if err := unix.IoctlSetTermios(fd, unix.TCSETS, termios); err != nil {
return nil, err
}
return &oldState, nil
}
// Restore restores the terminal connected to the given file descriptor to a
// previous state.
func Restore(fd int, oldState *State) error {
return unix.IoctlSetTermios(fd, unix.TCSETS, &oldState.termios)
}
// GetState returns the current state of a terminal which may be useful to
// restore the terminal after a signal.
func GetState(fd int) (*State, error) {
termios, err := unix.IoctlGetTermios(fd, unix.TCGETS)
if err != nil {
return nil, err
}
return &State{termios: *termios}, nil
}
// GetSize returns the dimensions of the given terminal.
func GetSize(fd int) (width, height int, err error) {
ws, err := unix.IoctlGetWinsize(fd, unix.TIOCGWINSZ)
if err != nil {
return 0, 0, err
}
return int(ws.Col), int(ws.Row), nil
}
-3
View File
@@ -1,3 +0,0 @@
# This source code refers to The Go Authors for copyright purposes.
# The master list of authors is in the main Go distribution,
# visible at http://tip.golang.org/AUTHORS.
-3
View File
@@ -1,3 +0,0 @@
# This source code was written by the Go contributors.
# The master list of contributors is in the main Go distribution,
# visible at http://tip.golang.org/CONTRIBUTORS.
+2 -2
View File
@@ -1,4 +1,4 @@
Copyright (c) 2009 The Go Authors. All rights reserved.
Copyright 2009 The Go Authors.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
@@ -10,7 +10,7 @@ notice, this list of conditions and the following disclaimer.
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
* Neither the name of Google LLC nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
+20 -5
View File
@@ -12,6 +12,7 @@ import (
"os"
"path"
"path/filepath"
"runtime"
"strings"
"sync"
"time"
@@ -57,6 +58,13 @@ type File interface {
// A Dir implements FileSystem using the native file system restricted to a
// specific directory tree.
//
// Dir does not prevent traversal of symbolic links within its directory tree,
// including links that reference locations outside of the tree.
//
// Dir does not prevent filesystem modifications which change the target
// of relative symbolic links. For example, moving a directory containing
// a symbolic link to "../target" may cause the link to point to a new target.
//
// While the FileSystem.OpenFile method takes '/'-separated paths, a Dir's
// string value is a filename on the native file system, not a URL, so it is
// separated by filepath.Separator, which isn't necessarily '/'.
@@ -162,6 +170,7 @@ type memFS struct {
// - "/", "foo", false
// - "/foo/", "bar", false
// - "/foo/bar/", "x", true
//
// The frag argument will be empty only if dir is the root node and the walk
// ends at that root node.
func (fs *memFS) walk(op, fullname string, f func(dir *memFSNode, frag string, final bool) error) error {
@@ -272,8 +281,14 @@ func (fs *memFS) OpenFile(ctx context.Context, name string, flag int, perm os.Fi
var n *memFSNode
if dir == nil {
// We're opening the root.
if flag&(os.O_WRONLY|os.O_RDWR) != 0 {
return nil, os.ErrPermission
if runtime.GOOS == "zos" {
if flag&os.O_WRONLY != 0 {
return nil, os.ErrPermission
}
} else {
if flag&(os.O_WRONLY|os.O_RDWR) != 0 {
return nil, os.ErrPermission
}
}
n, frag = &fs.root, "/"
@@ -539,11 +554,11 @@ func (f *memFile) Seek(offset int64, whence int) (int64, error) {
npos := f.pos
// TODO: How to handle offsets greater than the size of system int?
switch whence {
case os.SEEK_SET:
case io.SeekStart:
npos = int(offset)
case os.SEEK_CUR:
case io.SeekCurrent:
npos += int(offset)
case os.SEEK_END:
case io.SeekEnd:
npos = len(f.n.data) + int(offset)
default:
npos = -1
+1 -1
View File
@@ -24,7 +24,7 @@ type ifList struct {
// parseIfHeader parses the "If: foo bar" HTTP header. The httpHeader string
// should omit the "If:" prefix and have any "\r\n"s collapsed to a " ", as is
// returned by req.Header.Get("If") for a http.Request req.
// returned by req.Header.Get("If") for an http.Request req.
func parseIfHeader(httpHeader string) (h ifHeader, ok bool) {
s := strings.TrimSpace(httpHeader)
switch tokenType, _, _ := lex(s); tokenType {
+27 -27
View File
@@ -32,33 +32,33 @@ const (
// elements containing the data.
//
// The name for the XML elements is taken from, in order of preference:
// - the tag on the XMLName field, if the data is a struct
// - the value of the XMLName field of type xml.Name
// - the tag of the struct field used to obtain the data
// - the name of the struct field used to obtain the data
// - the name of the marshalled type
// - the tag on the XMLName field, if the data is a struct
// - the value of the XMLName field of type xml.Name
// - the tag of the struct field used to obtain the data
// - the name of the struct field used to obtain the data
// - the name of the marshalled type
//
// The XML element for a struct contains marshalled elements for each of the
// exported fields of the struct, with these exceptions:
// - the XMLName field, described above, is omitted.
// - a field with tag "-" is omitted.
// - a field with tag "name,attr" becomes an attribute with
// the given name in the XML element.
// - a field with tag ",attr" becomes an attribute with the
// field name in the XML element.
// - a field with tag ",chardata" is written as character data,
// not as an XML element.
// - a field with tag ",innerxml" is written verbatim, not subject
// to the usual marshalling procedure.
// - a field with tag ",comment" is written as an XML comment, not
// subject to the usual marshalling procedure. It must not contain
// the "--" string within it.
// - a field with a tag including the "omitempty" option is omitted
// if the field value is empty. The empty values are false, 0, any
// nil pointer or interface value, and any array, slice, map, or
// string of length zero.
// - an anonymous struct field is handled as if the fields of its
// value were part of the outer struct.
// - the XMLName field, described above, is omitted.
// - a field with tag "-" is omitted.
// - a field with tag "name,attr" becomes an attribute with
// the given name in the XML element.
// - a field with tag ",attr" becomes an attribute with the
// field name in the XML element.
// - a field with tag ",chardata" is written as character data,
// not as an XML element.
// - a field with tag ",innerxml" is written verbatim, not subject
// to the usual marshalling procedure.
// - a field with tag ",comment" is written as an XML comment, not
// subject to the usual marshalling procedure. It must not contain
// the "--" string within it.
// - a field with a tag including the "omitempty" option is omitted
// if the field value is empty. The empty values are false, 0, any
// nil pointer or interface value, and any array, slice, map, or
// string of length zero.
// - an anonymous struct field is handled as if the fields of its
// value were part of the outer struct.
//
// If a field uses a tag "a>b>c", then the element c will be nested inside
// parent elements a and b. Fields that appear next to each other that name
@@ -546,9 +546,9 @@ func (p *printer) setAttrPrefix(prefix, url string) {
}
var (
marshalerType = reflect.TypeOf((*Marshaler)(nil)).Elem()
marshalerAttrType = reflect.TypeOf((*MarshalerAttr)(nil)).Elem()
textMarshalerType = reflect.TypeOf((*encoding.TextMarshaler)(nil)).Elem()
marshalerType = reflect.TypeFor[Marshaler]()
marshalerAttrType = reflect.TypeFor[MarshalerAttr]()
textMarshalerType = reflect.TypeFor[encoding.TextMarshaler]()
)
// marshalValue writes one or more XML elements representing val.
+45 -49
View File
@@ -35,57 +35,57 @@ import (
// In the rules, the tag of a field refers to the value associated with the
// key 'xml' in the struct field's tag (see the example above).
//
// * If the struct has a field of type []byte or string with tag
// ",innerxml", Unmarshal accumulates the raw XML nested inside the
// element in that field. The rest of the rules still apply.
// - If the struct has a field of type []byte or string with tag
// ",innerxml", Unmarshal accumulates the raw XML nested inside the
// element in that field. The rest of the rules still apply.
//
// * If the struct has a field named XMLName of type xml.Name,
// Unmarshal records the element name in that field.
// - If the struct has a field named XMLName of type xml.Name,
// Unmarshal records the element name in that field.
//
// * If the XMLName field has an associated tag of the form
// "name" or "namespace-URL name", the XML element must have
// the given name (and, optionally, name space) or else Unmarshal
// returns an error.
// - If the XMLName field has an associated tag of the form
// "name" or "namespace-URL name", the XML element must have
// the given name (and, optionally, name space) or else Unmarshal
// returns an error.
//
// * If the XML element has an attribute whose name matches a
// struct field name with an associated tag containing ",attr" or
// the explicit name in a struct field tag of the form "name,attr",
// Unmarshal records the attribute value in that field.
// - If the XML element has an attribute whose name matches a
// struct field name with an associated tag containing ",attr" or
// the explicit name in a struct field tag of the form "name,attr",
// Unmarshal records the attribute value in that field.
//
// * If the XML element contains character data, that data is
// accumulated in the first struct field that has tag ",chardata".
// The struct field may have type []byte or string.
// If there is no such field, the character data is discarded.
// - If the XML element contains character data, that data is
// accumulated in the first struct field that has tag ",chardata".
// The struct field may have type []byte or string.
// If there is no such field, the character data is discarded.
//
// * If the XML element contains comments, they are accumulated in
// the first struct field that has tag ",comment". The struct
// field may have type []byte or string. If there is no such
// field, the comments are discarded.
// - If the XML element contains comments, they are accumulated in
// the first struct field that has tag ",comment". The struct
// field may have type []byte or string. If there is no such
// field, the comments are discarded.
//
// * If the XML element contains a sub-element whose name matches
// the prefix of a tag formatted as "a" or "a>b>c", unmarshal
// will descend into the XML structure looking for elements with the
// given names, and will map the innermost elements to that struct
// field. A tag starting with ">" is equivalent to one starting
// with the field name followed by ">".
// - If the XML element contains a sub-element whose name matches
// the prefix of a tag formatted as "a" or "a>b>c", unmarshal
// will descend into the XML structure looking for elements with the
// given names, and will map the innermost elements to that struct
// field. A tag starting with ">" is equivalent to one starting
// with the field name followed by ">".
//
// * If the XML element contains a sub-element whose name matches
// a struct field's XMLName tag and the struct field has no
// explicit name tag as per the previous rule, unmarshal maps
// the sub-element to that struct field.
// - If the XML element contains a sub-element whose name matches
// a struct field's XMLName tag and the struct field has no
// explicit name tag as per the previous rule, unmarshal maps
// the sub-element to that struct field.
//
// * If the XML element contains a sub-element whose name matches a
// field without any mode flags (",attr", ",chardata", etc), Unmarshal
// maps the sub-element to that struct field.
// - If the XML element contains a sub-element whose name matches a
// field without any mode flags (",attr", ",chardata", etc), Unmarshal
// maps the sub-element to that struct field.
//
// * If the XML element contains a sub-element that hasn't matched any
// of the above rules and the struct has a field with tag ",any",
// unmarshal maps the sub-element to that struct field.
// - If the XML element contains a sub-element that hasn't matched any
// of the above rules and the struct has a field with tag ",any",
// unmarshal maps the sub-element to that struct field.
//
// * An anonymous struct field is handled as if the fields of its
// value were part of the outer struct.
// - An anonymous struct field is handled as if the fields of its
// value were part of the outer struct.
//
// * A struct field with tag "-" is never unmarshalled into.
// - A struct field with tag "-" is never unmarshalled into.
//
// Unmarshal maps an XML element to a string or []byte by saving the
// concatenation of that element's character data in the string or
@@ -110,7 +110,6 @@ import (
//
// Unmarshal maps an XML element to a pointer by setting the pointer
// to a freshly allocated value and then mapping the element to that value.
//
func Unmarshal(data []byte, v interface{}) error {
return NewDecoder(bytes.NewReader(data)).Decode(v)
}
@@ -148,7 +147,7 @@ func (e UnmarshalError) Error() string { return string(e) }
// UnmarshalXML must consume exactly one XML element.
// One common implementation strategy is to unmarshal into
// a separate value with a layout matching the expected XML
// using d.DecodeElement, and then to copy the data from
// using d.DecodeElement, and then to copy the data from
// that value into the receiver.
// Another common strategy is to use d.Token to process the
// XML object one token at a time.
@@ -263,9 +262,9 @@ func (p *Decoder) unmarshalAttr(val reflect.Value, attr Attr) error {
}
var (
unmarshalerType = reflect.TypeOf((*Unmarshaler)(nil)).Elem()
unmarshalerAttrType = reflect.TypeOf((*UnmarshalerAttr)(nil)).Elem()
textUnmarshalerType = reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem()
unmarshalerType = reflect.TypeFor[Unmarshaler]()
unmarshalerAttrType = reflect.TypeFor[UnmarshalerAttr]()
textUnmarshalerType = reflect.TypeFor[encoding.TextUnmarshaler]()
)
// Unmarshal a single XML element into val.
@@ -360,10 +359,7 @@ func (p *Decoder) unmarshal(val reflect.Value, start *StartElement) error {
// Grow slice.
n := v.Len()
if n >= v.Cap() {
ncap := 2 * n
if ncap < 4 {
ncap = 4
}
ncap := max(2*n, 4)
new := reflect.MakeSlice(typ, n, ncap)
reflect.Copy(new, v)
v.Set(new)
+1 -8
View File
@@ -44,7 +44,7 @@ const (
var tinfoMap = make(map[reflect.Type]*typeInfo)
var tinfoLock sync.RWMutex
var nameType = reflect.TypeOf(Name{})
var nameType = reflect.TypeFor[Name]()
// getTypeInfo returns the typeInfo structure with details necessary
// for marshalling and unmarshalling typ.
@@ -258,13 +258,6 @@ func lookupXMLName(typ reflect.Type) (xmlname *fieldInfo) {
return nil
}
func min(a, b int) int {
if a <= b {
return a
}
return b
}
// addFieldInfo adds finfo to tinfo.fields if there are no
// conflicts, or if conflicts arise from previous fields that were
// obtained from deeper embedded structures than finfo. In the latter
+5 -5
View File
@@ -162,7 +162,7 @@ var liveProps = map[xml.Name]struct {
// TODO(nigeltao) merge props and allprop?
// Props returns the status of the properties named pnames for resource name.
// props returns the status of the properties named pnames for resource name.
//
// Each Propstat has a unique status and each property name will only be part
// of one Propstat element.
@@ -213,7 +213,7 @@ func props(ctx context.Context, fs FileSystem, ls LockSystem, name string, pname
return makePropstats(pstatOK, pstatNotFound), nil
}
// Propnames returns the property names defined for resource name.
// propnames returns the property names defined for resource name.
func propnames(ctx context.Context, fs FileSystem, ls LockSystem, name string) ([]xml.Name, error) {
f, err := fs.OpenFile(ctx, name, os.O_RDONLY, 0)
if err != nil {
@@ -246,7 +246,7 @@ func propnames(ctx context.Context, fs FileSystem, ls LockSystem, name string) (
return pnames, nil
}
// Allprop returns the properties defined for resource name and the properties
// allprop returns the properties defined for resource name and the properties
// named in include.
//
// Note that RFC 4918 defines 'allprop' to return the DAV: properties defined
@@ -272,7 +272,7 @@ func allprop(ctx context.Context, fs FileSystem, ls LockSystem, name string, inc
return props(ctx, fs, ls, name, pnames)
}
// Patch patches the properties of resource name. The return values are
// patch patches the properties of resource name. The return values are
// constrained in the same manner as DeadPropsHolder.Patch.
func patch(ctx context.Context, fs FileSystem, ls LockSystem, name string, patches []Proppatch) ([]Propstat, error) {
conflict := false
@@ -425,7 +425,7 @@ func findContentType(ctx context.Context, fs FileSystem, ls LockSystem, name str
}
ctype = http.DetectContentType(buf[:n])
// Rewind file.
_, err = f.Seek(0, os.SEEK_SET)
_, err = f.Seek(0, io.SeekStart)
return ctype, err
}
+59 -7
View File
@@ -3,6 +3,25 @@
// license that can be found in the LICENSE file.
// Package webdav provides a WebDAV server implementation.
//
// # Security Considerations
//
// This package is not hardened against malicious clients.
//
// The following misbehaviors are considered vulnerabilities
// and should be reported per the [Go Security Policy] if
// present:
//
// - Remote code execution.
// - Access of a file outside of the restricted tree
// defined by [Dir] (but see the Dir documentation for
// limitations).
//
// Other misbahaviors (crashes, excessive resource consumption,
// lack of isolation between clients, etc.) will be handled
// as ordinary bugs, not vulnerabilities.
//
// [Go Security Policy]: https://go.dev/security/policy
package webdav // import "golang.org/x/net/webdav"
import (
@@ -13,6 +32,7 @@ import (
"net/url"
"os"
"path"
"path/filepath"
"strings"
"time"
)
@@ -266,6 +286,9 @@ func (h *Handler) handlePut(w http.ResponseWriter, r *http.Request) (status int,
f, err := h.FileSystem.OpenFile(ctx, reqPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
if err != nil {
if os.IsNotExist(err) {
return http.StatusConflict, err
}
return http.StatusNotFound, err
}
_, copyErr := io.Copy(f, r.Body)
@@ -535,13 +558,14 @@ func (h *Handler) handlePropfind(w http.ResponseWriter, r *http.Request) (status
walkFn := func(reqPath string, info os.FileInfo, err error) error {
if err != nil {
return err
return handlePropfindError(err, info)
}
var pstats []Propstat
if pf.Propname != nil {
pnames, err := propnames(ctx, h.FileSystem, h.LockSystem, reqPath)
if err != nil {
return err
return handlePropfindError(err, info)
}
pstat := Propstat{Status: http.StatusOK}
for _, xmlname := range pnames {
@@ -554,7 +578,7 @@ func (h *Handler) handlePropfind(w http.ResponseWriter, r *http.Request) (status
pstats, err = props(ctx, h.FileSystem, h.LockSystem, reqPath, pf.Prop)
}
if err != nil {
return err
return handlePropfindError(err, info)
}
href := path.Join(h.Prefix, reqPath)
if href != "/" && info.IsDir() {
@@ -633,6 +657,33 @@ func makePropstatResponse(href string, pstats []Propstat) *response {
return &resp
}
func handlePropfindError(err error, info os.FileInfo) error {
var skipResp error = nil
if info != nil && info.IsDir() {
skipResp = filepath.SkipDir
}
if errors.Is(err, os.ErrPermission) {
// If the server cannot recurse into a directory because it is not allowed,
// then there is nothing more to say about it. Just skip sending anything.
return skipResp
}
if _, ok := err.(*os.PathError); ok {
// If the file is just bad, it couldn't be a proper WebDAV resource. Skip it.
return skipResp
}
// We need to be careful with other errors: there is no way to abort the xml stream
// part way through while returning a valid PROPFIND response. Returning only half
// the data would be misleading, but so would be returning results tainted by errors.
// The current behaviour by returning an error here leads to the stream being aborted,
// and the parent http server complaining about writing a spurious header. We should
// consider further enhancing this error handling to more gracefully fail, or perhaps
// buffer the entire response until we've walked the tree.
return err
}
const (
infiniteDepth = -1
invalidDepth = -2
@@ -642,10 +693,11 @@ const (
// infiniteDepth. Parsing any other string returns invalidDepth.
//
// Different WebDAV methods have further constraints on valid depths:
// - PROPFIND has no further restrictions, as per section 9.1.
// - COPY accepts only "0" or "infinity", as per section 9.8.3.
// - MOVE accepts only "infinity", as per section 9.9.2.
// - LOCK accepts only "0" or "infinity", as per section 9.10.3.
// - PROPFIND has no further restrictions, as per section 9.1.
// - COPY accepts only "0" or "infinity", as per section 9.8.3.
// - MOVE accepts only "infinity", as per section 9.9.2.
// - LOCK accepts only "0" or "infinity", as per section 9.10.3.
//
// These constraints are enforced by the handleXxx methods.
func parseDepth(s string) int {
switch s {
+1 -1
View File
@@ -113,7 +113,7 @@ func escape(s string) string {
return s
}
// Next returns the next token, if any, in the XML stream of d.
// next returns the next token, if any, in the XML stream of d.
// RFC 4918 requires to ignore comments, processing instructions
// and directives.
// http://www.webdav.org/specs/rfc4918.html#property_values
-3
View File
@@ -1,3 +0,0 @@
# This source code refers to The Go Authors for copyright purposes.
# The master list of authors is in the main Go distribution,
# visible at http://tip.golang.org/AUTHORS.
-3
View File
@@ -1,3 +0,0 @@
# This source code was written by the Go contributors.
# The master list of contributors is in the main Go distribution,
# visible at http://tip.golang.org/CONTRIBUTORS.
+2 -2
View File
@@ -1,4 +1,4 @@
Copyright (c) 2009 The Go Authors. All rights reserved.
Copyright 2009 The Go Authors.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
@@ -10,7 +10,7 @@ notice, this list of conditions and the following disclaimer.
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
* Neither the name of Google LLC nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
-30
View File
@@ -1,30 +0,0 @@
// Copyright 2020 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package unsafeheader contains header declarations for the Go runtime's
// slice and string implementations.
//
// This package allows x/sys to use types equivalent to
// reflect.SliceHeader and reflect.StringHeader without introducing
// a dependency on the (relatively heavy) "reflect" package.
package unsafeheader
import (
"unsafe"
)
// Slice is the runtime representation of a slice.
// It cannot be used safely or portably and its representation may change in a later release.
type Slice struct {
Data unsafe.Pointer
Len int
Cap int
}
// String is the runtime representation of a string.
// It cannot be used safely or portably and its representation may change in a later release.
type String struct {
Data unsafe.Pointer
Len int
}
+8
View File
@@ -0,0 +1,8 @@
// Copyright 2014 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
#include "textflag.h"
TEXT ·use(SB),NOSPLIT,$0
RET
@@ -2,28 +2,29 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build !gccgo
#include "textflag.h"
//
// System call support for 386, FreeBSD
// System call support for 386, Plan 9
//
// Just jump to package syscall's implementation for all these functions.
// The runtime may know about them.
TEXT ·Syscall(SB),NOSPLIT,$0-28
TEXT ·Syscall(SB),NOSPLIT,$0-32
JMP syscall·Syscall(SB)
TEXT ·Syscall6(SB),NOSPLIT,$0-40
TEXT ·Syscall6(SB),NOSPLIT,$0-44
JMP syscall·Syscall6(SB)
TEXT ·Syscall9(SB),NOSPLIT,$0-52
JMP syscall·Syscall9(SB)
TEXT ·RawSyscall(SB),NOSPLIT,$0-28
JMP syscall·RawSyscall(SB)
TEXT ·RawSyscall6(SB),NOSPLIT,$0-40
TEXT ·RawSyscall6(SB),NOSPLIT,$0-40
JMP syscall·RawSyscall6(SB)
TEXT ·seek(SB),NOSPLIT,$0-36
JMP syscall·seek(SB)
TEXT ·exit(SB),NOSPLIT,$4-4
JMP syscall·exit(SB)

Some files were not shown because too many files have changed in this diff Show More