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 [![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) @@ -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 } }