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 -->
<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 -->
@@ -13,13 +13,14 @@
1. [Terminating positional arguments](#terminating-positional-arguments)
1. [Slices](#slices)
1. [Maps](#maps)
1. [Custom named types](#custom-named-types)
1. [Supported tags](#supported-tags)
1. [Modifying Kong's behaviour](#modifying-kongs-behaviour)
1. [`Name(help)` and `Description(help)` - set the application name description](#namehelp-and-descriptionhelp---set-the-application-name-description)
1. [`Configuration(loader, paths...)` - load defaults from configuration files](#configurationloader-paths---load-defaults-from-configuration-files)
1. [`Resolver(...)` - support for default values from external sources](#resolver---support-for-default-values-from-external-sources)
1. [`*Mapper(...)` - customising how the command-line is mapped to Go values](#mapper---customising-how-the-command-line-is-mapped-to-go-values)
1. [`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. [Other options](#other-options)
@@ -176,7 +177,7 @@ You would use the following:
```go
var CLI struct {
Ls struct {
Files []string `arg`
Files []string `arg type:"existingfile"`
} `cmd`
}
```
@@ -195,12 +196,30 @@ You would use the following:
var CLI struct {
Config struct {
Set struct {
Config map[string]float64 `arg`
Config map[string]float64 `arg type:"file:"`
} `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
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.
| `name:"X"` | Long name, for overriding field name. |
| `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. |
| `default:"X"` | Default value. |
| `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)`.
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.
### `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() {
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 {
case "rm <paths>":
case "rm <path>":
fmt.Println(cli.Rm.Paths, cli.Rm.Force, cli.Rm.Recursive)
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) {
mapper := k.registry.ForNamedType(tag.Type, fv)
mapper := k.registry.ForNamedValue(tag.Type, fv)
if mapper == nil {
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()
}
// 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.
func (c *Context) Validate() error {
for _, path := range c.Path {
+48 -39
View File
@@ -13,46 +13,54 @@ const (
defaultColumnPadding = 4
)
// HelpOption configures the default help.
type HelpOption func(options *helpWriterOptions)
// HelpPrinterOptions for HelpPrinters.
type HelpPrinterOptions struct {
// Write a one-line summary of the context.
Summary bool
// CompactHelp writes help in a more compact form.
func CompactHelp() HelpOption {
return func(options *helpWriterOptions) {
options.compact = true
}
// Write help in a more compact form, but still fully-specified.
Compact bool
}
// HelpPrinter returns a HelpFunction configured with the given HelpOptions.
func HelpPrinter(options ...HelpOption) HelpFunction {
return func(ctx *Context) error {
w := newHelpWriter(guessWidth(ctx.App.Stdout))
for _, option := range options {
option(&w.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)
// HelpPrinter is used to print context-sensitive help.
type HelpPrinter func(options HelpPrinterOptions, ctx *Context) error
// DefaultHelpPrinter is the default HelpPrinter.
func DefaultHelpPrinter(options HelpPrinterOptions, ctx *Context) error {
if ctx.Empty() {
options.Summary = false
}
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) {
w.Printf("Usage: %s", app.Summary())
w.Printf("Usage: %s", app.Summary())
printNodeDetail(w, &app.Node)
cmds := app.Leaves()
if len(cmds) > 0 {
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) {
w.Printf("Usage: %s %s", app.Name, cmd.Summary())
w.Printf("Usage: %s %s", app.Name, cmd.Summary())
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) {
@@ -60,6 +68,9 @@ func printNodeDetail(w *helpWriter, node *Node) {
w.Print("")
w.Wrap(node.Help)
}
if w.Summary {
return
}
if len(node.Positional) > 0 {
w.Print("")
w.Print("Arguments:")
@@ -75,7 +86,7 @@ func printNodeDetail(w *helpWriter, node *Node) {
w.Print("")
w.Print("Commands:")
iw := w.Indent()
if w.options.compact {
if w.Compact {
rows := [][2]string{}
for _, cmd := range cmds {
rows = append(rows, [2]string{cmd.Path(), cmd.Help})
@@ -100,23 +111,21 @@ func printCommandSummary(w *helpWriter, cmd *Command) {
}
type helpWriter struct {
indent string
width int
lines *[]string
options helpWriterOptions
indent string
width int
lines *[]string
HelpPrinterOptions
}
type helpWriterOptions struct {
compact bool
}
func newHelpWriter(width int) *helpWriter {
func newHelpWriter(ctx *Context, options HelpPrinterOptions) *helpWriter {
lines := []string{}
return &helpWriter{
indent: "",
width: width,
lines: &lines,
w := &helpWriter{
indent: "",
width: guessWidth(ctx.App.Stdout),
lines: &lines,
HelpPrinterOptions: options,
}
return w
}
func (h *helpWriter) Printf(format string, args ...interface{}) {
@@ -128,7 +137,7 @@ func (h *helpWriter) Print(text string) {
}
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 {
+2 -2
View File
@@ -53,7 +53,7 @@ func TestHelp(t *testing.T) {
})
require.True(t, exited)
t.Log(w.String())
require.Equal(t, `Usage: test-app --required <command>
require.Equal(t, `Usage: test-app --required <command>
A test app.
@@ -89,7 +89,7 @@ Run "test-app <command> --help" for more information on a command.
})
require.True(t, exited)
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.
+28 -17
View File
@@ -38,13 +38,13 @@ type Kong struct {
Stdout io.Writer
Stderr io.Writer
before map[reflect.Value]HookFunc
resolvers []ResolverFunc
registry *Registry
noDefaultHelp bool
noUsageOnError bool
help func(*Context) error
helpOptions []HelpOption
before map[reflect.Value]HookFunc
resolvers []ResolverFunc
registry *Registry
noDefaultHelp bool
usageOnError bool
help HelpPrinter
helpOptions HelpPrinterOptions
// Set temporarily by Options. These are applied after build().
postBuildOptions []Option
@@ -70,7 +70,7 @@ func New(grammar interface{}, options ...Option) (*Kong, error) {
}
if k.help == nil {
k.help = HelpPrinter(k.helpOptions...)
k.help = DefaultHelpPrinter
}
model, err := build(k, grammar)
@@ -108,7 +108,9 @@ func (k *Kong) extraFlags() []*Flag {
}
helpFlag.Flag = helpFlag
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 {
return err
}
@@ -119,15 +121,22 @@ func (k *Kong) extraFlags() []*Flag {
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.
func (k *Kong) Help(args []string) error {
ctx, err := Trace(k, args)
if err != nil {
return err
func (k *Kong) Help(err error) error {
var ctx *Context
if perr, ok := err.(*ParseError); ok {
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.
@@ -215,9 +224,11 @@ func (k *Kong) FatalIfErrorf(err error, args ...interface{}) {
}
k.Errorf("%s", msg)
// Maybe display usage information.
if err, ok := err.(*ParseError); ok && !k.noUsageOnError {
if err, ok := err.(*ParseError); ok && k.usageOnError {
fmt.Fprintln(k.Stdout)
_ = k.help(err.Context)
options := k.helpOptions
options.Summary = true
_ = k.help(options, err.Context)
}
k.Exit(1)
}
+110 -37
View File
@@ -3,6 +3,7 @@ package kong
import (
"fmt"
"math/bits"
"os"
"reflect"
"strconv"
"strings"
@@ -20,9 +21,9 @@ type DecodeContext struct {
}
// 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{
Value: d.Value,
Value: r.Value,
Scan: scan,
}
}
@@ -44,8 +45,8 @@ type BoolMapper interface {
// A MapperFunc is a single function that complies with the Mapper interface.
type MapperFunc func(ctx *DecodeContext, target reflect.Value) error
func (d MapperFunc) Decode(ctx *DecodeContext, target reflect.Value) error { //nolint: golint
return d(ctx, target)
func (m MapperFunc) Decode(ctx *DecodeContext, target reflect.Value) error { //nolint: golint
return m(ctx, target)
}
// 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.
func (d *Registry) ForNamedType(name string, value reflect.Value) Mapper {
if mapper, ok := d.names[name]; ok {
func (r *Registry) ForNamedValue(name string, value reflect.Value) Mapper {
if mapper, ok := r.names[name]; ok {
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.
func (d *Registry) ForValue(value reflect.Value) Mapper {
if mapper, ok := d.values[value]; ok {
func (r *Registry) ForValue(value reflect.Value) Mapper {
if mapper, ok := r.values[value]; ok {
return mapper
}
return d.ForType(value.Type())
return r.ForType(value.Type())
}
// ForType finds a mapper from a type, by type, then kind.
//
// 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 ok bool
if mapper, ok = d.types[typ]; ok {
if mapper, ok = r.types[typ]; ok {
return mapper
} else if mapper, ok = d.kinds[typ.Kind()]; ok {
} else if mapper, ok = r.kinds[typ.Kind()]; ok {
return mapper
}
return nil
}
// RegisterKind registers a Mapper for a reflect.Kind.
func (d *Registry) RegisterKind(kind reflect.Kind, mapper Mapper) *Registry {
d.kinds[kind] = mapper
return d
func (r *Registry) RegisterKind(kind reflect.Kind, mapper Mapper) *Registry {
r.kinds[kind] = mapper
return r
}
// 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'`
// registry.RegisterName("colour", ...)
func (d *Registry) RegisterName(name string, mapper Mapper) *Registry {
d.names[name] = mapper
return d
func (r *Registry) RegisterName(name string, mapper Mapper) *Registry {
r.names[name] = mapper
return r
}
// RegisterType registers a Mapper for a reflect.Type.
func (d *Registry) RegisterType(typ reflect.Type, mapper Mapper) *Registry {
d.types[typ] = mapper
return d
func (r *Registry) RegisterType(typ reflect.Type, mapper Mapper) *Registry {
r.types[typ] = mapper
return r
}
// 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)
if key.Kind() != reflect.Ptr {
panic("expected a pointer")
}
key = key.Elem()
d.values[key] = mapper
return d
r.values[key] = mapper
return r
}
// RegisterDefaults registers Mappers for all builtin supported Go types and some common stdlib types.
func (d *Registry) RegisterDefaults() *Registry {
return d.RegisterKind(reflect.Int, intDecoder(bits.UintSize)).
func (r *Registry) RegisterDefaults() *Registry {
return r.RegisterKind(reflect.Int, intDecoder(bits.UintSize)).
RegisterKind(reflect.Int8, intDecoder(8)).
RegisterKind(reflect.Int16, intDecoder(16)).
RegisterKind(reflect.Int32, intDecoder(32)).
@@ -153,8 +164,11 @@ func (d *Registry) RegisterDefaults() *Registry {
RegisterKind(reflect.Bool, boolMapper{}).
RegisterType(reflect.TypeOf(time.Time{}), timeDecoder()).
RegisterType(reflect.TypeOf(time.Duration(0)), durationDecoder()).
RegisterKind(reflect.Slice, sliceDecoder(d)).
RegisterKind(reflect.Map, mapDecoder(d))
RegisterKind(reflect.Slice, sliceDecoder(r)).
RegisterKind(reflect.Map, mapDecoder(r)).
RegisterName("path", pathMapper(r)).
RegisterName("existingfile", existingFileMapper(r)).
RegisterName("existingdir", existingDirMapper(r))
}
type boolMapper struct{}
@@ -167,11 +181,11 @@ func (boolMapper) IsBool() bool { return true }
func durationDecoder() MapperFunc {
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 {
return err
}
target.Set(reflect.ValueOf(d))
target.Set(reflect.ValueOf(r))
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 {
if target.IsNil() {
target.Set(reflect.MakeMap(target.Type()))
@@ -240,15 +254,24 @@ func mapDecoder(d *Registry) MapperFunc {
}
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)
keyDecoder := d.ForType(el.Key())
keyDecoder := r.ForNamedType(keyTypeName, el.Key())
keyValue := reflect.New(el.Key()).Elem()
if err := keyDecoder.Decode(ctx.WithScanner(keyScanner), keyValue); err != nil {
return fmt.Errorf("invalid map key %q", key)
}
valueScanner := Scan(value)
valueDecoder := d.ForType(el.Elem())
valueDecoder := r.ForNamedType(valueTypeName, el.Elem())
valueValue := reflect.New(el.Elem()).Elem()
if err := valueDecoder.Decode(ctx.WithScanner(valueScanner), valueValue); err != nil {
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 {
el := target.Type().Elem()
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() })
childScanner = Scan(tokens...)
}
childDecoder := d.ForType(el)
childDecoder := r.ForNamedType(ctx.Value.Tag.Type, el)
if childDecoder == nil {
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.
//
// 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)
}
type testMooMapper struct{}
type testMooMapper struct {
text string
}
func (testMooMapper) Decode(ctx *DecodeContext, target reflect.Value) error {
target.SetString("MOO")
func (t testMooMapper) Decode(ctx *DecodeContext, target reflect.Value) error {
if t.text == "" {
target.SetString("MOO")
} else {
target.SetString(t.text)
}
return nil
}
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, 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.
type HelpFunction func(*Context) error
// Help function to use.
//
// Defaults to PrintHelp.
func Help(help HelpFunction) Option {
// Help printer to use.
func Help(help HelpPrinter) Option {
return func(k *Kong) error {
k.help = help
return nil
}
}
// HelpOptions specifies options for the default help printer, if used.
func HelpOptions(options ...HelpOption) Option {
// HelpOptions sets the HelpPrinterOptions to use for printing help.
func HelpOptions(options HelpPrinterOptions) Option {
return func(k *Kong) error {
k.helpOptions = options
return nil
}
}
// NoUsageOnError configures Kong to NOT display context-sensitive usage if FatalIfErrorf is called with an error.
func NoUsageOnError() Option {
// UsageOnError configures Kong to display context-sensitive usage if FatalIfErrorf is called with an error.
func UsageOnError() Option {
return func(k *Kong) error {
k.noUsageOnError = true
k.usageOnError = true
return nil
}
}