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)
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
package main
@@ -9,15 +37,15 @@ import "github.com/alecthomas/kong"
var CLI struct {
Rm struct {
Force bool `kong:"help='Force removal.'"`
Recursive bool `kong:"help='Recursively remove files.'"`
Force bool `help:"Force removal."`
Recursive bool `help:"Recursively remove files."`
Paths []string `kong:"help='Paths to remove.',type='path'"`
} `kong:"help='Remove files.'"`
Paths []string `arg help:"Paths to remove." type:"path"`
} `cmd help:"Remove files."`
Ls struct {
Paths []string `kong:"help='Paths to list.',type='path'"`
} `kong:"help='List paths.'"`
Paths []string `arg optional help:"Paths to list." type:"path"`
} `cmd help:"List paths."`
}
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
// A Decoder knows how to decode text into a Go value.
type Decoder interface {
// Decode scan into target.
//
// "ctx" contains context about the value being decoded that may be useful
// to some decoders.
Decode(ctx *DecoderContext, scan *Scanner, target reflect.Value) error
var CLI struct {
Rename struct {
Name struct {
Name string `arg` // <-- NOTE: identical name to enclosing struct field.
To struct {
Name struct {
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>'`.
2. By registering a `kong.KindDecoder` with a `reflect.Kind`.
3. By registering a `kong.TypeDecoder` with a `reflect.Type`.
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.
### `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"
)
// nolint: govet
var CLI struct {
Debug bool `kong:"help='Debug mode.'"`
Output string `kong:"help='File to output to.',placeholder='FILE'"`
Debug bool `help:"Debug mode."`
Rm struct {
Force bool `kong:"help='Force removal.'"`
Recursive bool `kong:"help='Recursively remove files.'"`
User string `help:"Run as user." short:"u"`
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'"`
} `kong:"cmd,help='Remove files.'"`
Paths []string `arg help:"Paths to remove." type:"path"`
} `cmd help:"Remove files."`
Ls struct {
Paths []string `kong:"help='Paths to list.',type='path'"`
} `kong:"cmd,help='List paths.'"`
Paths []string `arg optional help:"Paths to list." type:"path"`
} `cmd help:"List paths."`
}
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.
if tag.Arg {
if len(child.Positional) == 0 {
fail("positional branch %s.%s must have at least one child positional argument",
v.Type().Name(), ft.Name)
fail("positional branch %s.%s must have at least one child positional argument named %q",
v.Type().Name(), ft.Name, name)
}
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) {
mapper := k.registry.ForNamedType(tag.Type, fv)
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,
Flag: flag,
Help: tag.Help,
Default: tag.Default,
Mapper: mapper,
@@ -137,22 +134,24 @@ func buildField(k *Kong, node *Node, v reflect.Value, ft reflect.StructField, fv
Value: fv,
// 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,
}
if tag.Arg {
node.Positional = append(node.Positional, &value)
node.Positional = append(node.Positional, value)
} else {
if seenFlags[value.Name] {
fail("duplicate flag --%s", value.Name)
}
seenFlags[value.Name] = true
node.Flags = append(node.Flags, &Flag{
flag := &Flag{
Value: value,
Short: tag.Short,
PlaceHolder: tag.PlaceHolder,
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,
args: args,
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 {
return nil, err
}
c.Error = c.trace(&c.App.Node)
c.Error = c.trace(&c.App.Model.Node)
return c, nil
}
@@ -93,6 +93,10 @@ func (c *Context) Validate() error {
}
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 {
return err
}
@@ -341,7 +345,7 @@ func checkMissingFlags(flags []*Flag) error {
if !flag.Required || flag.Set {
continue
}
missing = append(missing, flag.Name)
missing = append(missing, flag.Summary())
}
if len(missing) == 0 {
return nil
@@ -352,12 +356,17 @@ func checkMissingFlags(flags []*Flag) error {
func checkMissingChildren(node *Node) error {
missing := []string{}
for _, arg := range node.Positional {
if arg.Required && !arg.Set {
missing = append(missing, strconv.Quote(arg.Summary()))
}
}
for _, child := range node.Children {
if child.Argument != nil {
if !child.Argument.Required {
continue
}
missing = append(missing, strconv.Quote("<"+child.Argument.Name+">"))
missing = append(missing, strconv.Quote(child.Summary()))
} else {
missing = append(missing, strconv.Quote(child.Name))
}
+35 -14
View File
@@ -13,13 +13,14 @@ const (
defaultIndent = 2
)
// PrintHelp is the default help printer.
func PrintHelp(ctx *Context) error {
w := newHelpWriter(guessWidth(ctx.App.Stdout))
selected := ctx.Selected()
if selected == nil {
printApp(w, ctx.App.Application)
printApp(w, ctx.App.Model)
} else {
printCommand(w, ctx.App.Application, selected)
printCommand(w, ctx.App.Model, selected)
}
return w.Write(ctx.App.Stdout)
}
@@ -39,10 +40,15 @@ func printNodeDetail(w *helpWriter, node *Node) {
w.Print("")
w.Wrap(node.Help)
}
if len(node.Flags) > 0 {
w.Printf("")
w.Printf("Flags:")
writeFlags(w.Indent(), node.Flags)
if len(node.Positional) > 0 {
w.Print("")
w.Print("Arguments:")
writePositionals(w.Indent(), node.Positional)
}
if flags := node.AllFlags(); len(flags) > 0 {
w.Print("")
w.Print("Flags:")
writeFlags(w.Indent(), flags)
}
cmds := node.Leaves()
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{}
haveShort := false
for _, flag := range flags {
if flag.Short != 0 {
haveShort = true
break
for _, group := range groups {
for _, flag := range group {
if flag.Short != 0 {
haveShort = true
break
}
}
}
for _, flag := range flags {
if !flag.Hidden {
rows = append(rows, [2]string{formatFlag(haveShort, flag), flag.Help})
for i, group := range groups {
if i > 0 {
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)
+56 -19
View File
@@ -9,29 +9,48 @@ import (
func TestHelp(t *testing.T) {
var cli struct {
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."`
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."`
Required bool `required help:"A required flag."`
One struct {
Flag string `help:"Nested flag."`
} `cmd help:"A subcommand."`
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."`
}
w := bytes.NewBuffer(nil)
exited := false
app := mustNew(t, &cli,
Name("test-app"),
Description("A test app."),
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)
require.True(t, exited)
require.Equal(t, `usage: test-app [<flags>]
t.Run("Full", func(t *testing.T) {
require.Panics(t, func() {
_, 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.
@@ -40,25 +59,43 @@ Flags:
--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.
Commands:
one [<flags>]
one --required [<flags>]
A subcommand.
two [<flags>]
Another subcommand.
two <three> --required --required-two --required-three [<flags>]
Sub-sub-arg.
two four --required --required-two [<flags>]
Sub-sub-command.
`, w.String())
})
exited = false
w.Truncate(0)
_, err = app.Parse([]string{"one", "--help"})
require.NoError(t, err)
require.True(t, exited)
require.Equal(t, `usage: test-app one [<flags>]
t.Run("Selected", func(t *testing.T) {
exited = false
w.Truncate(0)
require.Panics(t, func() {
_, err := app.Parse([]string{"two", "hello", "--help"})
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:
--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())
})
}
+9 -9
View File
@@ -28,7 +28,7 @@ func Must(ast interface{}, options ...Option) *Kong {
// Kong is the main parser type.
type Kong struct {
// Grammar model.
*Application
Model *Application
// Termination function (defaults to os.Exit)
Exit func(int)
@@ -36,7 +36,7 @@ type Kong struct {
Stdout io.Writer
Stderr io.Writer
before map[reflect.Value]HookFunction
before map[reflect.Value]HookFunc
registry *Registry
noDefaultHelp bool
help func(*Context) error
@@ -50,7 +50,7 @@ func New(grammar interface{}, options ...Option) (*Kong, error) {
Exit: os.Exit,
Stdout: os.Stdout,
Stderr: os.Stderr,
before: map[reflect.Value]HookFunction{},
before: map[reflect.Value]HookFunc{},
registry: NewRegistry().RegisterDefaults(),
help: PrintHelp,
}
@@ -63,8 +63,8 @@ func New(grammar interface{}, options ...Option) (*Kong, error) {
if err != nil {
return k, err
}
k.Application = model
k.Name = filepath.Base(os.Args[0])
model.Name = filepath.Base(os.Args[0])
k.Model = model
for _, option := range options {
option(k)
@@ -80,14 +80,14 @@ func (k *Kong) extraFlags() []*Flag {
helpValue := false
value := reflect.ValueOf(&helpValue).Elem()
helpFlag := &Flag{
Value: Value{
Value: &Value{
Name: "help",
Help: "Show context-sensitive help.",
Flag: true,
Value: value,
Mapper: k.registry.ForValue(value),
},
}
helpFlag.Flag = helpFlag
hook := Hook(&helpValue, func(ctx *Context, path *Path) error {
err := PrintHelp(ctx)
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.
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.
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.
+53 -47
View File
@@ -2,6 +2,7 @@ package kong
import (
"fmt"
"math/bits"
"reflect"
"strconv"
"strings"
@@ -113,16 +114,16 @@ func (d *Registry) RegisterValue(ptr interface{}, mapper Mapper) *Registry {
}
func (d *Registry) RegisterDefaults() *Registry {
return d.RegisterKind(reflect.Int, MapperFunc(intDecoder)).
RegisterKind(reflect.Int8, MapperFunc(intDecoder)).
RegisterKind(reflect.Int16, MapperFunc(intDecoder)).
RegisterKind(reflect.Int32, MapperFunc(intDecoder)).
RegisterKind(reflect.Int64, MapperFunc(intDecoder)).
RegisterKind(reflect.Uint, MapperFunc(uintDecoder)).
RegisterKind(reflect.Uint8, MapperFunc(uintDecoder)).
RegisterKind(reflect.Uint16, MapperFunc(uintDecoder)).
RegisterKind(reflect.Uint32, MapperFunc(uintDecoder)).
RegisterKind(reflect.Uint64, MapperFunc(uintDecoder)).
return d.RegisterKind(reflect.Int, intDecoder(bits.UintSize)).
RegisterKind(reflect.Int8, intDecoder(8)).
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.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 *DecoderContext, scan *Scanner, target reflect.Value) error {
@@ -130,8 +131,8 @@ func (d *Registry) RegisterDefaults() *Registry {
return nil
})).
RegisterKind(reflect.Bool, boolMapper{}).
RegisterType(reflect.TypeOf(time.Time{}), MapperFunc(timeDecoder)).
RegisterType(reflect.TypeOf(time.Duration(0)), MapperFunc(durationDecoder)).
RegisterType(reflect.TypeOf(time.Time{}), timeDecoder()).
RegisterType(reflect.TypeOf(time.Duration(0)), durationDecoder()).
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 durationDecoder(ctx *DecoderContext, scan *Scanner, target reflect.Value) error {
d, err := time.ParseDuration(scan.PopValue("duration"))
if err != nil {
return err
func durationDecoder() MapperFunc {
return func(ctx *DecoderContext, scan *Scanner, target reflect.Value) error {
d, err := time.ParseDuration(scan.PopValue("duration"))
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 {
fmt := time.RFC3339
if ctx.Value.Format != "" {
fmt = ctx.Value.Format
func timeDecoder() MapperFunc {
return func(ctx *DecoderContext, scan *Scanner, target reflect.Value) error {
fmt := time.RFC3339
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 {
value := scan.PopValue("int")
n, err := strconv.ParseInt(value, 10, 64)
if err != nil {
return fmt.Errorf("invalid int %q", value)
func intDecoder(bits int) MapperFunc {
return func(ctx *DecoderContext, scan *Scanner, target reflect.Value) error {
value := scan.PopValue("int")
n, err := strconv.ParseInt(value, 10, bits)
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 {
value := scan.PopValue("uint")
n, err := strconv.ParseUint(value, 10, 64)
if err != nil {
return fmt.Errorf("invalid uint %q", value)
func uintDecoder(bits int) MapperFunc {
return func(ctx *DecoderContext, scan *Scanner, target reflect.Value) error {
value := scan.PopValue("uint")
n, err := strconv.ParseUint(value, 10, bits)
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 {
@@ -200,12 +209,9 @@ func floatDecoder(bits int) MapperFunc {
func sliceDecoder(d *Registry) MapperFunc {
return func(ctx *DecoderContext, scan *Scanner, target reflect.Value) error {
el := target.Type().Elem()
sep, ok := ctx.Value.Tag.Get("sep")
if !ok {
sep = ","
}
sep := ctx.Value.Tag.Sep
var childScanner *Scanner
if ctx.Value.Flag {
if ctx.Value.Flag != nil {
// If decoding a flag, we need an argument.
childScanner = Scan(strings.Split(scan.PopValue("list"), sep)...)
} else {
+24
View File
@@ -3,6 +3,7 @@ package kong
import (
"reflect"
"testing"
"time"
"github.com/stretchr/testify/require"
)
@@ -40,3 +41,26 @@ func (testMooMapper) Decode(ctx *DecoderContext, scan *Scanner, target reflect.V
return nil
}
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.
}
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.
func (n *Node) Leaves() (out []*Node) {
var walk func(n *Node)
@@ -70,21 +80,12 @@ func (n *Node) Depth() int {
// Summary help string for the node.
func (n *Node) Summary() string {
summary := n.Path()
if n.Type == ArgumentNode {
summary = "<" + summary + ">"
}
if flags := n.FlagSummary(); flags != "" {
summary += " " + flags
}
args := []string{}
for _, arg := range n.Positional {
if arg.Required {
argText := "<" + arg.Name + ">"
if arg.IsCumulative() {
argText += " ..."
}
args = append(args, argText)
}
args = append(args, arg.Summary())
}
if len(args) != 0 {
summary += " " + strings.Join(args, " ")
@@ -96,13 +97,11 @@ func (n *Node) Summary() string {
func (n *Node) FlagSummary() string {
required := []string{}
count := 0
for _, flag := range n.Flags {
count++
if flag.Required {
if flag.IsBool() {
required = append(required, fmt.Sprintf("--%s", flag.Name))
} else {
required = append(required, fmt.Sprintf("--%s=%s", flag.Name, flag.FormatPlaceHolder()))
for _, group := range n.AllFlags() {
for _, flag := range group {
count++
if flag.Required {
required = append(required, flag.Summary())
}
}
}
@@ -128,7 +127,7 @@ func (n *Node) Path() (out string) {
// A Value is either a flag or a variable positional argument.
type Value struct {
Flag bool // True if flag, false if positional argument.
Flag *Flag
Name string
Help string
Default string
@@ -136,11 +135,28 @@ type Value struct {
Tag *Tag
Value reflect.Value
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.
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 {
return v.Value.Kind() == reflect.Slice
}
@@ -184,7 +200,7 @@ func (v *Value) Reset() error {
type Positional = Value
type Flag struct {
Value
*Value
PlaceHolder string
Env string
Short rune
+1 -1
View File
@@ -20,7 +20,7 @@ func TestModelApplicationCommands(t *testing.T) {
}
p := mustNew(t, &cli)
actual := []string{}
for _, cmd := range p.Leaves() {
for _, cmd := range p.Model.Leaves() {
actual = append(actual, cmd.Path())
}
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.
func Name(name string) Option {
return func(k *Kong) {
if k.Application != nil {
k.Name = name
if k.Model != nil {
k.Model.Name = name
}
}
}
@@ -55,8 +55,8 @@ func NamedMapper(name string, mapper Mapper) Option {
// Description sets the application description.
func Description(description string) Option {
return func(k *Kong) {
if k.Application != nil {
k.Help = description
if k.Model != nil {
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.
type HookFunction func(ctx *Context, path *Path) error
// HookFunc is a callback tied to a field of the grammar, called before a value is applied.
type HookFunc func(ctx *Context, path *Path) error
// Hook to aply before a command, flag or positional argument is encountered.
//
// "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)
if key.Kind() != reflect.Ptr {
panic("expected a pointer")
+2 -2
View File
@@ -10,8 +10,8 @@ func TestOptions(t *testing.T) {
var cli struct{}
p, err := New(&cli, Name("name"), Description("description"), Writers(nil, nil), ExitFunction(nil))
require.NoError(t, err)
require.Equal(t, "name", p.Name)
require.Equal(t, "description", p.Help)
require.Equal(t, "name", p.Model.Name)
require.Equal(t, "description", p.Model.Help)
require.Nil(t, p.Stdout)
require.Nil(t, p.Stderr)
require.Nil(t, p.Exit)
+18 -9
View File
@@ -21,6 +21,7 @@ type Tag struct {
Env string
Short rune
Hidden bool
Sep string
// Storage for all tag keys for arbitrary lookups.
items map[string]string
@@ -109,24 +110,32 @@ func getTagInfo(ft reflect.StructField) (string, tagChars) {
func parseTag(fv reflect.Value, ft reflect.StructField) *Tag {
s, chars := getTagInfo(ft)
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.Arg = t.Has("arg")
t.Required = t.Has("required")
t.Optional = t.Has("optional")
required := t.Has("required")
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.Help, _ = t.Get("help")
t.Type, _ = t.Get("type")
t.Env, _ = t.Get("env")
t.Short, _ = t.GetRune("short")
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")
if t.PlaceHolder == "" {
+1 -1
View File
@@ -96,7 +96,7 @@ func TestBareTagsWithJsonTag(t *testing.T) {
func TestManySeps(t *testing.T) {
var cli struct {
Arg string `arg optional default:"hi"`
Arg string `arg optional default:"hi"`
}
p := mustNew(t, &cli)