Add path, existingfile and existingdir types.

- Document custom types.
- Add docker example.
This commit is contained in:
Alec Thomas
2018-06-21 16:16:27 +10:00
parent cdcdf49f67
commit 212ea2a356
11 changed files with 440 additions and 121 deletions
+27 -8
View File
@@ -1,7 +1,7 @@
<!-- markdownlint-disable MD013 MD033 --> <!-- markdownlint-disable MD013 MD033 -->
<p align="center"><img width="90%" src="kong.png" /></p> <p align="center"><img width="90%" src="kong.png" /></p>
# 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://img.shields.io/circleci/project/github/alecthomas/kong.svg)](https://circleci.com/gh/alecthomas/kong)
<!-- MarkdownTOC --> <!-- MarkdownTOC -->
@@ -13,13 +13,14 @@
1. [Terminating positional arguments](#terminating-positional-arguments) 1. [Terminating positional arguments](#terminating-positional-arguments)
1. [Slices](#slices) 1. [Slices](#slices)
1. [Maps](#maps) 1. [Maps](#maps)
1. [Custom named types](#custom-named-types)
1. [Supported tags](#supported-tags) 1. [Supported tags](#supported-tags)
1. [Modifying Kong's behaviour](#modifying-kongs-behaviour) 1. [Modifying Kong's behaviour](#modifying-kongs-behaviour)
1. [`Name(help)` and `Description(help)` - set the application name description](#namehelp-and-descriptionhelp---set-the-application-name-description) 1. [`Name(help)` and `Description(help)` - set the application name description](#namehelp-and-descriptionhelp---set-the-application-name-description)
1. [`Configuration(loader, paths...)` - load defaults from configuration files](#configurationloader-paths---load-defaults-from-configuration-files) 1. [`Configuration(loader, paths...)` - load defaults from configuration files](#configurationloader-paths---load-defaults-from-configuration-files)
1. [`Resolver(...)` - support for default values from external sources](#resolver---support-for-default-values-from-external-sources) 1. [`Resolver(...)` - support for default values from external sources](#resolver---support-for-default-values-from-external-sources)
1. [`*Mapper(...)` - customising how the command-line is mapped to Go values](#mapper---customising-how-the-command-line-is-mapped-to-go-values) 1. [`*Mapper(...)` - customising how the command-line is mapped to Go values](#mapper---customising-how-the-command-line-is-mapped-to-go-values)
1. [`HelpOptions(...HelpOption)` and `Help(HelpFunc)` - customising help](#helpoptionshelpoption-and-helphelpfunc---customising-help) 1. [`HelpOptions(HelpPrinterOptions)` and `Help(HelpFunc)` - customising help](#helpoptionshelpprinteroptions-and-helphelpfunc---customising-help)
1. [`Hook(&field, HookFunc)` - callback hooks to execute when the command-line is parsed](#hookfield-hookfunc---callback-hooks-to-execute-when-the-command-line-is-parsed) 1. [`Hook(&field, HookFunc)` - callback hooks to execute when the command-line is parsed](#hookfield-hookfunc---callback-hooks-to-execute-when-the-command-line-is-parsed)
1. [Other options](#other-options) 1. [Other options](#other-options)
@@ -176,7 +177,7 @@ You would use the following:
```go ```go
var CLI struct { var CLI struct {
Ls struct { Ls struct {
Files []string `arg` Files []string `arg type:"existingfile"`
} `cmd` } `cmd`
} }
``` ```
@@ -195,12 +196,30 @@ You would use the following:
var CLI struct { var CLI struct {
Config struct { Config struct {
Set struct { Set struct {
Config map[string]float64 `arg` Config map[string]float64 `arg type:"file:"`
} `cmd` } `cmd`
} `cmd` } `cmd`
} }
``` ```
## Custom named types
Kong includes a number of builtin custom type mappers. These can be used by
specifying the tag `type:"<type>"`. They are registered with the option
function `NamedMapper(name, mapper)`.
| Name | Description |
|-------------------|---------------------------------------------------|
| `file` | A path. ~ expansion is applied. |
| `existingfile` | An existing path. ~ expansion is applied. |
| `existingdir` | An existing directory. ~ expansion is applied. |
Slices and maps treat type tags specially. For slices, the `type:""` tag
specifies the element type. For maps, the tag has the format
`tag:"[<key>]:[<value>]"` where either may be omitted.
## Supported tags ## Supported tags
Tags can be in two forms: Tags can be in two forms:
@@ -217,7 +236,7 @@ Both can coexist with standard Tag parsing.
| `env:"X"` | Specify envar to use for default value. | `env:"X"` | Specify envar to use for default value.
| `name:"X"` | Long name, for overriding field name. | | `name:"X"` | Long name, for overriding field name. |
| `help:"X"` | Help text. | | `help:"X"` | Help text. |
| `type:"X"` | Specify named Mapper to use. | | `type:"X"` | Specify [named types](#custom-named-types) to use. |
| `placeholder:"X"` | Placeholder text. | | `placeholder:"X"` | Placeholder text. |
| `default:"X"` | Default value. | | `default:"X"` | Default value. |
| `short:"X"` | Short name, if flag. | | `short:"X"` | Short name, if flag. |
@@ -279,11 +298,11 @@ All builtin Go types (as well as a bunch of useful stdlib types like `time.Time`
3. `TypeMapper(reflect.Type, Mapper)`. 3. `TypeMapper(reflect.Type, Mapper)`.
4. `ValueMapper(interface{}, Mapper)`, passing in a pointer to a field of the grammar. 4. `ValueMapper(interface{}, Mapper)`, passing in a pointer to a field of the grammar.
### `HelpOptions(...HelpOption)` and `Help(HelpFunc)` - customising help ### `HelpOptions(HelpPrinterOptions)` and `Help(HelpFunc)` - customising help
The default help output is usually sufficient, but if it's not, there are two solutions. The default help output is usually sufficient, but if not there are two solutions.
1. Use `HelpOptions(options...HelpOption)` to configure the default help (see [HelpOption](https://godoc.org/github.com/alecthomas/kong#HelpOption) for details). 1. Use `HelpOptions(HelpPrinterOptions)` to configure how help is formatted (see [HelpPrinterOptions](https://godoc.org/github.com/alecthomas/kong#HelpPrinterOptions) for details).
2. Custom help can be wired into Kong via the `Help(HelpFunc)` option. The `HelpFunc` is passed a `Context`, which contains the parsed context for the current command-line. See the implementation of `PrintHelp` for an example. 2. Custom help can be wired into Kong via the `Help(HelpFunc)` option. The `HelpFunc` is passed a `Context`, which contains the parsed context for the current command-line. See the implementation of `PrintHelp` for an example.
### `Hook(&field, HookFunc)` - callback hooks to execute when the command-line is parsed ### `Hook(&field, HookFunc)` - callback hooks to execute when the command-line is parsed
+178
View File
@@ -0,0 +1,178 @@
// nolint
package main
import (
"fmt"
"github.com/alecthomas/kong"
)
type AttachCmd struct {
DetachKeys string `help:"Override the key sequence for detaching a container"`
NoStdin bool `help:"Do not attach STDIN"`
SigProxy bool `help:"Proxy all received signals to the process" default:"true"`
Container string `arg required help:"Container ID to attach to."`
}
func (a *AttachCmd) Run() error {
fmt.Printf("Attaching to: %v\n", a.Container)
fmt.Printf("SigProxy: %v\n", a.SigProxy)
return nil
}
var cli struct {
Config string `help:"Location of client config files" default:"~/.docker" type:"path"`
Debug bool `short:"D" help:"Enable debug mode"`
Host []string `short:"H" help:"Daemon socket(s) to connect to"`
LogLevel string `short:"l" help:"Set the logging level (debug|info|warn|error|fatal)" default:"info"`
TLS bool `help:"Use TLS; implied by --tlsverify"`
TLSCACert string `name:"tls-ca-cert" help:"Trust certs signed only by this CA" default:"~/.docker/ca.pem" type:"path"`
TLSCert string `name:"tls-cert" help:"Path to TLS certificate file" default:"~/.docker/cert.pem" type:"path"`
TLSKey string `help:"Path to TLS key file" default:"~/.docker/key.pem" type:"path"`
TLSVerify bool `help:"Use TLS and verify the remote"`
PrintVersion bool `name:"version" help:"Print version information and quit"`
Attach AttachCmd `cmd help:"Attach local standard input, output, and error streams to a running container"`
Build struct {
Arg string `arg required`
} `cmd help:"Build an image from a Dockerfile"`
Commit struct {
Arg string `arg required`
} `cmd help:"Create a new image from a container's changes"`
Cp struct {
Arg string `arg required`
} `cmd help:"Copy files/folders between a container and the local filesystem"`
Create struct {
Arg string `arg required`
} `cmd help:"Create a new container"`
Deploy struct {
Arg string `arg required`
} `cmd help:"Deploy a new stack or update an existing stack"`
Diff struct {
Arg string `arg required`
} `cmd help:"Inspect changes to files or directories on a container's filesystem"`
Events struct {
Arg string `arg required`
} `cmd help:"Get real time events from the server"`
Exec struct {
Arg string `arg required`
} `cmd help:"Run a command in a running container"`
Export struct {
Arg string `arg required`
} `cmd help:"Export a container's filesystem as a tar archive"`
History struct {
Arg string `arg required`
} `cmd help:"Show the history of an image"`
Images struct {
Arg string `arg required`
} `cmd help:"List images"`
Import struct {
Arg string `arg required`
} `cmd help:"Import the contents from a tarball to create a filesystem image"`
Info struct {
Arg string `arg required`
} `cmd help:"Display system-wide information"`
Inspect struct {
Arg string `arg required`
} `cmd help:"Return low-level information on Docker objects"`
Kill struct {
Arg string `arg required`
} `cmd help:"Kill one or more running containers"`
Load struct {
Arg string `arg required`
} `cmd help:"Load an image from a tar archive or STDIN"`
Login struct {
Arg string `arg required`
} `cmd help:"Log in to a Docker registry"`
Logout struct {
Arg string `arg required`
} `cmd help:"Log out from a Docker registry"`
Logs struct {
Arg string `arg required`
} `cmd help:"Fetch the logs of a container"`
Pause struct {
Arg string `arg required`
} `cmd help:"Pause all processes within one or more containers"`
Port struct {
Arg string `arg required`
} `cmd help:"List port mappings or a specific mapping for the container"`
Ps struct {
Arg string `arg required`
} `cmd help:"List containers"`
Pull struct {
Arg string `arg required`
} `cmd help:"Pull an image or a repository from a registry"`
Push struct {
Arg string `arg required`
} `cmd help:"Push an image or a repository to a registry"`
Rename struct {
Arg string `arg required`
} `cmd help:"Rename a container"`
Restart struct {
Arg string `arg required`
} `cmd help:"Restart one or more containers"`
Rm struct {
Arg string `arg required`
} `cmd help:"Remove one or more containers"`
Rmi struct {
Arg string `arg required`
} `cmd help:"Remove one or more images"`
Run struct {
Arg string `arg required`
} `cmd help:"Run a command in a new container"`
Save struct {
Arg string `arg required`
} `cmd help:"Save one or more images to a tar archive (streamed to STDOUT by default)"`
Search struct {
Arg string `arg required`
} `cmd help:"Search the Docker Hub for images"`
Start struct {
Arg string `arg required`
} `cmd help:"Start one or more stopped containers"`
Stats struct {
Arg string `arg required`
} `cmd help:"Display a live stream of container(s) resource usage statistics"`
Stop struct {
Arg string `arg required`
} `cmd help:"Stop one or more running containers"`
Tag struct {
Arg string `arg required`
} `cmd help:"Create a tag TARGET_IMAGE that refers to SOURCE_IMAGE"`
Top struct {
Arg string `arg required`
} `cmd help:"Display the running processes of a container"`
Unpause struct {
Arg string `arg required`
} `cmd help:"Unpause all processes within one or more containers"`
Update struct {
Arg string `arg required`
} `cmd help:"Update configuration of one or more containers"`
Version struct {
Arg string `arg required`
} `cmd help:"Show the Docker version information"`
Wait struct {
Arg string `arg required`
} `cmd help:"Block until one or more containers stop, then print their exit codes"`
}
func main() {
cmd := kong.Parse(&cli,
kong.Name("docker"),
kong.Description("A self-sufficient runtime for containers"),
kong.UsageOnError(),
kong.HelpOptions(kong.HelpPrinterOptions{
Compact: true,
}))
var err error
switch cmd {
case "attach <container>":
fmt.Println(cli.Config)
err = cli.Attach.Run()
default:
panic("unsupported command " + cmd)
}
kong.FatalIfErrorf(err)
}
+6 -2
View File
@@ -24,9 +24,13 @@ var cli struct {
func main() { func main() {
cmd := kong.Parse(&cli, kong.Description("A shell-like example app."), cmd := kong.Parse(&cli, kong.Description("A shell-like example app."),
kong.HelpOptions(kong.CompactHelp())) kong.UsageOnError(),
kong.HelpOptions(kong.HelpPrinterOptions{
Compact: true,
Summary: true,
}))
switch cmd { switch cmd {
case "rm <paths>": case "rm <path>":
fmt.Println(cli.Rm.Paths, cli.Rm.Force, cli.Rm.Recursive) fmt.Println(cli.Rm.Paths, cli.Rm.Force, cli.Rm.Recursive)
case "ls": case "ls":
+1 -1
View File
@@ -138,7 +138,7 @@ 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.ForNamedValue(tag.Type, fv)
if mapper == nil { if mapper == nil {
fail("unsupported field type %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)
} }
+10
View File
@@ -97,6 +97,16 @@ func Trace(k *Kong, args []string) (*Context, error) {
return c, c.traceResolvers() return c, c.traceResolvers()
} }
// Empty returns true if there were no arguments provided.
func (c *Context) Empty() bool {
for _, path := range c.Path {
if !path.Resolved && path.App == nil {
return false
}
}
return true
}
// Validate the current context. // Validate the current context.
func (c *Context) Validate() error { func (c *Context) Validate() error {
for _, path := range c.Path { for _, path := range c.Path {
+48 -39
View File
@@ -13,46 +13,54 @@ const (
defaultColumnPadding = 4 defaultColumnPadding = 4
) )
// HelpOption configures the default help. // HelpPrinterOptions for HelpPrinters.
type HelpOption func(options *helpWriterOptions) type HelpPrinterOptions struct {
// Write a one-line summary of the context.
Summary bool
// CompactHelp writes help in a more compact form. // Write help in a more compact form, but still fully-specified.
func CompactHelp() HelpOption { Compact bool
return func(options *helpWriterOptions) {
options.compact = true
}
} }
// HelpPrinter returns a HelpFunction configured with the given HelpOptions. // HelpPrinter is used to print context-sensitive help.
func HelpPrinter(options ...HelpOption) HelpFunction { type HelpPrinter func(options HelpPrinterOptions, ctx *Context) error
return func(ctx *Context) error {
w := newHelpWriter(guessWidth(ctx.App.Stdout)) // DefaultHelpPrinter is the default HelpPrinter.
for _, option := range options { func DefaultHelpPrinter(options HelpPrinterOptions, ctx *Context) error {
option(&w.options) if ctx.Empty() {
} options.Summary = false
selected := ctx.Selected()
if selected == nil {
printApp(w, ctx.App.Model)
} else {
printCommand(w, ctx.App.Model, selected)
}
return w.Write(ctx.App.Stdout)
} }
w := newHelpWriter(ctx, options)
selected := ctx.Selected()
if selected == nil {
printApp(w, ctx.App.Model)
} else {
printCommand(w, ctx.App.Model, selected)
}
return w.Write(ctx.App.Stdout)
} }
func printApp(w *helpWriter, app *Application) { func printApp(w *helpWriter, app *Application) {
w.Printf("Usage: %s", app.Summary()) w.Printf("Usage: %s", app.Summary())
printNodeDetail(w, &app.Node) printNodeDetail(w, &app.Node)
cmds := app.Leaves() cmds := app.Leaves()
if len(cmds) > 0 { if len(cmds) > 0 {
w.Print("") w.Print("")
w.Printf(`Run "%s <command> --help" for more information on a command.`, app.Name) if w.Summary {
w.Printf(`Run "%s --help" for more information.`, app.Name)
} else {
w.Printf(`Run "%s <command> --help" for more information on a command.`, app.Name)
}
} }
} }
func printCommand(w *helpWriter, app *Application, cmd *Command) { func printCommand(w *helpWriter, app *Application, cmd *Command) {
w.Printf("Usage: %s %s", app.Name, cmd.Summary()) w.Printf("Usage: %s %s", app.Name, cmd.Summary())
printNodeDetail(w, cmd) printNodeDetail(w, cmd)
if w.Summary {
w.Print("")
w.Printf(`Run "%s %s --help" for more information.`, app.Name, cmd.Path())
}
} }
func printNodeDetail(w *helpWriter, node *Node) { func printNodeDetail(w *helpWriter, node *Node) {
@@ -60,6 +68,9 @@ func printNodeDetail(w *helpWriter, node *Node) {
w.Print("") w.Print("")
w.Wrap(node.Help) w.Wrap(node.Help)
} }
if w.Summary {
return
}
if len(node.Positional) > 0 { if len(node.Positional) > 0 {
w.Print("") w.Print("")
w.Print("Arguments:") w.Print("Arguments:")
@@ -75,7 +86,7 @@ func printNodeDetail(w *helpWriter, node *Node) {
w.Print("") w.Print("")
w.Print("Commands:") w.Print("Commands:")
iw := w.Indent() iw := w.Indent()
if w.options.compact { if w.Compact {
rows := [][2]string{} rows := [][2]string{}
for _, cmd := range cmds { for _, cmd := range cmds {
rows = append(rows, [2]string{cmd.Path(), cmd.Help}) rows = append(rows, [2]string{cmd.Path(), cmd.Help})
@@ -100,23 +111,21 @@ func printCommandSummary(w *helpWriter, cmd *Command) {
} }
type helpWriter struct { type helpWriter struct {
indent string indent string
width int width int
lines *[]string lines *[]string
options helpWriterOptions HelpPrinterOptions
} }
type helpWriterOptions struct { func newHelpWriter(ctx *Context, options HelpPrinterOptions) *helpWriter {
compact bool
}
func newHelpWriter(width int) *helpWriter {
lines := []string{} lines := []string{}
return &helpWriter{ w := &helpWriter{
indent: "", indent: "",
width: width, width: guessWidth(ctx.App.Stdout),
lines: &lines, lines: &lines,
HelpPrinterOptions: options,
} }
return w
} }
func (h *helpWriter) Printf(format string, args ...interface{}) { func (h *helpWriter) Printf(format string, args ...interface{}) {
@@ -128,7 +137,7 @@ func (h *helpWriter) Print(text string) {
} }
func (h *helpWriter) Indent() *helpWriter { func (h *helpWriter) Indent() *helpWriter {
return &helpWriter{indent: h.indent + " ", lines: h.lines, width: h.width - 2, options: h.options} return &helpWriter{indent: h.indent + " ", lines: h.lines, width: h.width - 2, HelpPrinterOptions: h.HelpPrinterOptions}
} }
func (h *helpWriter) String() string { func (h *helpWriter) String() string {
+2 -2
View File
@@ -53,7 +53,7 @@ func TestHelp(t *testing.T) {
}) })
require.True(t, exited) require.True(t, exited)
t.Log(w.String()) t.Log(w.String())
require.Equal(t, `Usage: test-app --required <command> require.Equal(t, `Usage: test-app --required <command>
A test app. A test app.
@@ -89,7 +89,7 @@ Run "test-app <command> --help" for more information on a command.
}) })
require.True(t, exited) require.True(t, exited)
t.Log(w.String()) t.Log(w.String())
require.Equal(t, `Usage: test-app two <three> --required --required-two --required-three require.Equal(t, `Usage: test-app two <three> --required --required-two --required-three
Sub-sub-arg. Sub-sub-arg.
+28 -17
View File
@@ -38,13 +38,13 @@ type Kong struct {
Stdout io.Writer Stdout io.Writer
Stderr io.Writer Stderr io.Writer
before map[reflect.Value]HookFunc before map[reflect.Value]HookFunc
resolvers []ResolverFunc resolvers []ResolverFunc
registry *Registry registry *Registry
noDefaultHelp bool noDefaultHelp bool
noUsageOnError bool usageOnError bool
help func(*Context) error help HelpPrinter
helpOptions []HelpOption helpOptions HelpPrinterOptions
// Set temporarily by Options. These are applied after build(). // Set temporarily by Options. These are applied after build().
postBuildOptions []Option postBuildOptions []Option
@@ -70,7 +70,7 @@ func New(grammar interface{}, options ...Option) (*Kong, error) {
} }
if k.help == nil { if k.help == nil {
k.help = HelpPrinter(k.helpOptions...) k.help = DefaultHelpPrinter
} }
model, err := build(k, grammar) model, err := build(k, grammar)
@@ -108,7 +108,9 @@ func (k *Kong) extraFlags() []*Flag {
} }
helpFlag.Flag = helpFlag helpFlag.Flag = helpFlag
hook := Hook(&helpValue, func(ctx *Context, path *Path) error { hook := Hook(&helpValue, func(ctx *Context, path *Path) error {
err := k.help(ctx) options := k.helpOptions
options.Summary = false
err := k.help(options, ctx)
if err != nil { if err != nil {
return err return err
} }
@@ -119,15 +121,22 @@ func (k *Kong) extraFlags() []*Flag {
return []*Flag{helpFlag} return []*Flag{helpFlag}
} }
// Help writes help for the given args to the stdout io.Writer associated with this Kong. // Help writes help for the given error to the stdout io.Writer associated with this Kong.
//
// "err" should be the error returned by Parse().
// //
// See Help() and Writers() for overriding the help function and stdout, respectively. // See Help() and Writers() for overriding the help function and stdout, respectively.
func (k *Kong) Help(args []string) error { func (k *Kong) Help(err error) error {
ctx, err := Trace(k, args) var ctx *Context
if err != nil { if perr, ok := err.(*ParseError); ok {
return err ctx = perr.Context
} else {
ctx, err = Trace(k, nil)
if err != nil {
return err
}
} }
return k.help(ctx) return k.help(k.helpOptions, ctx)
} }
// Parse arguments into target. // Parse arguments into target.
@@ -215,9 +224,11 @@ func (k *Kong) FatalIfErrorf(err error, args ...interface{}) {
} }
k.Errorf("%s", msg) k.Errorf("%s", msg)
// Maybe display usage information. // Maybe display usage information.
if err, ok := err.(*ParseError); ok && !k.noUsageOnError { if err, ok := err.(*ParseError); ok && k.usageOnError {
fmt.Fprintln(k.Stdout) fmt.Fprintln(k.Stdout)
_ = k.help(err.Context) options := k.helpOptions
options.Summary = true
_ = k.help(options, err.Context)
} }
k.Exit(1) k.Exit(1)
} }
+110 -37
View File
@@ -3,6 +3,7 @@ package kong
import ( import (
"fmt" "fmt"
"math/bits" "math/bits"
"os"
"reflect" "reflect"
"strconv" "strconv"
"strings" "strings"
@@ -20,9 +21,9 @@ type DecodeContext struct {
} }
// WithScanner creates a clone of this context with a new Scanner. // WithScanner creates a clone of this context with a new Scanner.
func (d *DecodeContext) WithScanner(scan *Scanner) *DecodeContext { func (r *DecodeContext) WithScanner(scan *Scanner) *DecodeContext {
return &DecodeContext{ return &DecodeContext{
Value: d.Value, Value: r.Value,
Scan: scan, Scan: scan,
} }
} }
@@ -44,8 +45,8 @@ type BoolMapper interface {
// A MapperFunc is a single function that complies with the Mapper interface. // A MapperFunc is a single function that complies with the Mapper interface.
type MapperFunc func(ctx *DecodeContext, target reflect.Value) error type MapperFunc func(ctx *DecodeContext, target reflect.Value) error
func (d MapperFunc) Decode(ctx *DecodeContext, target reflect.Value) error { //nolint: golint func (m MapperFunc) Decode(ctx *DecodeContext, target reflect.Value) error { //nolint: golint
return d(ctx, target) return m(ctx, target)
} }
// A Registry contains a set of mappers and supporting lookup methods. // A Registry contains a set of mappers and supporting lookup methods.
@@ -66,42 +67,52 @@ func NewRegistry() *Registry {
} }
} }
// ForNamedType finds a mapper for a value with a user-specified type. // ForNamedValue finds a mapper for a value with a user-specified name.
// //
// Will return nil if a mapper can not be determined. // Will return nil if a mapper can not be determined.
func (d *Registry) ForNamedType(name string, value reflect.Value) Mapper { func (r *Registry) ForNamedValue(name string, value reflect.Value) Mapper {
if mapper, ok := d.names[name]; ok { if mapper, ok := r.names[name]; ok {
return mapper return mapper
} }
return d.ForValue(value) return r.ForValue(value)
}
// ForNamedType finds a mapper for a type with a user-specified name.
//
// Will return nil if a mapper can not be determined.
func (r *Registry) ForNamedType(name string, typ reflect.Type) Mapper {
if mapper, ok := r.names[name]; ok {
return mapper
}
return r.ForType(typ)
} }
// ForValue looks up the Mapper for a reflect.Value. // ForValue looks up the Mapper for a reflect.Value.
func (d *Registry) ForValue(value reflect.Value) Mapper { func (r *Registry) ForValue(value reflect.Value) Mapper {
if mapper, ok := d.values[value]; ok { if mapper, ok := r.values[value]; ok {
return mapper return mapper
} }
return d.ForType(value.Type()) return r.ForType(value.Type())
} }
// ForType finds a mapper from a type, by type, then kind. // ForType finds a mapper from a type, by type, then kind.
// //
// Will return nil if a mapper can not be determined. // Will return nil if a mapper can not be determined.
func (d *Registry) ForType(typ reflect.Type) Mapper { func (r *Registry) ForType(typ reflect.Type) Mapper {
var mapper Mapper var mapper Mapper
var ok bool var ok bool
if mapper, ok = d.types[typ]; ok { if mapper, ok = r.types[typ]; ok {
return mapper return mapper
} else if mapper, ok = d.kinds[typ.Kind()]; ok { } else if mapper, ok = r.kinds[typ.Kind()]; ok {
return mapper return mapper
} }
return nil return nil
} }
// RegisterKind registers a Mapper for a reflect.Kind. // RegisterKind registers a Mapper for a reflect.Kind.
func (d *Registry) RegisterKind(kind reflect.Kind, mapper Mapper) *Registry { func (r *Registry) RegisterKind(kind reflect.Kind, mapper Mapper) *Registry {
d.kinds[kind] = mapper r.kinds[kind] = mapper
return d return r
} }
// RegisterName registeres a mapper to be used if the value mapper has a "type" tag matching name. // RegisterName registeres a mapper to be used if the value mapper has a "type" tag matching name.
@@ -110,31 +121,31 @@ func (d *Registry) RegisterKind(kind reflect.Kind, mapper Mapper) *Registry {
// //
// Mapper string `kong:"type='colour'` // Mapper string `kong:"type='colour'`
// registry.RegisterName("colour", ...) // registry.RegisterName("colour", ...)
func (d *Registry) RegisterName(name string, mapper Mapper) *Registry { func (r *Registry) RegisterName(name string, mapper Mapper) *Registry {
d.names[name] = mapper r.names[name] = mapper
return d return r
} }
// RegisterType registers a Mapper for a reflect.Type. // RegisterType registers a Mapper for a reflect.Type.
func (d *Registry) RegisterType(typ reflect.Type, mapper Mapper) *Registry { func (r *Registry) RegisterType(typ reflect.Type, mapper Mapper) *Registry {
d.types[typ] = mapper r.types[typ] = mapper
return d return r
} }
// RegisterValue registers a Mapper by pointer to the field value. // RegisterValue registers a Mapper by pointer to the field value.
func (d *Registry) RegisterValue(ptr interface{}, mapper Mapper) *Registry { func (r *Registry) RegisterValue(ptr interface{}, mapper Mapper) *Registry {
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")
} }
key = key.Elem() key = key.Elem()
d.values[key] = mapper r.values[key] = mapper
return d return r
} }
// RegisterDefaults registers Mappers for all builtin supported Go types and some common stdlib types. // RegisterDefaults registers Mappers for all builtin supported Go types and some common stdlib types.
func (d *Registry) RegisterDefaults() *Registry { func (r *Registry) RegisterDefaults() *Registry {
return d.RegisterKind(reflect.Int, intDecoder(bits.UintSize)). return r.RegisterKind(reflect.Int, intDecoder(bits.UintSize)).
RegisterKind(reflect.Int8, intDecoder(8)). RegisterKind(reflect.Int8, intDecoder(8)).
RegisterKind(reflect.Int16, intDecoder(16)). RegisterKind(reflect.Int16, intDecoder(16)).
RegisterKind(reflect.Int32, intDecoder(32)). RegisterKind(reflect.Int32, intDecoder(32)).
@@ -153,8 +164,11 @@ func (d *Registry) RegisterDefaults() *Registry {
RegisterKind(reflect.Bool, boolMapper{}). RegisterKind(reflect.Bool, boolMapper{}).
RegisterType(reflect.TypeOf(time.Time{}), timeDecoder()). RegisterType(reflect.TypeOf(time.Time{}), timeDecoder()).
RegisterType(reflect.TypeOf(time.Duration(0)), durationDecoder()). RegisterType(reflect.TypeOf(time.Duration(0)), durationDecoder()).
RegisterKind(reflect.Slice, sliceDecoder(d)). RegisterKind(reflect.Slice, sliceDecoder(r)).
RegisterKind(reflect.Map, mapDecoder(d)) RegisterKind(reflect.Map, mapDecoder(r)).
RegisterName("path", pathMapper(r)).
RegisterName("existingfile", existingFileMapper(r)).
RegisterName("existingdir", existingDirMapper(r))
} }
type boolMapper struct{} type boolMapper struct{}
@@ -167,11 +181,11 @@ func (boolMapper) IsBool() bool { return true }
func durationDecoder() MapperFunc { func durationDecoder() MapperFunc {
return func(ctx *DecodeContext, target reflect.Value) error { return func(ctx *DecodeContext, target reflect.Value) error {
d, err := time.ParseDuration(ctx.Scan.PopValue("duration")) r, err := time.ParseDuration(ctx.Scan.PopValue("duration"))
if err != nil { if err != nil {
return err return err
} }
target.Set(reflect.ValueOf(d)) target.Set(reflect.ValueOf(r))
return nil return nil
} }
} }
@@ -227,7 +241,7 @@ func floatDecoder(bits int) MapperFunc {
} }
} }
func mapDecoder(d *Registry) MapperFunc { func mapDecoder(r *Registry) MapperFunc {
return func(ctx *DecodeContext, target reflect.Value) error { return func(ctx *DecodeContext, target reflect.Value) error {
if target.IsNil() { if target.IsNil() {
target.Set(reflect.MakeMap(target.Type())) target.Set(reflect.MakeMap(target.Type()))
@@ -240,15 +254,24 @@ func mapDecoder(d *Registry) MapperFunc {
} }
key, value := parts[0], parts[1] key, value := parts[0], parts[1]
keyTypeName, valueTypeName := "", ""
if typ := ctx.Value.Tag.Type; typ != "" {
parts := strings.Split(typ, ":")
if len(parts) != 2 {
return fmt.Errorf("type:\"\" on map field must be in the form \"[<keytype>]:[<valuetype>]\"")
}
keyTypeName, valueTypeName = parts[0], parts[1]
}
keyScanner := Scan(key) keyScanner := Scan(key)
keyDecoder := d.ForType(el.Key()) keyDecoder := r.ForNamedType(keyTypeName, el.Key())
keyValue := reflect.New(el.Key()).Elem() keyValue := reflect.New(el.Key()).Elem()
if err := keyDecoder.Decode(ctx.WithScanner(keyScanner), keyValue); err != nil { if err := keyDecoder.Decode(ctx.WithScanner(keyScanner), keyValue); err != nil {
return fmt.Errorf("invalid map key %q", key) return fmt.Errorf("invalid map key %q", key)
} }
valueScanner := Scan(value) valueScanner := Scan(value)
valueDecoder := d.ForType(el.Elem()) valueDecoder := r.ForNamedType(valueTypeName, el.Elem())
valueValue := reflect.New(el.Elem()).Elem() valueValue := reflect.New(el.Elem()).Elem()
if err := valueDecoder.Decode(ctx.WithScanner(valueScanner), valueValue); err != nil { if err := valueDecoder.Decode(ctx.WithScanner(valueScanner), valueValue); err != nil {
return fmt.Errorf("invalid map value %q", value) return fmt.Errorf("invalid map value %q", value)
@@ -259,7 +282,7 @@ func mapDecoder(d *Registry) MapperFunc {
} }
} }
func sliceDecoder(d *Registry) MapperFunc { func sliceDecoder(r *Registry) MapperFunc {
return func(ctx *DecodeContext, target reflect.Value) error { return func(ctx *DecodeContext, target reflect.Value) error {
el := target.Type().Elem() el := target.Type().Elem()
sep := ctx.Value.Tag.Sep sep := ctx.Value.Tag.Sep
@@ -271,7 +294,7 @@ func sliceDecoder(d *Registry) MapperFunc {
tokens := ctx.Scan.PopWhile(func(t Token) bool { return t.IsValue() }) tokens := ctx.Scan.PopWhile(func(t Token) bool { return t.IsValue() })
childScanner = Scan(tokens...) childScanner = Scan(tokens...)
} }
childDecoder := d.ForType(el) childDecoder := r.ForNamedType(ctx.Value.Tag.Type, el)
if childDecoder == nil { if childDecoder == nil {
return fmt.Errorf("no mapper for element type of %s", target.Type()) return fmt.Errorf("no mapper for element type of %s", target.Type())
} }
@@ -287,6 +310,56 @@ func sliceDecoder(d *Registry) MapperFunc {
} }
} }
func pathMapper(r *Registry) MapperFunc {
return func(ctx *DecodeContext, target reflect.Value) error {
if target.Kind() == reflect.Slice {
return sliceDecoder(r)(ctx, target)
}
path := ctx.Scan.PopValue("file")
path = expandPath(path)
target.SetString(path)
return nil
}
}
func existingFileMapper(r *Registry) MapperFunc {
return func(ctx *DecodeContext, target reflect.Value) error {
if target.Kind() == reflect.Slice {
return sliceDecoder(r)(ctx, target)
}
path := ctx.Scan.PopValue("file")
path = expandPath(path)
stat, err := os.Stat(path)
if err != nil {
return err
}
if stat.IsDir() {
return fmt.Errorf("%q exists but is a directory", path)
}
target.SetString(path)
return nil
}
}
func existingDirMapper(r *Registry) MapperFunc {
return func(ctx *DecodeContext, target reflect.Value) error {
if target.Kind() == reflect.Slice {
return sliceDecoder(r)(ctx, target)
}
path := ctx.Scan.PopValue("file")
path = expandPath(path)
stat, err := os.Stat(path)
if err != nil {
return err
}
if !stat.IsDir() {
return fmt.Errorf("%q exists but is not a directory", path)
}
target.SetString(path)
return nil
}
}
// SplitEscaped splits a string on a separator. // SplitEscaped splits a string on a separator.
// //
// It differs from strings.Split() in that the separator can exist in a field by escaping it with a \. eg. // It differs from strings.Split() in that the separator can exist in a field by escaping it with a \. eg.
+23 -3
View File
@@ -34,10 +34,16 @@ func TestNamedMapper(t *testing.T) {
require.Equal(t, "MOO", cli.Flag) require.Equal(t, "MOO", cli.Flag)
} }
type testMooMapper struct{} type testMooMapper struct {
text string
}
func (testMooMapper) Decode(ctx *DecodeContext, target reflect.Value) error { func (t testMooMapper) Decode(ctx *DecodeContext, target reflect.Value) error {
target.SetString("MOO") if t.text == "" {
target.SetString("MOO")
} else {
target.SetString(t.text)
}
return nil return nil
} }
func (testMooMapper) IsBool() bool { return true } func (testMooMapper) IsBool() bool { return true }
@@ -75,3 +81,17 @@ func TestJoinEscaped(t *testing.T) {
require.Equal(t, `a\,b,c`, JoinEscaped([]string{`a,b`, `c`}, ',')) require.Equal(t, `a\,b,c`, JoinEscaped([]string{`a,b`, `c`}, ','))
require.Equal(t, JoinEscaped(SplitEscaped(`a\,b,c`, ','), ','), `a\,b,c`) require.Equal(t, JoinEscaped(SplitEscaped(`a\,b,c`, ','), ','), `a\,b,c`)
} }
func TestMapWithNamedTypes(t *testing.T) {
var cli struct {
TypedValue map[string]string `type:":moo"`
TypedKey map[string]string `type:"upper:"`
}
k := mustNew(t, &cli, NamedMapper("moo", testMooMapper{}), NamedMapper("upper", testUppercaseMapper{}))
_, err := k.Parse([]string{"--typed-value", "first=5s", "--typed-value", "second=10s"})
require.NoError(t, err)
require.Equal(t, map[string]string{"first": "MOO", "second": "MOO"}, cli.TypedValue)
_, err = k.Parse([]string{"--typed-key", "first=5s", "--typed-key", "second=10s"})
require.NoError(t, err)
require.Equal(t, map[string]string{"FIRST": "5s", "SECOND": "10s"}, cli.TypedKey)
}
+7 -12
View File
@@ -111,31 +111,26 @@ func Hook(ptr interface{}, hook HookFunc) Option {
} }
} }
// HelpFunction is the type of a function used to display help. // Help printer to use.
type HelpFunction func(*Context) error func Help(help HelpPrinter) Option {
// Help function to use.
//
// Defaults to PrintHelp.
func Help(help HelpFunction) Option {
return func(k *Kong) error { return func(k *Kong) error {
k.help = help k.help = help
return nil return nil
} }
} }
// HelpOptions specifies options for the default help printer, if used. // HelpOptions sets the HelpPrinterOptions to use for printing help.
func HelpOptions(options ...HelpOption) Option { func HelpOptions(options HelpPrinterOptions) Option {
return func(k *Kong) error { return func(k *Kong) error {
k.helpOptions = options k.helpOptions = options
return nil return nil
} }
} }
// NoUsageOnError configures Kong to NOT display context-sensitive usage if FatalIfErrorf is called with an error. // UsageOnError configures Kong to display context-sensitive usage if FatalIfErrorf is called with an error.
func NoUsageOnError() Option { func UsageOnError() Option {
return func(k *Kong) error { return func(k *Kong) error {
k.noUsageOnError = true k.usageOnError = true
return nil return nil
} }
} }