Add path, existingfile and existingdir types.
- Document custom types. - Add docker example.
This commit is contained in:
@@ -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 [](https://circleci.com/gh/alecthomas/kong)
|
||||
# Kong is a command-line parser for Go [](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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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":
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user