Improved documentation and help.
This commit is contained in:
@@ -1,6 +1,34 @@
|
||||
# Kong is a command-line parser for Go [](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.
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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
@@ -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())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user