Only reset grammar when Apply()ing.
Also add a Kong.Help() function for writing context-sensitive help.
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
<!-- markdownlint-disable MD013 MD033 -->
|
||||
<p align="center"><img width="90%" src="kong.png" /></p>
|
||||
|
||||
# Kong is a command-line parser for Go [](https://circleci.com/gh/alecthomas/kong)
|
||||
@@ -8,17 +9,20 @@
|
||||
1. [Help](#help)
|
||||
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. [Supported tags](#supported-tags)
|
||||
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. [`Help(HelpFunc)` - customising help](#helphelpfunc---customising-help)
|
||||
1. [`Hook(&field, HookFunc)` - callback hooks to execute when the command-line is parsed](#hookfield-hookfunc---callback-hooks-to-execute-when-the-command-line-is-parsed)
|
||||
1. [Other options](#other-options)
|
||||
|
||||
<!-- /MarkdownTOC -->
|
||||
|
||||
|
||||
## Introduction
|
||||
|
||||
Kong aims to support arbitrarily complex command-line structures with as little developer effort as possible.
|
||||
@@ -27,10 +31,8 @@ To achieve that, command-lines are expressed as Go types, with the structure and
|
||||
|
||||
For example, the following command-line:
|
||||
|
||||
```
|
||||
shell rm [-f] [-r] <paths> ...
|
||||
shell ls [<paths> ...]
|
||||
```
|
||||
shell rm [-f] [-r] <paths> ...
|
||||
shell ls [<paths> ...]
|
||||
|
||||
Can be represented by the following command-line structure:
|
||||
|
||||
@@ -63,59 +65,78 @@ Help is automatically generated. With no other arguments provided, help will dis
|
||||
|
||||
eg.
|
||||
|
||||
```
|
||||
$ shell --help
|
||||
usage: shell <command>
|
||||
$ shell --help
|
||||
usage: shell <command>
|
||||
|
||||
A shell-like example app.
|
||||
A shell-like example app.
|
||||
|
||||
Flags:
|
||||
--help Show context-sensitive help.
|
||||
--debug Debug mode.
|
||||
Flags:
|
||||
--help Show context-sensitive help.
|
||||
--debug Debug mode.
|
||||
|
||||
Commands:
|
||||
rm <paths> ...
|
||||
Remove files.
|
||||
Commands:
|
||||
rm <paths> ...
|
||||
Remove files.
|
||||
|
||||
ls [<paths> ...]
|
||||
List paths.
|
||||
```
|
||||
ls [<paths> ...]
|
||||
List paths.
|
||||
|
||||
If a command is provided, the help will show full detail on the command including all available flags.
|
||||
|
||||
eg.
|
||||
|
||||
```
|
||||
$ shell --help rm
|
||||
usage: shell rm <paths> ...
|
||||
$ shell --help rm
|
||||
usage: shell rm <paths> ...
|
||||
|
||||
Remove files.
|
||||
Remove files.
|
||||
|
||||
Arguments:
|
||||
<paths> ... Paths to remove.
|
||||
Arguments:
|
||||
<paths> ... Paths to remove.
|
||||
|
||||
Flags:
|
||||
--debug Debug mode.
|
||||
Flags:
|
||||
--debug Debug mode.
|
||||
|
||||
-f, --force Force removal.
|
||||
-r, --recursive Recursively remove files.
|
||||
```
|
||||
-f, --force Force removal.
|
||||
-r, --recursive Recursively remove files.
|
||||
|
||||
## Flags
|
||||
|
||||
Any 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.
|
||||
|
||||
```go
|
||||
type CLI struct {
|
||||
Flag string
|
||||
}
|
||||
```
|
||||
|
||||
## Commands and sub-commands
|
||||
|
||||
Kong supports arbitrarily nested commands and positional arguments. Nested structs tagged with `cmd` will be treated as commands.
|
||||
Sub-commands are specified by tagging a struct field with `cmd`. Kong supports arbitrarily nested commands.
|
||||
|
||||
Arguments can also optionally have children, in order to support commands like the following:
|
||||
eg. The following struct represents the CLI structure `command [--flag="str"] sub-command`.
|
||||
|
||||
```
|
||||
app rename <name> to <name>
|
||||
```go
|
||||
type CLI struct {
|
||||
Command struct {
|
||||
Flag string
|
||||
|
||||
SubCommand struct {
|
||||
} `cmd`
|
||||
} `cmd`
|
||||
}
|
||||
```
|
||||
|
||||
This is achieved by tagging a nested struct with `arg`, then including a positional argument field inside that struct with the same name. For example:
|
||||
## Branching positional arguments
|
||||
|
||||
In addition to sub-commands, structs can also be configured as branching positional arguments.
|
||||
|
||||
This is achieved by tagging an [unmapped](#mapper---customising-how-the-command-line-is-mapped-to-go-values) nested struct field with `arg`, then including a positional argument field inside that struct _with the same name_. For example, the following command structure:
|
||||
|
||||
app rename <name> to <name>
|
||||
|
||||
Can be represented with the following:
|
||||
|
||||
```go
|
||||
var CLI struct {
|
||||
@@ -131,8 +152,13 @@ var CLI struct {
|
||||
} `cmd`
|
||||
}
|
||||
```
|
||||
|
||||
This looks a little verbose in this contrived example, but typically this will not be the case.
|
||||
|
||||
## Terminating positional arguments
|
||||
|
||||
If a [mapped type](#mapper---customising-how-the-command-line-is-mapped-to-go-values) is tagged with `arg`,
|
||||
|
||||
## Supported tags
|
||||
|
||||
Tags can be in two forms:
|
||||
@@ -147,12 +173,12 @@ Both can coexist with standard Tag parsing.
|
||||
| `cmd` | If present, struct is a command. |
|
||||
| `arg` | If present, field is an argument. |
|
||||
| `env:"X"` | Specify envar to use for default value.
|
||||
| `type:"X"` | Specify named Mapper to use. |
|
||||
| `name:"X"` | Long name, for overriding field name. |
|
||||
| `help:"X"` | Help text. |
|
||||
| `type:"X"` | Specify named Mapper to use. |
|
||||
| `placeholder:"X"` | Placeholder text. |
|
||||
| `default:"X"` | Default value. |
|
||||
| `short:"X"` | Short name, if flag. |
|
||||
| `name:"X"` | Long name, for overriding field name. |
|
||||
| `required` | If present, flag/arg is required. |
|
||||
| `optional` | If present, flag/arg is optional. |
|
||||
| `hidden` | If present, flag is hidden. |
|
||||
@@ -163,7 +189,15 @@ Both can coexist with standard Tag parsing.
|
||||
|
||||
Each Kong parser can be configured via functional options passed to `New(cli interface{}, options...Option)`.
|
||||
|
||||
The full set of options can be found in [options.go](https://github.com/alecthomas/kong/blob/master/options.go).
|
||||
The full set of options can be found [here](https://godoc.org/github.com/alecthomas/kong#Option).
|
||||
|
||||
### `Name(help)` and `Description(help)` - set the application name description
|
||||
|
||||
Set the application name and/or description.
|
||||
|
||||
The name of the application will default to the binary name, but can be overridden with `Name(name)`.
|
||||
|
||||
As with all help in Kong, text will be wrapped to the terminal.
|
||||
|
||||
### `Configuration(loader, paths...)` - load defaults from configuration files
|
||||
|
||||
@@ -201,8 +235,7 @@ All builtin Go types (as well as a bunch of useful stdlib types like `time.Time`
|
||||
1. `NamedMapper(string, Mapper)` and using the tag key `type:"<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(interface{}, Mapper)`, passing in a pointer to a field of the grammar.
|
||||
|
||||
### `Help(HelpFunc)` - customising help
|
||||
|
||||
@@ -230,3 +263,7 @@ if CLI.Debug {
|
||||
```
|
||||
|
||||
But under some circumstances, hooks are the right choice.
|
||||
|
||||
### Other options
|
||||
|
||||
The full set of options can be found [here](https://godoc.org/github.com/alecthomas/kong#Option).
|
||||
|
||||
+34
-35
@@ -55,6 +55,8 @@ func (c *Context) Selected() *Node {
|
||||
// Trace path of "args" through the gammar tree.
|
||||
//
|
||||
// The returned Context will include a Path of all commands, arguments, positionals and flags.
|
||||
//
|
||||
// Note that this will not modify the target grammar. Call Apply() to do so.
|
||||
func Trace(k *Kong, args []string) (*Context, error) {
|
||||
c := &Context{
|
||||
App: k,
|
||||
@@ -62,17 +64,10 @@ func Trace(k *Kong, args []string) (*Context, error) {
|
||||
Path: []*Path{
|
||||
{App: k.Model, Flags: k.Model.Flags, Value: k.Model.Target},
|
||||
},
|
||||
}
|
||||
err := c.reset(&c.App.Model.Node)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
scan: Scan(args...),
|
||||
}
|
||||
c.Error = c.trace(&c.App.Model.Node)
|
||||
err = c.traceResolvers()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c, nil
|
||||
return c, c.traceResolvers()
|
||||
}
|
||||
|
||||
// Validate the current context.
|
||||
@@ -155,7 +150,6 @@ func (c *Context) FlagValue(flag *Flag) reflect.Value {
|
||||
|
||||
// Recursively reset values to defaults (as specified in the grammar) or the zero value.
|
||||
func (c *Context) reset(node *Node) error {
|
||||
c.scan = Scan(c.args...)
|
||||
for _, flag := range node.Flags {
|
||||
err := flag.Value.Reset()
|
||||
if err != nil {
|
||||
@@ -310,31 +304,6 @@ func (c *Context) trace(node *Node) (err error) { // nolint: gocyclo
|
||||
return nil
|
||||
}
|
||||
|
||||
// Apply traced context to the target grammar.
|
||||
func (c *Context) Apply() (string, error) {
|
||||
path := []string{}
|
||||
|
||||
for _, trace := range c.Path {
|
||||
switch {
|
||||
case trace.App != nil:
|
||||
case trace.Argument != nil:
|
||||
path = append(path, "<"+trace.Argument.Name+">")
|
||||
trace.Argument.Argument.Apply(trace.Value)
|
||||
case trace.Command != nil:
|
||||
path = append(path, trace.Command.Name)
|
||||
case trace.Flag != nil:
|
||||
trace.Flag.Value.Apply(trace.Value)
|
||||
case trace.Positional != nil:
|
||||
path = append(path, "<"+trace.Positional.Name+">")
|
||||
trace.Positional.Apply(trace.Value)
|
||||
default:
|
||||
panic("unsupported path ?!")
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(path, " "), nil
|
||||
}
|
||||
|
||||
// Walk through flags from existing nodes in the path.
|
||||
func (c *Context) traceResolvers() error {
|
||||
if len(c.App.resolvers) == 0 {
|
||||
@@ -370,6 +339,36 @@ func (c *Context) traceResolvers() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Apply traced context to the target grammar.
|
||||
func (c *Context) Apply() (string, error) {
|
||||
err := c.reset(&c.App.Model.Node)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
path := []string{}
|
||||
|
||||
for _, trace := range c.Path {
|
||||
switch {
|
||||
case trace.App != nil:
|
||||
case trace.Argument != nil:
|
||||
path = append(path, "<"+trace.Argument.Name+">")
|
||||
trace.Argument.Argument.Apply(trace.Value)
|
||||
case trace.Command != nil:
|
||||
path = append(path, trace.Command.Name)
|
||||
case trace.Flag != nil:
|
||||
trace.Flag.Value.Apply(trace.Value)
|
||||
case trace.Positional != nil:
|
||||
path = append(path, "<"+trace.Positional.Name+">")
|
||||
trace.Positional.Apply(trace.Value)
|
||||
default:
|
||||
panic("unsupported path ?!")
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(path, " "), nil
|
||||
}
|
||||
|
||||
func (c *Context) matchFlags(flags []*Flag, matcher func(f *Flag) bool) (err error) {
|
||||
defer catch(&err)
|
||||
token := c.scan.Peek()
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@ func TestParseHandlingBadBuild(t *testing.T) {
|
||||
}
|
||||
}()
|
||||
|
||||
Parse(&cli, ExitFunction(func(_ int) { panic("exiting") }))
|
||||
Parse(&cli, Exit(func(_ int) { panic("exiting") }))
|
||||
|
||||
require.Fail(t, "we were expecting a panic")
|
||||
}
|
||||
|
||||
+1
-1
@@ -38,7 +38,7 @@ func TestHelp(t *testing.T) {
|
||||
Name("test-app"),
|
||||
Description("A test app."),
|
||||
Writers(w, w),
|
||||
ExitFunction(func(int) {
|
||||
Exit(func(int) {
|
||||
exited = true
|
||||
panic(true) // Panic to fake "exit".
|
||||
}),
|
||||
|
||||
@@ -108,9 +108,15 @@ func (k *Kong) extraFlags() []*Flag {
|
||||
return []*Flag{helpFlag}
|
||||
}
|
||||
|
||||
// Trace parses the command-line, validating and collecting matching grammar nodes.
|
||||
func (k *Kong) Trace(args []string) (*Context, error) {
|
||||
return Trace(k, args)
|
||||
// Help writes help for the given args to the stdout io.Writer associated with this Kong.
|
||||
//
|
||||
// See Help() and Writers() for overriding the help function and stdout, respectively.
|
||||
func (k *Kong) Help(args []string) error {
|
||||
ctx, err := Trace(k, args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return k.help(ctx)
|
||||
}
|
||||
|
||||
// Parse arguments into target.
|
||||
@@ -119,7 +125,7 @@ func (k *Kong) Trace(args []string) (*Context, error) {
|
||||
// the command name while positional arguments are the argument name surrounded by "<argument>".
|
||||
func (k *Kong) Parse(args []string) (command string, err error) {
|
||||
defer catch(&err)
|
||||
ctx, err := k.Trace(args)
|
||||
ctx, err := Trace(k, args)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -165,13 +171,15 @@ func (k *Kong) applyHooks(ctx *Context) error {
|
||||
}
|
||||
|
||||
// Printf writes a message to Kong.Stdout with the application name prefixed.
|
||||
func (k *Kong) Printf(format string, args ...interface{}) {
|
||||
func (k *Kong) Printf(format string, args ...interface{}) *Kong {
|
||||
fmt.Fprintf(k.Stdout, 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{}) {
|
||||
func (k *Kong) Errorf(format string, args ...interface{}) *Kong {
|
||||
fmt.Fprintf(k.Stderr, k.Model.Name+": error: "+format, args...)
|
||||
return k
|
||||
}
|
||||
|
||||
// FatalIfErrorf terminates with an error message if err != nil.
|
||||
|
||||
+2
-2
@@ -10,7 +10,7 @@ import (
|
||||
func mustNew(t *testing.T, cli interface{}, options ...Option) *Kong {
|
||||
t.Helper()
|
||||
options = append([]Option{
|
||||
ExitFunction(func(int) {
|
||||
Exit(func(int) {
|
||||
t.Helper()
|
||||
t.Fatalf("unexpected exit()")
|
||||
}),
|
||||
@@ -322,7 +322,7 @@ func TestTraceErrorPartiallySucceeds(t *testing.T) {
|
||||
} `kong:"cmd"`
|
||||
}
|
||||
p := mustNew(t, &cli)
|
||||
ctx, err := p.Trace([]string{"one", "bad"})
|
||||
ctx, err := Trace(p, []string{"one", "bad"})
|
||||
require.NoError(t, err)
|
||||
require.Error(t, ctx.Error)
|
||||
require.Equal(t, []string{"one"}, ctx.Command())
|
||||
|
||||
+2
-2
@@ -12,8 +12,8 @@ import (
|
||||
// An Option applies optional changes to the Kong application.
|
||||
type Option func(k *Kong)
|
||||
|
||||
// ExitFunction overrides the function used to terminate. This is useful for testing or interactive use.
|
||||
func ExitFunction(exit func(int)) Option {
|
||||
// Exit overrides the function used to terminate. This is useful for testing or interactive use.
|
||||
func Exit(exit func(int)) Option {
|
||||
return func(k *Kong) { k.Exit = exit }
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ import (
|
||||
|
||||
func TestOptions(t *testing.T) {
|
||||
var cli struct{}
|
||||
p, err := New(&cli, Name("name"), Description("description"), Writers(nil, nil), ExitFunction(nil))
|
||||
p, err := New(&cli, Name("name"), Description("description"), Writers(nil, nil), Exit(nil))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "name", p.Model.Name)
|
||||
require.Equal(t, "description", p.Model.Help)
|
||||
|
||||
Reference in New Issue
Block a user