diff --git a/README.md b/README.md
index 6758dca..cde4e55 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@

-# Kong is a command-line parser for Go [](https://circleci.com/gh/alecthomas/kong)
+# Kong is a command-line parser for Go [](https://circleci.com/gh/alecthomas/kong)
@@ -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:""`. 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:"[]:[]"` 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
diff --git a/_examples/docker/main.go b/_examples/docker/main.go
new file mode 100644
index 0000000..c961e03
--- /dev/null
+++ b/_examples/docker/main.go
@@ -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 ":
+ fmt.Println(cli.Config)
+ err = cli.Attach.Run()
+
+ default:
+ panic("unsupported command " + cmd)
+ }
+ kong.FatalIfErrorf(err)
+}
diff --git a/_examples/shell/main.go b/_examples/shell/main.go
index 46e07f5..9aa3652 100644
--- a/_examples/shell/main.go
+++ b/_examples/shell/main.go
@@ -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 ":
+ case "rm ":
fmt.Println(cli.Rm.Paths, cli.Rm.Force, cli.Rm.Recursive)
case "ls":
diff --git a/build.go b/build.go
index 75ad002..41229fd 100644
--- a/build.go
+++ b/build.go
@@ -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)
}
diff --git a/context.go b/context.go
index 5a56565..aa77f36 100644
--- a/context.go
+++ b/context.go
@@ -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 {
diff --git a/help.go b/help.go
index 087d10c..cf81059 100644
--- a/help.go
+++ b/help.go
@@ -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 --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 --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 {
diff --git a/help_test.go b/help_test.go
index 762a95b..37ee440 100644
--- a/help_test.go
+++ b/help_test.go
@@ -53,7 +53,7 @@ func TestHelp(t *testing.T) {
})
require.True(t, exited)
t.Log(w.String())
- require.Equal(t, `Usage: test-app --required
+ require.Equal(t, `Usage: test-app --required
A test app.
@@ -89,7 +89,7 @@ Run "test-app --help" for more information on a command.
})
require.True(t, exited)
t.Log(w.String())
- require.Equal(t, `Usage: test-app two --required --required-two --required-three
+ require.Equal(t, `Usage: test-app two --required --required-two --required-three
Sub-sub-arg.
diff --git a/kong.go b/kong.go
index ed7ff80..5e4e785 100644
--- a/kong.go
+++ b/kong.go
@@ -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)
}
diff --git a/mapper.go b/mapper.go
index 6e01a76..863478e 100644
--- a/mapper.go
+++ b/mapper.go
@@ -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 \"[]:[]\"")
+ }
+ 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.
diff --git a/mapper_test.go b/mapper_test.go
index 44008f5..978a23a 100644
--- a/mapper_test.go
+++ b/mapper_test.go
@@ -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)
+}
diff --git a/options.go b/options.go
index 2a4f3ef..d610a66 100644
--- a/options.go
+++ b/options.go
@@ -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
}
}