Improved documentation and help.

This commit is contained in:
Alec Thomas
2018-06-05 10:32:23 +10:00
parent 2afd4ba47b
commit 96fa9c43d5
15 changed files with 457 additions and 173 deletions
+182 -20
View File
@@ -1,6 +1,34 @@
# Kong is a command-line parser for Go [![CircleCI](https://circleci.com/gh/alecthomas/kong.svg?style=svg&circle-token=477fecac758383bf281453187269b913130f17d2)](https://circleci.com/gh/alecthomas/kong) # Kong is a command-line parser for Go [![CircleCI](https://circleci.com/gh/alecthomas/kong.svg?style=svg&circle-token=477fecac758383bf281453187269b913130f17d2)](https://circleci.com/gh/alecthomas/kong)
It parses a command-line into a struct. eg. <!-- MarkdownTOC -->
1. [Introduction](#introduction)
1. [Help](#help)
1. [Flags](#flags)
1. [Commands and sub-commands](#commands-and-sub-commands)
1. [Supported tags](#supported-tags)
1. [Configuring Kong](#configuring-kong)
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)
<!-- /MarkdownTOC -->
## Introduction
Kong aims to support arbitrarily complex command-line structures with as little developer effort as possible.
To achieve that, command-lines are expressed as Go types, with the structure and tags directing how the command line is mapped onto the struct.
For example, the following command-line:
```
shell rm [-f] [-r] <paths> ...
shell ls [<paths> ...]
```
Can be represented by the following command-line structure:
```go ```go
package main package main
@@ -9,15 +37,15 @@ import "github.com/alecthomas/kong"
var CLI struct { var CLI struct {
Rm struct { Rm struct {
Force bool `kong:"help='Force removal.'"` Force bool `help:"Force removal."`
Recursive bool `kong:"help='Recursively remove files.'"` Recursive bool `help:"Recursively remove files."`
Paths []string `kong:"help='Paths to remove.',type='path'"` Paths []string `arg help:"Paths to remove." type:"path"`
} `kong:"help='Remove files.'"` } `cmd help:"Remove files."`
Ls struct { Ls struct {
Paths []string `kong:"help='Paths to list.',type='path'"` Paths []string `arg optional help:"Paths to list." type:"path"`
} `kong:"help='List paths.'"` } `cmd help:"List paths."`
} }
func main() { func main() {
@@ -25,23 +53,157 @@ func main() {
} }
``` ```
## Decoders ## Help
Command-line arguments are mapped to Go values via the Decoder interface: Help is automatically generated. With no other arguments provided, help will display a full summary of all available commands.
eg.
```
$ shell --help
usage: shell [<flags>]
A shell-like example app.
Flags:
--help Show context-sensitive help.
--debug Debug mode.
Commands:
rm [<flags>] <paths> ...
Remove files.
ls [<flags>] [<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 [<flags>] <paths> ...
Remove files.
Arguments:
<paths> ... Paths to remove.
Flags:
--debug Debug mode.
-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.
## Commands and sub-commands
Kong supports arbitrarily nested commands and positional arguments. Nested structs tagged with `cmd` will be treated as commands.
Arguments can also optionally have children, in order to support commands like the following:
```
app rename <name> to <name>
```
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:
```go ```go
// A Decoder knows how to decode text into a Go value. var CLI struct {
type Decoder interface { Rename struct {
// Decode scan into target. Name struct {
// Name string `arg` // <-- NOTE: identical name to enclosing struct field.
// "ctx" contains context about the value being decoded that may be useful To struct {
// to some decoders. Name struct {
Decode(ctx *DecoderContext, scan *Scanner, target reflect.Value) error Name string `arg`
} `arg`
} `cmd`
} `arg`
} `cmd`
}
```
This looks a little verbose in this contrived example, but typically this will not be the case.
## Supported tags
Tags can be in two forms:
1. Standard Go syntax, eg. `kong:"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. |
| `type:"X"` | Specify named Mapper to use. |
| `help:"X"` | Help text. |
| `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. |
| `format:"X"` | Format for parsing input, if supported. |
| `sep:"X"` | Separator for sequences (defaults to ",") |
## Configuring Kong
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`.
### `*Mapper(...)` - customising how the command-line is mapped to Go values
Command-line arguments are mapped to Go values via the Mapper interface:
```go
// A Mapper knows how to map command-line input to Go.
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
} }
``` ```
All builtin Go types (as well as a bunch of useful stdlib types like `time.Time`) have decoders registered by default. Decoders for custom types can be added using `kong.RegisterDecoder(decoder)`. Decoders are mapped from fields in three ways: All builtin Go types (as well as a bunch of useful stdlib types like `time.Time`) have mapperss registered by default. Mappers for custom types can be added using `kong.??Mapper(...)` options. Mappers are applied to fields in four ways:
1. By registering a `kong.NamedDecoder` and using the key `type='<name>'`. 1. `NamedMapper(string, Mapper)` and using the tag key `type:"<name>"`.
2. By registering a `kong.KindDecoder` with a `reflect.Kind`. 2. `KindMapper(reflect.Kind, Mapper)`.
3. By registering a `kong.TypeDecoder` with a `reflect.Type`. 3. `TypeMapper(reflect.Type, Mapper)`.
4. `ValueMapper(interface{}, Mapper)`, passing in a pointer to a field of the grammar.
### `Help(HelpFunc)` - customising help
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.
### `Hook(&field, HookFunc)` - callback hooks to execute when the command-line is parsed
Hooks are callback functions that are bound to a node in the command-line and executed at parse time, before structural validation and assignment.
eg.
```go
app := kong.Must(&CLI, kong.Hook(&CLI.Debug, func(ctx *Context, path *Path) error {
log.SetLevel(DEBUG)
return nil
}))
```
Note: it is generally more advisable to use an imperative approach to building command-lines, eg.
```go
if CLI.Debug {
log.SetLevel(DEBUG)
}
```
But under some circumstances, hooks are the right choice.
+9 -8
View File
@@ -8,20 +8,21 @@ import (
"github.com/alecthomas/kong" "github.com/alecthomas/kong"
) )
// nolint: govet
var CLI struct { var CLI struct {
Debug bool `kong:"help='Debug mode.'"` Debug bool `help:"Debug mode."`
Output string `kong:"help='File to output to.',placeholder='FILE'"`
Rm struct { Rm struct {
Force bool `kong:"help='Force removal.'"` User string `help:"Run as user." short:"u"`
Recursive bool `kong:"help='Recursively remove files.'"` Force bool `help:"Force removal." short:"f"`
Recursive bool `help:"Recursively remove files." short:"r"`
Paths []string `kong:"arg,help='Paths to remove.',type='path'"` Paths []string `arg help:"Paths to remove." type:"path"`
} `kong:"cmd,help='Remove files.'"` } `cmd help:"Remove files."`
Ls struct { Ls struct {
Paths []string `kong:"help='Paths to list.',type='path'"` Paths []string `arg optional help:"Paths to list." type:"path"`
} `kong:"cmd,help='List paths.'"` } `cmd help:"List paths."`
} }
func main() { func main() {
+10 -11
View File
@@ -92,8 +92,8 @@ 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. // a positional argument is provided to the child, and move it to the branching argument field.
if tag.Arg { if tag.Arg {
if len(child.Positional) == 0 { if len(child.Positional) == 0 {
fail("positional branch %s.%s must have at least one child positional argument", fail("positional branch %s.%s must have at least one child positional argument named %q",
v.Type().Name(), ft.Name) v.Type().Name(), ft.Name, name)
} }
value := child.Positional[0] value := child.Positional[0]
@@ -122,14 +122,11 @@ func buildChild(k *Kong, node *Node, typ NodeType, v reflect.Value, ft reflect.S
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) {
mapper := k.registry.ForNamedType(tag.Type, fv) mapper := k.registry.ForNamedType(tag.Type, fv)
if mapper == nil { if mapper == nil {
fail("no mapper for %s.%s (of type %s)", v.Type(), ft.Name, ft.Type) fail("unsupported field type %s.%s (of type %s)", v.Type(), ft.Name, ft.Type)
} }
flag := !tag.Arg value := &Value{
value := Value{
Name: name, Name: name,
Flag: flag,
Help: tag.Help, Help: tag.Help,
Default: tag.Default, Default: tag.Default,
Mapper: mapper, Mapper: mapper,
@@ -137,22 +134,24 @@ func buildField(k *Kong, node *Node, v reflect.Value, ft reflect.StructField, fv
Value: fv, Value: fv,
// Flags are optional by default, and args are required by default. // Flags are optional by default, and args are required by default.
Required: (flag && tag.Required) || (tag.Arg && !tag.Optional), Required: (!tag.Arg && tag.Required) || (tag.Arg && !tag.Optional),
Format: tag.Format, Format: tag.Format,
} }
if tag.Arg { if tag.Arg {
node.Positional = append(node.Positional, &value) node.Positional = append(node.Positional, value)
} else { } else {
if seenFlags[value.Name] { if seenFlags[value.Name] {
fail("duplicate flag --%s", value.Name) fail("duplicate flag --%s", value.Name)
} }
seenFlags[value.Name] = true seenFlags[value.Name] = true
node.Flags = append(node.Flags, &Flag{ flag := &Flag{
Value: value, Value: value,
Short: tag.Short, Short: tag.Short,
PlaceHolder: tag.PlaceHolder, PlaceHolder: tag.PlaceHolder,
Env: tag.Env, Env: tag.Env,
}) }
value.Flag = flag
node.Flags = append(node.Flags, flag)
} }
} }
+14 -5
View File
@@ -56,14 +56,14 @@ func Trace(k *Kong, args []string) (*Context, error) {
App: k, App: k,
args: args, args: args,
Path: []*Path{ Path: []*Path{
{App: k.Application, Flags: k.Flags, Value: k.Target}, {App: k.Model, Flags: k.Model.Flags, Value: k.Model.Target},
}, },
} }
err := c.reset(&c.App.Node) err := c.reset(&c.App.Model.Node)
if err != nil { if err != nil {
return nil, err return nil, err
} }
c.Error = c.trace(&c.App.Node) c.Error = c.trace(&c.App.Model.Node)
return c, nil return c, nil
} }
@@ -93,6 +93,10 @@ func (c *Context) Validate() error {
} }
case path.Argument != nil: case path.Argument != nil:
value := path.Argument.Argument
if value.Required && !value.Set {
return fmt.Errorf("%s is required", path.Argument.Summary())
}
if err := checkMissingChildren(path.Argument); err != nil { if err := checkMissingChildren(path.Argument); err != nil {
return err return err
} }
@@ -341,7 +345,7 @@ func checkMissingFlags(flags []*Flag) error {
if !flag.Required || flag.Set { if !flag.Required || flag.Set {
continue continue
} }
missing = append(missing, flag.Name) missing = append(missing, flag.Summary())
} }
if len(missing) == 0 { if len(missing) == 0 {
return nil return nil
@@ -352,12 +356,17 @@ func checkMissingFlags(flags []*Flag) error {
func checkMissingChildren(node *Node) error { func checkMissingChildren(node *Node) error {
missing := []string{} missing := []string{}
for _, arg := range node.Positional {
if arg.Required && !arg.Set {
missing = append(missing, strconv.Quote(arg.Summary()))
}
}
for _, child := range node.Children { for _, child := range node.Children {
if child.Argument != nil { if child.Argument != nil {
if !child.Argument.Required { if !child.Argument.Required {
continue continue
} }
missing = append(missing, strconv.Quote("<"+child.Argument.Name+">")) missing = append(missing, strconv.Quote(child.Summary()))
} else { } else {
missing = append(missing, strconv.Quote(child.Name)) missing = append(missing, strconv.Quote(child.Name))
} }
+35 -14
View File
@@ -13,13 +13,14 @@ const (
defaultIndent = 2 defaultIndent = 2
) )
// PrintHelp is the default help printer.
func PrintHelp(ctx *Context) error { func PrintHelp(ctx *Context) error {
w := newHelpWriter(guessWidth(ctx.App.Stdout)) w := newHelpWriter(guessWidth(ctx.App.Stdout))
selected := ctx.Selected() selected := ctx.Selected()
if selected == nil { if selected == nil {
printApp(w, ctx.App.Application) printApp(w, ctx.App.Model)
} else { } else {
printCommand(w, ctx.App.Application, selected) printCommand(w, ctx.App.Model, selected)
} }
return w.Write(ctx.App.Stdout) return w.Write(ctx.App.Stdout)
} }
@@ -39,10 +40,15 @@ func printNodeDetail(w *helpWriter, node *Node) {
w.Print("") w.Print("")
w.Wrap(node.Help) w.Wrap(node.Help)
} }
if len(node.Flags) > 0 { if len(node.Positional) > 0 {
w.Printf("") w.Print("")
w.Printf("Flags:") w.Print("Arguments:")
writeFlags(w.Indent(), node.Flags) writePositionals(w.Indent(), node.Positional)
}
if flags := node.AllFlags(); len(flags) > 0 {
w.Print("")
w.Print("Flags:")
writeFlags(w.Indent(), flags)
} }
cmds := node.Leaves() cmds := node.Leaves()
if len(cmds) > 0 { if len(cmds) > 0 {
@@ -114,18 +120,33 @@ func (h *helpWriter) Wrap(text string) {
} }
} }
func writeFlags(w *helpWriter, flags []*Flag) { func writePositionals(w *helpWriter, args []*Positional) {
rows := [][2]string{}
for _, arg := range args {
rows = append(rows, [2]string{arg.Summary(), arg.Help})
}
writeTwoColumns(w, 2, rows)
}
func writeFlags(w *helpWriter, groups [][]*Flag) {
rows := [][2]string{} rows := [][2]string{}
haveShort := false haveShort := false
for _, flag := range flags { for _, group := range groups {
if flag.Short != 0 { for _, flag := range group {
haveShort = true if flag.Short != 0 {
break haveShort = true
break
}
} }
} }
for _, flag := range flags { for i, group := range groups {
if !flag.Hidden { if i > 0 {
rows = append(rows, [2]string{formatFlag(haveShort, flag), flag.Help}) rows = append(rows, [2]string{"", ""})
}
for _, flag := range group {
if !flag.Hidden {
rows = append(rows, [2]string{formatFlag(haveShort, flag), flag.Help})
}
} }
} }
writeTwoColumns(w, 2, rows) writeTwoColumns(w, 2, rows)
+56 -19
View File
@@ -9,29 +9,48 @@ import (
func TestHelp(t *testing.T) { func TestHelp(t *testing.T) {
var cli struct { var cli struct {
String string `help:"A string flag."` String string `help:"A string flag."`
Bool bool `help:"A bool flag with very long help that wraps a lot and is verbose and is really verbose."` Bool bool `help:"A bool flag with very long help that wraps a lot and is verbose and is really verbose."`
Required bool `required help:"A required flag."`
One struct { One struct {
Flag string `help:"Nested flag."` Flag string `help:"Nested flag."`
} `cmd help:"A subcommand."` } `cmd help:"A subcommand."`
Two struct { Two struct {
Flag string `help:"Nested flag under two."` Flag string `help:"Nested flag under two."`
RequiredTwo bool `required`
Three struct {
RequiredThree bool `required`
Three string `arg`
} `arg help:"Sub-sub-arg."`
Four struct {
} `cmd help:"Sub-sub-command."`
} `cmd help:"Another subcommand."` } `cmd help:"Another subcommand."`
} }
w := bytes.NewBuffer(nil) w := bytes.NewBuffer(nil)
exited := false exited := false
app := mustNew(t, &cli, app := mustNew(t, &cli,
Name("test-app"), Name("test-app"),
Description("A test app."), Description("A test app."),
Writers(w, w), Writers(w, w),
ExitFunction(func(int) { exited = true }), ExitFunction(func(int) {
exited = true
panic(true) // Panic to fake "exit".
}),
) )
_, err := app.Parse([]string{"--help"})
require.NoError(t, err) t.Run("Full", func(t *testing.T) {
require.True(t, exited) require.Panics(t, func() {
require.Equal(t, `usage: test-app [<flags>] _, err := app.Parse([]string{"--help"})
require.NoError(t, err)
})
require.True(t, exited)
t.Log(w.String())
require.Equal(t, `usage: test-app --required [<flags>]
A test app. A test app.
@@ -40,25 +59,43 @@ Flags:
--string=STRING A string flag. --string=STRING A string flag.
--bool A bool flag with very long help that wraps a lot and is --bool A bool flag with very long help that wraps a lot and is
verbose and is really verbose. verbose and is really verbose.
--required A required flag.
Commands: Commands:
one [<flags>] one --required [<flags>]
A subcommand. A subcommand.
two [<flags>] two <three> --required --required-two --required-three [<flags>]
Another subcommand. Sub-sub-arg.
two four --required --required-two [<flags>]
Sub-sub-command.
`, w.String()) `, w.String())
})
exited = false t.Run("Selected", func(t *testing.T) {
w.Truncate(0) exited = false
_, err = app.Parse([]string{"one", "--help"}) w.Truncate(0)
require.NoError(t, err) require.Panics(t, func() {
require.True(t, exited) _, err := app.Parse([]string{"two", "hello", "--help"})
require.Equal(t, `usage: test-app one [<flags>] require.NoError(t, err)
})
require.True(t, exited)
t.Log(w.String())
require.Equal(t, `usage: test-app two <three> --required --required-two --required-three [<flags>]
A subcommand. Sub-sub-arg.
Flags: Flags:
--flag=STRING Nested flag. --string=STRING A string flag.
--bool A bool flag with very long help that wraps a lot and is
verbose and is really verbose.
--required A required flag.
--flag=STRING Nested flag under two.
--required-two
--required-three
`, w.String()) `, w.String())
})
} }
+9 -9
View File
@@ -28,7 +28,7 @@ func Must(ast interface{}, options ...Option) *Kong {
// Kong is the main parser type. // Kong is the main parser type.
type Kong struct { type Kong struct {
// Grammar model. // Grammar model.
*Application Model *Application
// Termination function (defaults to os.Exit) // Termination function (defaults to os.Exit)
Exit func(int) Exit func(int)
@@ -36,7 +36,7 @@ type Kong struct {
Stdout io.Writer Stdout io.Writer
Stderr io.Writer Stderr io.Writer
before map[reflect.Value]HookFunction before map[reflect.Value]HookFunc
registry *Registry registry *Registry
noDefaultHelp bool noDefaultHelp bool
help func(*Context) error help func(*Context) error
@@ -50,7 +50,7 @@ func New(grammar interface{}, options ...Option) (*Kong, error) {
Exit: os.Exit, Exit: os.Exit,
Stdout: os.Stdout, Stdout: os.Stdout,
Stderr: os.Stderr, Stderr: os.Stderr,
before: map[reflect.Value]HookFunction{}, before: map[reflect.Value]HookFunc{},
registry: NewRegistry().RegisterDefaults(), registry: NewRegistry().RegisterDefaults(),
help: PrintHelp, help: PrintHelp,
} }
@@ -63,8 +63,8 @@ func New(grammar interface{}, options ...Option) (*Kong, error) {
if err != nil { if err != nil {
return k, err return k, err
} }
k.Application = model model.Name = filepath.Base(os.Args[0])
k.Name = filepath.Base(os.Args[0]) k.Model = model
for _, option := range options { for _, option := range options {
option(k) option(k)
@@ -80,14 +80,14 @@ func (k *Kong) extraFlags() []*Flag {
helpValue := false helpValue := false
value := reflect.ValueOf(&helpValue).Elem() value := reflect.ValueOf(&helpValue).Elem()
helpFlag := &Flag{ helpFlag := &Flag{
Value: Value{ Value: &Value{
Name: "help", Name: "help",
Help: "Show context-sensitive help.", Help: "Show context-sensitive help.",
Flag: true,
Value: value, Value: value,
Mapper: k.registry.ForValue(value), Mapper: k.registry.ForValue(value),
}, },
} }
helpFlag.Flag = helpFlag
hook := Hook(&helpValue, func(ctx *Context, path *Path) error { hook := Hook(&helpValue, func(ctx *Context, path *Path) error {
err := PrintHelp(ctx) err := PrintHelp(ctx)
if err != nil { if err != nil {
@@ -158,12 +158,12 @@ func (k *Kong) applyHooks(ctx *Context) error {
// Printf writes a message to Kong.Stdout with the application name prefixed. // 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{}) {
fmt.Fprintf(k.Stdout, k.Name+": "+format, args...) fmt.Fprintf(k.Stdout, k.Model.Name+": "+format, args...)
} }
// Errorf writes a message to Kong.Stderr with the application name prefixed. // 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{}) {
fmt.Fprintf(k.Stderr, k.Name+": "+format, args...) fmt.Fprintf(k.Stderr, k.Model.Name+": "+format, args...)
} }
// FatalIfError terminates with an error message if err != nil. // FatalIfError terminates with an error message if err != nil.
+53 -47
View File
@@ -2,6 +2,7 @@ package kong
import ( import (
"fmt" "fmt"
"math/bits"
"reflect" "reflect"
"strconv" "strconv"
"strings" "strings"
@@ -113,16 +114,16 @@ func (d *Registry) RegisterValue(ptr interface{}, mapper Mapper) *Registry {
} }
func (d *Registry) RegisterDefaults() *Registry { func (d *Registry) RegisterDefaults() *Registry {
return d.RegisterKind(reflect.Int, MapperFunc(intDecoder)). return d.RegisterKind(reflect.Int, intDecoder(bits.UintSize)).
RegisterKind(reflect.Int8, MapperFunc(intDecoder)). RegisterKind(reflect.Int8, intDecoder(8)).
RegisterKind(reflect.Int16, MapperFunc(intDecoder)). RegisterKind(reflect.Int16, intDecoder(16)).
RegisterKind(reflect.Int32, MapperFunc(intDecoder)). RegisterKind(reflect.Int32, intDecoder(32)).
RegisterKind(reflect.Int64, MapperFunc(intDecoder)). RegisterKind(reflect.Int64, intDecoder(64)).
RegisterKind(reflect.Uint, MapperFunc(uintDecoder)). RegisterKind(reflect.Uint, uintDecoder(64)).
RegisterKind(reflect.Uint8, MapperFunc(uintDecoder)). RegisterKind(reflect.Uint8, uintDecoder(bits.UintSize)).
RegisterKind(reflect.Uint16, MapperFunc(uintDecoder)). RegisterKind(reflect.Uint16, uintDecoder(16)).
RegisterKind(reflect.Uint32, MapperFunc(uintDecoder)). RegisterKind(reflect.Uint32, uintDecoder(32)).
RegisterKind(reflect.Uint64, MapperFunc(uintDecoder)). RegisterKind(reflect.Uint64, uintDecoder(64)).
RegisterKind(reflect.Float32, floatDecoder(32)). RegisterKind(reflect.Float32, floatDecoder(32)).
RegisterKind(reflect.Float64, floatDecoder(64)). RegisterKind(reflect.Float64, floatDecoder(64)).
RegisterKind(reflect.String, MapperFunc(func(ctx *DecoderContext, scan *Scanner, target reflect.Value) error { RegisterKind(reflect.String, MapperFunc(func(ctx *DecoderContext, scan *Scanner, target reflect.Value) error {
@@ -130,8 +131,8 @@ func (d *Registry) RegisterDefaults() *Registry {
return nil return nil
})). })).
RegisterKind(reflect.Bool, boolMapper{}). RegisterKind(reflect.Bool, boolMapper{}).
RegisterType(reflect.TypeOf(time.Time{}), MapperFunc(timeDecoder)). RegisterType(reflect.TypeOf(time.Time{}), timeDecoder()).
RegisterType(reflect.TypeOf(time.Duration(0)), MapperFunc(durationDecoder)). RegisterType(reflect.TypeOf(time.Duration(0)), durationDecoder()).
RegisterKind(reflect.Slice, sliceDecoder(d)) RegisterKind(reflect.Slice, sliceDecoder(d))
} }
@@ -143,46 +144,54 @@ func (boolMapper) Decode(ctx *DecoderContext, scan *Scanner, target reflect.Valu
} }
func (boolMapper) IsBool() bool { return true } func (boolMapper) IsBool() bool { return true }
func durationDecoder(ctx *DecoderContext, scan *Scanner, target reflect.Value) error { func durationDecoder() MapperFunc {
d, err := time.ParseDuration(scan.PopValue("duration")) return func(ctx *DecoderContext, scan *Scanner, target reflect.Value) error {
if err != nil { d, err := time.ParseDuration(scan.PopValue("duration"))
return err if err != nil {
return err
}
target.Set(reflect.ValueOf(d))
return nil
} }
target.Set(reflect.ValueOf(d))
return nil
} }
func timeDecoder(ctx *DecoderContext, scan *Scanner, target reflect.Value) error { func timeDecoder() MapperFunc {
fmt := time.RFC3339 return func(ctx *DecoderContext, scan *Scanner, target reflect.Value) error {
if ctx.Value.Format != "" { fmt := time.RFC3339
fmt = ctx.Value.Format if ctx.Value.Format != "" {
fmt = ctx.Value.Format
}
t, err := time.Parse(fmt, scan.PopValue("time"))
if err != nil {
return err
}
target.Set(reflect.ValueOf(t))
return nil
} }
t, err := time.Parse(fmt, scan.PopValue("time"))
if err != nil {
return err
}
target.Set(reflect.ValueOf(t))
return nil
} }
func intDecoder(ctx *DecoderContext, scan *Scanner, target reflect.Value) error { func intDecoder(bits int) MapperFunc {
value := scan.PopValue("int") return func(ctx *DecoderContext, scan *Scanner, target reflect.Value) error {
n, err := strconv.ParseInt(value, 10, 64) value := scan.PopValue("int")
if err != nil { n, err := strconv.ParseInt(value, 10, bits)
return fmt.Errorf("invalid int %q", value) if err != nil {
return fmt.Errorf("invalid int %q", value)
}
target.SetInt(n)
return nil
} }
target.SetInt(n)
return nil
} }
func uintDecoder(ctx *DecoderContext, scan *Scanner, target reflect.Value) error { func uintDecoder(bits int) MapperFunc {
value := scan.PopValue("uint") return func(ctx *DecoderContext, scan *Scanner, target reflect.Value) error {
n, err := strconv.ParseUint(value, 10, 64) value := scan.PopValue("uint")
if err != nil { n, err := strconv.ParseUint(value, 10, bits)
return fmt.Errorf("invalid uint %q", value) if err != nil {
return fmt.Errorf("invalid uint %q", value)
}
target.SetUint(n)
return nil
} }
target.SetUint(n)
return nil
} }
func floatDecoder(bits int) MapperFunc { func floatDecoder(bits int) MapperFunc {
@@ -200,12 +209,9 @@ func floatDecoder(bits int) MapperFunc {
func sliceDecoder(d *Registry) MapperFunc { func sliceDecoder(d *Registry) MapperFunc {
return func(ctx *DecoderContext, scan *Scanner, target reflect.Value) error { return func(ctx *DecoderContext, scan *Scanner, target reflect.Value) error {
el := target.Type().Elem() el := target.Type().Elem()
sep, ok := ctx.Value.Tag.Get("sep") sep := ctx.Value.Tag.Sep
if !ok {
sep = ","
}
var childScanner *Scanner var childScanner *Scanner
if ctx.Value.Flag { if ctx.Value.Flag != nil {
// If decoding a flag, we need an argument. // If decoding a flag, we need an argument.
childScanner = Scan(strings.Split(scan.PopValue("list"), sep)...) childScanner = Scan(strings.Split(scan.PopValue("list"), sep)...)
} else { } else {
+24
View File
@@ -3,6 +3,7 @@ package kong
import ( import (
"reflect" "reflect"
"testing" "testing"
"time"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@@ -40,3 +41,26 @@ func (testMooMapper) Decode(ctx *DecoderContext, scan *Scanner, target reflect.V
return nil return nil
} }
func (testMooMapper) IsBool() bool { return true } func (testMooMapper) IsBool() bool { return true }
func TestTimeMapper(t *testing.T) {
var cli struct {
Flag time.Time `format:"2006"`
}
k := mustNew(t, &cli)
_, err := k.Parse([]string{"--flag=2008"})
require.NoError(t, err)
expected, err := time.Parse("2006", "2008")
require.NoError(t, err)
require.Equal(t, 2008, expected.Year())
require.Equal(t, expected, cli.Flag)
}
func TestDurationMapper(t *testing.T) {
var cli struct {
Flag time.Duration
}
k := mustNew(t, &cli)
_, err := k.Parse([]string{"--flag=5s"})
require.NoError(t, err)
require.Equal(t, time.Second*5, cli.Flag)
}
+36 -20
View File
@@ -37,6 +37,16 @@ type Node struct {
Argument *Value // Populated when Type is ArgumentNode. Argument *Value // Populated when Type is ArgumentNode.
} }
func (n *Node) AllFlags() (out [][]*Flag) {
if n.Parent != nil {
out = append(out, n.Parent.AllFlags()...)
}
if len(n.Flags) > 0 {
out = append(out, n.Flags)
}
return
}
// Leaves returns the leaf commands/arguments under Node. // Leaves returns the leaf commands/arguments under Node.
func (n *Node) Leaves() (out []*Node) { func (n *Node) Leaves() (out []*Node) {
var walk func(n *Node) var walk func(n *Node)
@@ -70,21 +80,12 @@ func (n *Node) Depth() int {
// Summary help string for the node. // Summary help string for the node.
func (n *Node) Summary() string { func (n *Node) Summary() string {
summary := n.Path() summary := n.Path()
if n.Type == ArgumentNode {
summary = "<" + summary + ">"
}
if flags := n.FlagSummary(); flags != "" { if flags := n.FlagSummary(); flags != "" {
summary += " " + flags summary += " " + flags
} }
args := []string{} args := []string{}
for _, arg := range n.Positional { for _, arg := range n.Positional {
if arg.Required { args = append(args, arg.Summary())
argText := "<" + arg.Name + ">"
if arg.IsCumulative() {
argText += " ..."
}
args = append(args, argText)
}
} }
if len(args) != 0 { if len(args) != 0 {
summary += " " + strings.Join(args, " ") summary += " " + strings.Join(args, " ")
@@ -96,13 +97,11 @@ func (n *Node) Summary() string {
func (n *Node) FlagSummary() string { func (n *Node) FlagSummary() string {
required := []string{} required := []string{}
count := 0 count := 0
for _, flag := range n.Flags { for _, group := range n.AllFlags() {
count++ for _, flag := range group {
if flag.Required { count++
if flag.IsBool() { if flag.Required {
required = append(required, fmt.Sprintf("--%s", flag.Name)) required = append(required, flag.Summary())
} else {
required = append(required, fmt.Sprintf("--%s=%s", flag.Name, flag.FormatPlaceHolder()))
} }
} }
} }
@@ -128,7 +127,7 @@ func (n *Node) Path() (out string) {
// A Value is either a flag or a variable positional argument. // A Value is either a flag or a variable positional argument.
type Value struct { type Value struct {
Flag bool // True if flag, false if positional argument. Flag *Flag
Name string Name string
Help string Help string
Default string Default string
@@ -136,11 +135,28 @@ type Value struct {
Tag *Tag Tag *Tag
Value reflect.Value Value reflect.Value
Required bool Required bool
Set bool // Used with Required to test if a value has been given. Set bool // Set to true when this value is set through some mechanism.
Format string // Formatting directive, if applicable. Format string // Formatting directive, if applicable.
Position int // Position (for positional arguments). Position int // Position (for positional arguments).
} }
func (v *Value) Summary() string {
if v.Flag != nil {
if v.IsBool() {
return fmt.Sprintf("--%s", v.Name)
}
return fmt.Sprintf("--%s=%s", v.Name, v.Flag.FormatPlaceHolder())
}
argText := "<" + v.Name + ">"
if v.IsCumulative() {
argText += " ..."
}
if !v.Required {
argText = "[" + argText + "]"
}
return argText
}
func (v *Value) IsCumulative() bool { func (v *Value) IsCumulative() bool {
return v.Value.Kind() == reflect.Slice return v.Value.Kind() == reflect.Slice
} }
@@ -184,7 +200,7 @@ func (v *Value) Reset() error {
type Positional = Value type Positional = Value
type Flag struct { type Flag struct {
Value *Value
PlaceHolder string PlaceHolder string
Env string Env string
Short rune Short rune
+1 -1
View File
@@ -20,7 +20,7 @@ func TestModelApplicationCommands(t *testing.T) {
} }
p := mustNew(t, &cli) p := mustNew(t, &cli)
actual := []string{} actual := []string{}
for _, cmd := range p.Leaves() { for _, cmd := range p.Model.Leaves() {
actual = append(actual, cmd.Path()) actual = append(actual, cmd.Path())
} }
require.Equal(t, []string{"one two", "one three <four>"}, actual) require.Equal(t, []string{"one two", "one three <four>"}, actual)
+7 -7
View File
@@ -26,8 +26,8 @@ func NoDefaultHelp() Option {
// Name overrides the application name. // Name overrides the application name.
func Name(name string) Option { func Name(name string) Option {
return func(k *Kong) { return func(k *Kong) {
if k.Application != nil { if k.Model != nil {
k.Name = name k.Model.Name = name
} }
} }
} }
@@ -55,8 +55,8 @@ func NamedMapper(name string, mapper Mapper) Option {
// Description sets the application description. // Description sets the application description.
func Description(description string) Option { func Description(description string) Option {
return func(k *Kong) { return func(k *Kong) {
if k.Application != nil { if k.Model != nil {
k.Help = description k.Model.Help = description
} }
} }
} }
@@ -69,13 +69,13 @@ func Writers(stdout, stderr io.Writer) Option {
} }
} }
// HookFunction is a callback tied to a field of the grammar, called before a value is applied. // HookFunc is a callback tied to a field of the grammar, called before a value is applied.
type HookFunction func(ctx *Context, path *Path) error type HookFunc func(ctx *Context, path *Path) error
// Hook to aply before a command, flag or positional argument is encountered. // Hook to aply before a command, flag or positional argument is encountered.
// //
// "ptr" is a pointer to a field of the grammar. // "ptr" is a pointer to a field of the grammar.
func Hook(ptr interface{}, hook HookFunction) Option { func Hook(ptr interface{}, hook HookFunc) Option {
key := reflect.ValueOf(ptr) key := reflect.ValueOf(ptr)
if key.Kind() != reflect.Ptr { if key.Kind() != reflect.Ptr {
panic("expected a pointer") panic("expected a pointer")
+2 -2
View File
@@ -10,8 +10,8 @@ func TestOptions(t *testing.T) {
var cli struct{} 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), ExitFunction(nil))
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, "name", p.Name) require.Equal(t, "name", p.Model.Name)
require.Equal(t, "description", p.Help) require.Equal(t, "description", p.Model.Help)
require.Nil(t, p.Stdout) require.Nil(t, p.Stdout)
require.Nil(t, p.Stderr) require.Nil(t, p.Stderr)
require.Nil(t, p.Exit) require.Nil(t, p.Exit)
+18 -9
View File
@@ -21,6 +21,7 @@ type Tag struct {
Env string Env string
Short rune Short rune
Hidden bool Hidden bool
Sep string
// Storage for all tag keys for arbitrary lookups. // Storage for all tag keys for arbitrary lookups.
items map[string]string items map[string]string
@@ -109,24 +110,32 @@ func getTagInfo(ft reflect.StructField) (string, tagChars) {
func parseTag(fv reflect.Value, ft reflect.StructField) *Tag { func parseTag(fv reflect.Value, ft reflect.StructField) *Tag {
s, chars := getTagInfo(ft) s, chars := getTagInfo(ft)
t := &Tag{ t := &Tag{
items: map[string]string{}, items: parseTagItems(s, chars),
} }
if s == "" {
return t
}
t.items = parseTagItems(s, chars)
t.Cmd = t.Has("cmd") t.Cmd = t.Has("cmd")
t.Arg = t.Has("arg") t.Arg = t.Has("arg")
t.Required = t.Has("required") required := t.Has("required")
t.Optional = t.Has("optional") optional := t.Has("optional")
if required && optional {
fail("can't specify both required and optional")
}
t.Required = required
t.Optional = optional
t.Default, _ = t.Get("default") t.Default, _ = t.Get("default")
t.Help, _ = t.Get("help") t.Help, _ = t.Get("help")
t.Type, _ = t.Get("type") t.Type, _ = t.Get("type")
t.Env, _ = t.Get("env") t.Env, _ = t.Get("env")
t.Short, _ = t.GetRune("short") t.Short, _ = t.GetRune("short")
t.Hidden = t.Has("hidden") t.Hidden = t.Has("hidden")
t.Format, _ = t.Get("format")
t.Sep, _ = t.Get("sep")
if t.Sep == "" {
if t.Cmd || t.Arg {
t.Sep = " "
} else {
t.Sep = ","
}
}
t.PlaceHolder, _ = t.Get("placeholder") t.PlaceHolder, _ = t.Get("placeholder")
if t.PlaceHolder == "" { if t.PlaceHolder == "" {
+1 -1
View File
@@ -96,7 +96,7 @@ func TestBareTagsWithJsonTag(t *testing.T) {
func TestManySeps(t *testing.T) { func TestManySeps(t *testing.T) {
var cli struct { var cli struct {
Arg string `arg optional default:"hi"` Arg string `arg optional default:"hi"`
} }
p := mustNew(t, &cli) p := mustNew(t, &cli)