Implement a robust Context.Run().

This helps when composing large applications from separate command
structs.
This commit is contained in:
Alec Thomas
2018-06-21 21:50:30 +10:00
parent e4f37b5d1a
commit a2ec050947
16 changed files with 727 additions and 279 deletions
+77 -5
View File
@@ -7,6 +7,9 @@
1. [Introduction](#introduction)
1. [Help](#help)
1. [Command handling](#command-handling)
1. [Switch on the command string](#switch-on-the-command-string)
1. [Attach a `Run(...) error` method to each command](#attach-a-run-error-method-to-each-command)
1. [Flags](#flags)
1. [Commands and sub-commands](#commands-and-sub-commands)
1. [Branching positional arguments](#branching-positional-arguments)
@@ -49,16 +52,22 @@ var CLI struct {
Force bool `help:"Force removal."`
Recursive bool `help:"Recursively remove files."`
Paths []string `arg help:"Paths to remove." type:"path"`
Paths []string `arg name:"path" help:"Paths to remove." type:"path"`
} `cmd help:"Remove files."`
Ls struct {
Paths []string `arg optional help:"Paths to list." type:"path"`
Paths []string `arg optional name:"path" help:"Paths to list." type:"path"`
} `cmd help:"List paths."`
}
func main() {
kong.Parse(&CLI)
cmd := kong.Parse(&CLI)
switch cmd {
case "rm <path>":
case "ls":
default:
panic(cmd)
}
}
```
@@ -78,10 +87,10 @@ eg.
--debug Debug mode.
Commands:
rm <paths> ...
rm <path> ...
Remove files.
ls [<paths> ...]
ls [<path> ...]
List paths.
If a command is provided, the help will show full detail on the command including all available flags.
@@ -102,6 +111,69 @@ eg.
-f, --force Force removal.
-r, --recursive Recursively remove files.
## Command handling
There are two ways to handle commands in Kong.
### Switch on the command string
When you call `kong.Parse()` it will return a unique string representation of the command. Each command branch in the hierarchy will be a bare word and each branching argument or required positional argument will be the name surrounded by angle brackets.
This has the advantage that it is convenient, but the downside that if you modify your CLI structure, the strings may change. This can be fragile.
### Attach a `Run(...) error` method to each command
A more robust approach is to break each command out into their own structs:
1. Attach a `Run(...) error` method to all leaf commands (`Run()` signatures must match).
2. Call `kong.Kong.Parse()` to obtain a `kong.Context`.
3. Call `kong.Context.Run(params...)` to call the selected parsed command.
eg.
```go
type RmCmd struct {
Force bool `help:"Force removal."`
Recursive bool `help:"Recursively remove files."`
Paths []string `arg name:"path" help:"Paths to remove." type:"path"`
}
func (r *RmCmd) Run(debug bool) error {
fmt.Println("rm", r.Paths)
return nil
}
type LsCmd struct {
Paths []string `arg optional name:"path" help:"Paths to list." type:"path"`
}
func (l *LsCmd) Run(debug bool) error {
fmt.Println("ls", l.Paths)
return nil
}
var cli struct {
Debug bool `help:"Enable debug mode."`
Rm RmCmd `cmd help:"Remove files."`
Ls LsCmd `cmd help:"List paths."`
}
func main() {
parser := kong.Must(&cli)
// Parse and apply the command-line.
ctx, err := parser.Parse(os.Args[1:])
parser.FatalIfErrorf(err)
// Call the Run() method of the selected parsed command.
err = ctx.Run(cli.Debug)
parser.FatalIfErrorf(err)
}
```
## Flags
Any [mapped](#mapper---customising-how-the-command-line-is-mapped-to-go-values) field in the command structure *not* tagged with `cmd` or `arg` will be a flag. Flags are optional by default.
+3
View File
@@ -0,0 +1,3 @@
# Large-scale composed CLI
This directory illustrates how a large-scale CLI app could be structured.
+339
View File
@@ -0,0 +1,339 @@
// nolint
package main
import "fmt"
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(globals *Globals) error {
fmt.Printf("Config: %s\n", globals.Config)
fmt.Printf("Attaching to: %v\n", a.Container)
fmt.Printf("SigProxy: %v\n", a.SigProxy)
return nil
}
type BuildCmd struct {
Arg string `arg required`
}
func (cmd *BuildCmd) Run(globals *Globals) error {
return nil
}
type CommitCmd struct {
Arg string `arg required`
}
func (cmd *CommitCmd) Run(globals *Globals) error {
return nil
}
type CpCmd struct {
Arg string `arg required`
}
func (cmd *CpCmd) Run(globals *Globals) error {
return nil
}
type CreateCmd struct {
Arg string `arg required`
}
func (cmd *CreateCmd) Run(globals *Globals) error {
return nil
}
type DeployCmd struct {
Arg string `arg required`
}
func (cmd *DeployCmd) Run(globals *Globals) error {
return nil
}
type DiffCmd struct {
Arg string `arg required`
}
func (cmd *DiffCmd) Run(globals *Globals) error {
return nil
}
type EventsCmd struct {
Arg string `arg required`
}
func (cmd *EventsCmd) Run(globals *Globals) error {
return nil
}
type ExecCmd struct {
Arg string `arg required`
}
func (cmd *ExecCmd) Run(globals *Globals) error {
return nil
}
type ExportCmd struct {
Arg string `arg required`
}
func (cmd *ExportCmd) Run(globals *Globals) error {
return nil
}
type HistoryCmd struct {
Arg string `arg required`
}
func (cmd *HistoryCmd) Run(globals *Globals) error {
return nil
}
type ImagesCmd struct {
Arg string `arg required`
}
func (cmd *ImagesCmd) Run(globals *Globals) error {
return nil
}
type ImportCmd struct {
Arg string `arg required`
}
func (cmd *ImportCmd) Run(globals *Globals) error {
return nil
}
type InfoCmd struct {
Arg string `arg required`
}
func (cmd *InfoCmd) Run(globals *Globals) error {
return nil
}
type InspectCmd struct {
Arg string `arg required`
}
func (cmd *InspectCmd) Run(globals *Globals) error {
return nil
}
type KillCmd struct {
Arg string `arg required`
}
func (cmd *KillCmd) Run(globals *Globals) error {
return nil
}
type LoadCmd struct {
Arg string `arg required`
}
func (cmd *LoadCmd) Run(globals *Globals) error {
return nil
}
type LoginCmd struct {
Arg string `arg required`
}
func (cmd *LoginCmd) Run(globals *Globals) error {
return nil
}
type LogoutCmd struct {
Arg string `arg required`
}
func (cmd *LogoutCmd) Run(globals *Globals) error {
return nil
}
type LogsCmd struct {
Arg string `arg required`
}
func (cmd *LogsCmd) Run(globals *Globals) error {
return nil
}
type PauseCmd struct {
Arg string `arg required`
}
func (cmd *PauseCmd) Run(globals *Globals) error {
return nil
}
type PortCmd struct {
Arg string `arg required`
}
func (cmd *PortCmd) Run(globals *Globals) error {
return nil
}
type PsCmd struct {
Arg string `arg required`
}
func (cmd *PsCmd) Run(globals *Globals) error {
return nil
}
type PullCmd struct {
Arg string `arg required`
}
func (cmd *PullCmd) Run(globals *Globals) error {
return nil
}
type PushCmd struct {
Arg string `arg required`
}
func (cmd *PushCmd) Run(globals *Globals) error {
return nil
}
type RenameCmd struct {
Arg string `arg required`
}
func (cmd *RenameCmd) Run(globals *Globals) error {
return nil
}
type RestartCmd struct {
Arg string `arg required`
}
func (cmd *RestartCmd) Run(globals *Globals) error {
return nil
}
type RmCmd struct {
Arg string `arg required`
}
func (cmd *RmCmd) Run(globals *Globals) error {
return nil
}
type RmiCmd struct {
Arg string `arg required`
}
func (cmd *RmiCmd) Run(globals *Globals) error {
return nil
}
type RunCmd struct {
Arg string `arg required`
}
func (cmd *RunCmd) Run(globals *Globals) error {
return nil
}
type SaveCmd struct {
Arg string `arg required`
}
func (cmd *SaveCmd) Run(globals *Globals) error {
return nil
}
type SearchCmd struct {
Arg string `arg required`
}
func (cmd *SearchCmd) Run(globals *Globals) error {
return nil
}
type StartCmd struct {
Arg string `arg required`
}
func (cmd *StartCmd) Run(globals *Globals) error {
return nil
}
type StatsCmd struct {
Arg string `arg required`
}
func (cmd *StatsCmd) Run(globals *Globals) error {
return nil
}
type StopCmd struct {
Arg string `arg required`
}
func (cmd *StopCmd) Run(globals *Globals) error {
return nil
}
type TagCmd struct {
Arg string `arg required`
}
func (cmd *TagCmd) Run(globals *Globals) error {
return nil
}
type TopCmd struct {
Arg string `arg required`
}
func (cmd *TopCmd) Run(globals *Globals) error {
return nil
}
type UnpauseCmd struct {
Arg string `arg required`
}
func (cmd *UnpauseCmd) Run(globals *Globals) error {
return nil
}
type UpdateCmd struct {
Arg string `arg required`
}
func (cmd *UpdateCmd) Run(globals *Globals) error {
return nil
}
type VersionCmd struct {
Arg string `arg required`
}
func (cmd *VersionCmd) Run(globals *Globals) error {
return nil
}
type WaitCmd struct {
Arg string `arg required`
}
func (cmd *WaitCmd) Run(globals *Globals) error {
return nil
}
+64 -157
View File
@@ -2,177 +2,84 @@
package main
import (
"fmt"
"os"
"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."`
type Globals 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 --tls-verify"`
TLSCACert string `name:"tls-ca-cert" help:"Trust certs signed only by this CA" default:"~/.docker/ca.pem" type:"path"`
TLSCert string `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"`
}
func (a *AttachCmd) Run() error {
fmt.Printf("Attaching to: %v\n", a.Container)
fmt.Printf("SigProxy: %v\n", a.SigProxy)
return nil
}
type CLI struct {
Globals
VersionFlag bool `name:"version" help:"Print version information and quit"`
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"`
Attach AttachCmd `cmd help:"Attach local standard input, output, and error streams to a running container"`
Build BuildCmd `cmd help:"Build an image from a Dockerfile"`
Commit CommitCmd `cmd help:"Create a new image from a container's changes"`
Cp CpCmd `cmd help:"Copy files/folders between a container and the local filesystem"`
Create CreateCmd `cmd help:"Create a new container"`
Deploy DeployCmd `cmd help:"Deploy a new stack or update an existing stack"`
Diff DiffCmd `cmd help:"Inspect changes to files or directories on a container's filesystem"`
Events EventsCmd `cmd help:"Get real time events from the server"`
Exec ExecCmd `cmd help:"Run a command in a running container"`
Export ExportCmd `cmd help:"Export a container's filesystem as a tar archive"`
History HistoryCmd `cmd help:"Show the history of an image"`
Images ImagesCmd `cmd help:"List images"`
Import ImportCmd `cmd help:"Import the contents from a tarball to create a filesystem image"`
Info InfoCmd `cmd help:"Display system-wide information"`
Inspect InspectCmd `cmd help:"Return low-level information on Docker objects"`
Kill KillCmd `cmd help:"Kill one or more running containers"`
Load LoadCmd `cmd help:"Load an image from a tar archive or STDIN"`
Login LoginCmd `cmd help:"Log in to a Docker registry"`
Logout LogoutCmd `cmd help:"Log out from a Docker registry"`
Logs LogsCmd `cmd help:"Fetch the logs of a container"`
Pause PauseCmd `cmd help:"Pause all processes within one or more containers"`
Port PortCmd `cmd help:"List port mappings or a specific mapping for the container"`
Ps PsCmd `cmd help:"List containers"`
Pull PullCmd `cmd help:"Pull an image or a repository from a registry"`
Push PushCmd `cmd help:"Push an image or a repository to a registry"`
Rename RenameCmd `cmd help:"Rename a container"`
Restart RestartCmd `cmd help:"Restart one or more containers"`
Rm RmCmd `cmd help:"Remove one or more containers"`
Rmi RmiCmd `cmd help:"Remove one or more images"`
Run RunCmd `cmd help:"Run a command in a new container"`
Save SaveCmd `cmd help:"Save one or more images to a tar archive (streamed to STDOUT by default)"`
Search SearchCmd `cmd help:"Search the Docker Hub for images"`
Start StartCmd `cmd help:"Start one or more stopped containers"`
Stats StatsCmd `cmd help:"Display a live stream of container(s) resource usage statistics"`
Stop StopCmd `cmd help:"Stop one or more running containers"`
Tag TagCmd `cmd help:"Create a tag TARGET_IMAGE that refers to SOURCE_IMAGE"`
Top TopCmd `cmd help:"Display the running processes of a container"`
Unpause UnpauseCmd `cmd help:"Unpause all processes within one or more containers"`
Update UpdateCmd `cmd help:"Update configuration of one or more containers"`
Version VersionCmd `cmd help:"Show the Docker version information"`
Wait WaitCmd `cmd help:"Block until one or more containers stop, then print their exit codes"`
}
func main() {
cmd := kong.Parse(&cli,
cli := CLI{}
parser := kong.Must(&cli,
kong.Name("docker"),
kong.Description("A self-sufficient runtime for containers"),
kong.UsageOnError(),
kong.HelpOptions(kong.HelpPrinterOptions{
Compact: true,
}),
//
kong.Hook(&cli.VersionFlag, func(ctx *kong.Context, path *kong.Path) error {
ctx.App.Printf("1.0.0").Exit(0)
return nil
}))
var err error
switch cmd {
case "attach <container>":
fmt.Println(cli.Config)
err = cli.Attach.Run()
default:
panic("unsupported command " + cmd)
}
kong.FatalIfErrorf(err)
err := parser.Run(os.Args[1:], "ofo")
parser.FatalIfErrorf(err)
}
+110 -25
View File
@@ -42,15 +42,37 @@ func (p *Path) Node() *Node {
// Context contains the current parse context.
type Context struct {
App *Kong
Path []*Path // A trace through parsed nodes.
Args []string // Original command-line arguments.
Error error // Error that occurred during trace, if any.
App *Kong
// A trace through parsed nodes.
Path []*Path
// Original command-line arguments.
Args []string
// Error that occurred during trace, if any.
Error error
values map[*Value]reflect.Value // Temporary values during tracing.
scan *Scanner
}
// Trace path of "args" through the gammar tree.
//
// The returned Context will include a Path of all commands, arguments, positionals and flags.
//
// Note that this will not modify the target grammar. Call Apply() to do so.
func Trace(k *Kong, args []string) (*Context, error) {
c := &Context{
App: k,
Args: args,
Path: []*Path{
{App: k.Model, Flags: k.Model.Flags},
},
values: map[*Value]reflect.Value{},
scan: Scan(args...),
}
c.Error = c.trace(&c.App.Model.Node)
return c, c.traceResolvers()
}
// Value returns the value for a particular path element.
func (c *Context) Value(path *Path) reflect.Value {
switch {
@@ -78,25 +100,6 @@ func (c *Context) Selected() *Node {
return selected
}
// Trace path of "args" through the gammar tree.
//
// The returned Context will include a Path of all commands, arguments, positionals and flags.
//
// Note that this will not modify the target grammar. Call Apply() to do so.
func Trace(k *Kong, args []string) (*Context, error) {
c := &Context{
App: k,
Args: args,
Path: []*Path{
{App: k.Model, Flags: k.Model.Flags},
},
values: map[*Value]reflect.Value{},
scan: Scan(args...),
}
c.Error = c.trace(&c.App.Model.Node)
return c, c.traceResolvers()
}
// Empty returns true if there were no arguments provided.
func (c *Context) Empty() bool {
for _, path := range c.Path {
@@ -153,7 +156,8 @@ func (c *Context) Flags() (flags []*Flag) {
}
// Command returns the full command path.
func (c *Context) Command() (command []string) {
func (c *Context) Command() string {
command := []string{}
for _, trace := range c.Path {
switch {
case trace.Positional != nil:
@@ -166,7 +170,7 @@ func (c *Context) Command() (command []string) {
command = append(command, trace.Command.Name)
}
}
return
return strings.Join(command, " ")
}
// FlagValue returns the set value of a flag, if it was encountered and exists.
@@ -432,6 +436,87 @@ func (c *Context) parseFlag(flags []*Flag, match string) (err error) {
return fmt.Errorf("unknown flag %s", match)
}
// Run executes the corresponding Run(params...) method on the target command selected by the parsed args.
//
// The target Run() method must exist and have the type signature "Run(params...) error".
func (c *Context) Run(params ...interface{}) (err error) {
defer catch(&err)
expectedRunSignature, err := c.validateRun(&c.App.Model.Node, nil)
if err != nil {
return err
}
if expectedRunSignature.NumIn() != len(params) {
return fmt.Errorf("expected %d params but received %d; does not match target Run() signature of %s",
expectedRunSignature.NumIn(), len(params), expectedRunSignature)
}
for i, param := range params {
if reflect.TypeOf(param) != expectedRunSignature.In(i) {
return fmt.Errorf("param %d is of type %s but should be of type %s to match target Run() signature of %s",
i, reflect.TypeOf(param), expectedRunSignature.In(i), expectedRunSignature)
}
}
node := c.Selected()
if node == nil {
return fmt.Errorf("no command selected")
}
method, err := getRunMethod(node.Target)
if err != nil {
return err
}
_, err = c.Apply()
if err != nil {
return err
}
reflectedParams := []reflect.Value{}
for _, param := range params {
reflectedParams = append(reflectedParams, reflect.ValueOf(param))
}
result := method.Call(reflectedParams)
if result[0].IsNil() {
return nil
}
return result[0].Interface().(error)
}
// Validate that all commands have Run() methods and that their signatures are the same.
func (c *Context) validateRun(node *Node, signature reflect.Type) (reflect.Type, error) {
if node.Leaf() {
method, err := getRunMethod(node.Target)
if err != nil {
return nil, err
}
if signature == nil {
signature = method.Type()
} else if signature != method.Type() {
return nil, fmt.Errorf("Run() methods are not consistent on %s, expected %s but got %s", node.Target.Type(), signature, method.Type())
}
if signature.NumOut() != 1 || signature.Out(0) != expectedRunReturnSignature {
return nil, fmt.Errorf("Run() method on %s should return (error)", node.Target.Type())
}
}
for _, child := range node.Children {
if childSignature, err := c.validateRun(child, signature); err != nil {
return nil, err
} else if signature == nil {
signature = childSignature
}
}
return signature, nil
}
func getRunMethod(value reflect.Value) (reflect.Value, error) {
method := value.MethodByName("Run")
if !method.IsValid() {
if value.CanAddr() {
method = value.Addr().MethodByName("Run")
}
if !method.IsValid() {
return method, fmt.Errorf("no Run() method on %s", value.Type())
}
}
return method, nil
}
func checkMissingFlags(flags []*Flag) error {
missing := []string{}
for _, flag := range flags {
+2 -2
View File
@@ -14,9 +14,9 @@ func Parse(cli interface{}, options ...Option) string {
panic(err)
}
App = parser
cmd, err := parser.Parse(os.Args[1:])
ctx, err := parser.Parse(os.Args[1:])
parser.FatalIfErrorf(err)
return cmd
return ctx.Command()
}
// FatalIfErrorf terminates with an error message if err != nil.
+7 -5
View File
@@ -1,10 +1,12 @@
package kong
package kong_test
import (
"bytes"
"testing"
"github.com/stretchr/testify/require"
"github.com/alecthomas/kong"
)
func TestHelp(t *testing.T) {
@@ -37,10 +39,10 @@ func TestHelp(t *testing.T) {
w := bytes.NewBuffer(nil)
exited := false
app := mustNew(t, &cli,
Name("test-app"),
Description("A test app."),
Writers(w, w),
Exit(func(int) {
kong.Name("test-app"),
kong.Description("A test app."),
kong.Writers(w, w),
kong.Exit(func(int) {
exited = true
panic(true) // Panic to fake "exit".
}),
+16 -27
View File
@@ -9,6 +9,10 @@ import (
"strings"
)
var (
expectedRunReturnSignature = reflect.TypeOf((*error)(nil)).Elem()
)
// Error reported by Kong.
type Error struct{ msg string }
@@ -121,47 +125,32 @@ func (k *Kong) extraFlags() []*Flag {
return []*Flag{helpFlag}
}
// 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(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(k.helpOptions, ctx)
}
// Parse arguments into target.
//
// The returned "command" is a space separated path to the final selected command, if any. Commands appear as
// the command name while positional arguments are the argument name surrounded by "<argument>".
// The return Context can be used to further inspect the parsed command-line, to format help, to find the
// selected command, to run command Run() methods, and so on. See Context and README for more information.
//
// Will return a ParseError if a *semantically* invalid command-line is encountered (as opposed to a syntactically
// invalid one, which will report a normal error).
func (k *Kong) Parse(args []string) (command string, err error) {
func (k *Kong) Parse(args []string) (ctx *Context, err error) {
defer catch(&err)
ctx, err := Trace(k, args)
ctx, err = Trace(k, args)
if err != nil {
return "", err
return nil, err
}
if err = k.applyHooks(ctx); err != nil {
return "", &ParseError{error: err, Context: ctx}
return nil, &ParseError{error: err, Context: ctx}
}
if ctx.Error != nil {
return "", &ParseError{error: ctx.Error, Context: ctx}
return nil, &ParseError{error: ctx.Error, Context: ctx}
}
if err = ctx.Validate(); err != nil {
return "", &ParseError{error: err, Context: ctx}
return nil, &ParseError{error: err, Context: ctx}
}
return ctx.Apply()
if _, err = ctx.Apply(); err != nil {
return nil, &ParseError{error: err, Context: ctx}
}
return ctx, nil
}
func (k *Kong) applyHooks(ctx *Context) error {
+62 -26
View File
@@ -1,23 +1,26 @@
package kong
package kong_test
import (
"bytes"
"fmt"
"strings"
"testing"
"github.com/stretchr/testify/require"
"github.com/alecthomas/kong"
)
func mustNew(t *testing.T, cli interface{}, options ...Option) *Kong {
func mustNew(t *testing.T, cli interface{}, options ...kong.Option) *kong.Kong {
t.Helper()
options = append([]Option{
Name("test"),
Exit(func(int) {
options = append([]kong.Option{
kong.Name("test"),
kong.Exit(func(int) {
t.Helper()
t.Fatalf("unexpected exit()")
}),
}, options...)
parser, err := New(cli, options...)
parser, err := kong.New(cli, options...)
require.NoError(t, err)
return parser
}
@@ -33,9 +36,9 @@ func TestPositionalArguments(t *testing.T) {
} `kong:"cmd"`
}
p := mustNew(t, &cli)
cmd, err := p.Parse([]string{"user", "create", "10", "Alec", "Thomas"})
ctx, err := p.Parse([]string{"user", "create", "10", "Alec", "Thomas"})
require.NoError(t, err)
require.Equal(t, "user create <id> <first> <last>", cmd)
require.Equal(t, "user create <id> <first> <last>", ctx.Command())
t.Run("Missing", func(t *testing.T) {
_, err := p.Parse([]string{"user", "create", "10"})
require.Error(t, err)
@@ -69,10 +72,10 @@ func TestBranchingArgument(t *testing.T) {
} `kong:"cmd,help='User management.'"`
}
p := mustNew(t, &cli)
cmd, err := p.Parse([]string{"user", "10", "delete"})
ctx, err := p.Parse([]string{"user", "10", "delete"})
require.NoError(t, err)
require.Equal(t, 10, cli.User.ID.ID)
require.Equal(t, "user <id> delete", cmd)
require.Equal(t, "user <id> delete", ctx.Command())
t.Run("Missing", func(t *testing.T) {
_, err = p.Parse([]string{"user"})
require.Error(t, err)
@@ -143,7 +146,7 @@ func TestUnsupportedFieldErrors(t *testing.T) {
var cli struct {
Keys struct{}
}
_, err := New(&cli)
_, err := kong.New(&cli)
require.Error(t, err)
}
@@ -154,7 +157,7 @@ func TestMatchingArgField(t *testing.T) {
} `kong:"arg"`
}
_, err := New(&cli)
_, err := kong.New(&cli)
require.Error(t, err)
}
@@ -164,7 +167,7 @@ func TestCantMixPositionalAndBranches(t *testing.T) {
Command struct {
} `kong:"cmd"`
}
_, err := New(&cli)
_, err := kong.New(&cli)
require.Error(t, err)
}
@@ -220,7 +223,7 @@ func TestInvalidRequiredAfterOptional(t *testing.T) {
Name string `kong:"arg"`
}
_, err := New(&cli)
_, err := kong.New(&cli)
require.Error(t, err)
}
@@ -288,7 +291,7 @@ func TestCommandMissingTagIsInvalid(t *testing.T) {
var cli struct {
One struct{}
}
_, err := New(&cli)
_, err := kong.New(&cli)
require.Error(t, err)
}
@@ -299,7 +302,7 @@ func TestDuplicateFlag(t *testing.T) {
Flag bool
} `kong:"cmd"`
}
_, err := New(&cli)
_, err := kong.New(&cli)
require.Error(t, err)
}
@@ -312,7 +315,7 @@ func TestDuplicateFlagOnPeerCommandIsOkay(t *testing.T) {
Flag bool
} `kong:"cmd"`
}
_, err := New(&cli)
_, err := kong.New(&cli)
require.NoError(t, err)
}
@@ -324,10 +327,10 @@ func TestTraceErrorPartiallySucceeds(t *testing.T) {
} `kong:"cmd"`
}
p := mustNew(t, &cli)
ctx, err := Trace(p, []string{"one", "bad"})
ctx, err := kong.Trace(p, []string{"one", "bad"})
require.NoError(t, err)
require.Error(t, ctx.Error)
require.Equal(t, []string{"one"}, ctx.Command())
require.Equal(t, "one", ctx.Command())
}
func TestHooks(t *testing.T) {
@@ -353,13 +356,13 @@ func TestHooks(t *testing.T) {
{"Flag", "one --three=three", values{true, "", "three"}},
{"ArgAndFlag", "one two --three=three", values{true, "two", "three"}},
}
setOne := func(ctx *Context, path *Path) error { hooked.one = true; return nil }
setTwo := func(ctx *Context, path *Path) error { hooked.two = ctx.Value(path).String(); return nil }
setThree := func(ctx *Context, path *Path) error { hooked.three = ctx.Value(path).String(); return nil }
setOne := func(ctx *kong.Context, path *kong.Path) error { hooked.one = true; return nil }
setTwo := func(ctx *kong.Context, path *kong.Path) error { hooked.two = ctx.Value(path).String(); return nil }
setThree := func(ctx *kong.Context, path *kong.Path) error { hooked.three = ctx.Value(path).String(); return nil }
p := mustNew(t, &cli,
Hook(&cli.One, setOne),
Hook(&cli.One.Two, setTwo),
Hook(&cli.One.Three, setThree))
kong.Hook(&cli.One, setOne),
kong.Hook(&cli.One.Two, setTwo),
kong.Hook(&cli.One.Three, setThree))
for _, test := range tests {
hooked = values{}
@@ -450,7 +453,40 @@ func TestSliceWithDisabledSeparator(t *testing.T) {
func TestMultilineMessage(t *testing.T) {
w := &bytes.Buffer{}
var cli struct{}
p := mustNew(t, &cli, Writers(w, w))
p := mustNew(t, &cli, kong.Writers(w, w))
p.Printf("hello\nworld")
require.Equal(t, "test: hello\n world\n", w.String())
}
type cmdWithRun struct {
Arg string `arg:""`
}
func (c *cmdWithRun) Run(key string) error {
c.Arg += key
if key == "ERROR" {
return fmt.Errorf("ERROR")
}
return nil
}
type grammarWithRun struct {
One cmdWithRun `cmd:""`
Two cmdWithRun `cmd:""`
}
func TestRun(t *testing.T) {
cli := &grammarWithRun{}
p := mustNew(t, cli)
ctx, err := p.Parse([]string{"one", "two"})
require.NoError(t, err)
err = ctx.Run("hello")
require.NoError(t, err)
require.Equal(t, "twohello", cli.One.Arg)
ctx, err = p.Parse([]string{"two", "three"})
require.NoError(t, err)
err = ctx.Run("ERROR")
require.Error(t, err)
}
+12 -10
View File
@@ -1,4 +1,4 @@
package kong
package kong_test
import (
"net/url"
@@ -7,13 +7,15 @@ import (
"time"
"github.com/stretchr/testify/require"
"github.com/alecthomas/kong"
)
func TestValueMapper(t *testing.T) {
var cli struct {
Flag string
}
k := mustNew(t, &cli, ValueMapper(&cli.Flag, testMooMapper{}))
k := mustNew(t, &cli, kong.ValueMapper(&cli.Flag, testMooMapper{}))
_, err := k.Parse(nil)
require.NoError(t, err)
require.Equal(t, "", cli.Flag)
@@ -26,7 +28,7 @@ func TestNamedMapper(t *testing.T) {
var cli struct {
Flag string `type:"moo"`
}
k := mustNew(t, &cli, NamedMapper("moo", testMooMapper{}))
k := mustNew(t, &cli, kong.NamedMapper("moo", testMooMapper{}))
_, err := k.Parse(nil)
require.NoError(t, err)
require.Equal(t, "", cli.Flag)
@@ -39,7 +41,7 @@ type testMooMapper struct {
text string
}
func (t testMooMapper) Decode(ctx *DecodeContext, target reflect.Value) error {
func (t testMooMapper) Decode(ctx *kong.DecodeContext, target reflect.Value) error {
if t.text == "" {
target.SetString("MOO")
} else {
@@ -73,14 +75,14 @@ func TestDurationMapper(t *testing.T) {
}
func TestSplitEscaped(t *testing.T) {
require.Equal(t, []string{"a", "b"}, SplitEscaped("a,b", ','))
require.Equal(t, []string{"a,b", "c"}, SplitEscaped(`a\,b,c`, ','))
require.Equal(t, []string{"a", "b"}, kong.SplitEscaped("a,b", ','))
require.Equal(t, []string{"a,b", "c"}, kong.SplitEscaped(`a\,b,c`, ','))
}
func TestJoinEscaped(t *testing.T) {
require.Equal(t, `a,b`, JoinEscaped([]string{"a", "b"}, ','))
require.Equal(t, `a\,b,c`, JoinEscaped([]string{`a,b`, `c`}, ','))
require.Equal(t, JoinEscaped(SplitEscaped(`a\,b,c`, ','), ','), `a\,b,c`)
require.Equal(t, `a,b`, kong.JoinEscaped([]string{"a", "b"}, ','))
require.Equal(t, `a\,b,c`, kong.JoinEscaped([]string{`a,b`, `c`}, ','))
require.Equal(t, kong.JoinEscaped(kong.SplitEscaped(`a\,b,c`, ','), ','), `a\,b,c`)
}
func TestMapWithNamedTypes(t *testing.T) {
@@ -88,7 +90,7 @@ func TestMapWithNamedTypes(t *testing.T) {
TypedValue map[string]string `type:":moo"`
TypedKey map[string]string `type:"upper:"`
}
k := mustNew(t, &cli, NamedMapper("moo", testMooMapper{}), NamedMapper("upper", testUppercaseMapper{}))
k := mustNew(t, &cli, kong.NamedMapper("moo", testMooMapper{}), kong.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)
+5
View File
@@ -43,6 +43,11 @@ type Node struct {
Argument *Value // Populated when Type is ArgumentNode.
}
// Leaf returns true if this Node is a leaf node.
func (n *Node) Leaf() bool {
return len(n.Children) == 0
}
// Find a command/argument/flag by pointer to its field.
//
// Returns nil if not found. Panics if ptr is not a pointer.
+1 -1
View File
@@ -1,4 +1,4 @@
package kong
package kong_test
import (
"testing"
+2
View File
@@ -92,6 +92,8 @@ func Writers(stdout, stderr io.Writer) Option {
}
// HookFunc is a callback tied to a field of the grammar, called before a value is applied.
//
// "ctx" is the current parse Context, "path" is the Path entry corresponding to the hooked value.
type HookFunc func(ctx *Context, path *Path) error
// Hook to apply before a command, flag or positional argument is encountered.
+5 -3
View File
@@ -1,4 +1,4 @@
package kong
package kong_test
import (
"encoding/json"
@@ -7,11 +7,13 @@ import (
"testing"
"github.com/stretchr/testify/require"
"github.com/alecthomas/kong"
)
func TestOptions(t *testing.T) {
var cli struct{}
p, err := New(&cli, Name("name"), Description("description"), Writers(nil, nil), Exit(nil))
p, err := kong.New(&cli, kong.Name("name"), kong.Description("description"), kong.Writers(nil, nil), kong.Exit(nil))
require.NoError(t, err)
require.Equal(t, "name", p.Model.Name)
require.Equal(t, "description", p.Model.Help)
@@ -42,7 +44,7 @@ func TestConfigLoading(t *testing.T) {
err = json.NewEncoder(second).Encode(&cli)
require.NoError(t, err)
p := mustNew(t, &cli, Configuration(JSON, first.Name(), second.Name()))
p := mustNew(t, &cli, kong.Configuration(kong.JSON, first.Name(), second.Name()))
_, err = p.Parse(nil)
require.NoError(t, err)
require.Equal(t, "first", cli.Flag)
+17 -15
View File
@@ -1,4 +1,4 @@
package kong
package kong_test
import (
"os"
@@ -7,6 +7,8 @@ import (
"testing"
"github.com/stretchr/testify/require"
"github.com/alecthomas/kong"
)
type envMap map[string]string
@@ -23,7 +25,7 @@ func tempEnv(env envMap) func() {
}
}
func newEnvParser(t *testing.T, cli interface{}, env envMap) (*Kong, func()) {
func newEnvParser(t *testing.T, cli interface{}, env envMap) (*kong.Kong, func()) {
t.Helper()
restoreEnv := tempEnv(env)
parser := mustNew(t, cli)
@@ -111,10 +113,10 @@ func TestJSONBasic(t *testing.T) {
"slice_with_commas": ["a,b", "c"]
}`
r, err := JSON(strings.NewReader(json))
r, err := kong.JSON(strings.NewReader(json))
require.NoError(t, err)
parser := mustNew(t, &cli, Resolver(r))
parser := mustNew(t, &cli, kong.Resolver(r))
_, err = parser.Parse([]string{})
require.NoError(t, err)
require.Equal(t, "🍕", cli.String)
@@ -127,14 +129,14 @@ func TestResolvedValueTriggersHooks(t *testing.T) {
var cli struct {
Int int
}
resolver := func(context *Context, parent *Path, flag *Flag) (string, error) {
resolver := func(context *kong.Context, parent *kong.Path, flag *kong.Flag) (string, error) {
if flag.Name == "int" {
return "1", nil
}
return "", nil
}
hooked := 0
p := mustNew(t, &cli, Resolver(resolver), Hook(&cli.Int, func(ctx *Context, path *Path) error {
p := mustNew(t, &cli, kong.Resolver(resolver), kong.Hook(&cli.Int, func(ctx *kong.Context, path *kong.Path) error {
hooked++
return nil
}))
@@ -152,7 +154,7 @@ func TestResolvedValueTriggersHooks(t *testing.T) {
type testUppercaseMapper struct{}
func (testUppercaseMapper) Decode(ctx *DecodeContext, target reflect.Value) error {
func (testUppercaseMapper) Decode(ctx *kong.DecodeContext, target reflect.Value) error {
value := ctx.Scan.PopValue("lowercase")
target.SetString(strings.ToUpper(value))
return nil
@@ -167,7 +169,7 @@ func TestResolversWithMappers(t *testing.T) {
defer restoreEnv()
parser := mustNew(t, &cli,
NamedMapper("upper", testUppercaseMapper{}),
kong.NamedMapper("upper", testUppercaseMapper{}),
)
_, err := parser.Parse([]string{})
require.NoError(t, err)
@@ -179,14 +181,14 @@ func TestResolverWithBool(t *testing.T) {
Bool bool
}
resolver := func(context *Context, parent *Path, flag *Flag) (string, error) {
resolver := func(context *kong.Context, parent *kong.Path, flag *kong.Flag) (string, error) {
if flag.Name == "bool" {
return "true", nil
}
return "", nil
}
p := mustNew(t, &cli, Resolver(resolver))
p := mustNew(t, &cli, kong.Resolver(resolver))
_, err := p.Parse(nil)
require.NoError(t, err)
@@ -198,21 +200,21 @@ func TestLastResolverWins(t *testing.T) {
Int []int
}
var first ResolverFunc = func(context *Context, parent *Path, flag *Flag) (string, error) {
var first kong.ResolverFunc = func(context *kong.Context, parent *kong.Path, flag *kong.Flag) (string, error) {
if flag.Name == "int" {
return "1", nil
}
return "", nil
}
var second ResolverFunc = func(context *Context, parent *Path, flag *Flag) (string, error) {
var second kong.ResolverFunc = func(context *kong.Context, parent *kong.Path, flag *kong.Flag) (string, error) {
if flag.Name == "int" {
return "2", nil
}
return "", nil
}
p := mustNew(t, &cli, Resolver(first), Resolver(second))
p := mustNew(t, &cli, kong.Resolver(first), kong.Resolver(second))
_, err := p.Parse(nil)
require.NoError(t, err)
require.Equal(t, []int{2}, cli.Int)
@@ -223,13 +225,13 @@ func TestResolverSatisfiesRequired(t *testing.T) {
var cli struct {
Int int `required`
}
resolver := func(context *Context, parent *Path, flag *Flag) (string, error) {
resolver := func(context *kong.Context, parent *kong.Path, flag *kong.Flag) (string, error) {
if flag.Name == "int" {
return "1", nil
}
return "", nil
}
_, err := mustNew(t, &cli, Resolver(resolver)).Parse(nil)
_, err := mustNew(t, &cli, kong.Resolver(resolver)).Parse(nil)
require.NoError(t, err)
require.Equal(t, 1, cli.Int)
}
+5 -3
View File
@@ -1,9 +1,11 @@
package kong
package kong_test
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/alecthomas/kong"
)
func TestDefaultValueForOptionalArg(t *testing.T) {
@@ -42,7 +44,7 @@ func TestBadString(t *testing.T) {
var cli struct {
Numbers string `kong:"default='yay'n"`
}
_, err := New(&cli)
_, err := kong.New(&cli)
require.Error(t, err)
}
@@ -50,7 +52,7 @@ func TestNoQuoteEnd(t *testing.T) {
var cli struct {
Numbers string `kong:"default='yay"`
}
_, err := New(&cli)
_, err := kong.New(&cli)
require.Error(t, err)
}