Clean up disparity between Context and Kong.

Previously, there was a confusing mix of functionality shared between
the two wherein you would need to use the Kong type for printing errors,
etc. but it did not have access to the context in order to print
context-sensitive usage information. This has been fixed.

Additionally, there are now fuzzy correction suggestions for flags and
commands

Also added a server example which shows how Kong can be used for parsing
in interactive shells. Run with:

    $ go run ./_examples/server/*.go

Then interact with:

    $ ssh -p 6740 127.0.0.1
This commit is contained in:
Alec Thomas
2018-06-25 14:45:20 +10:00
parent f7acb2b389
commit 6408010083
20 changed files with 489 additions and 130 deletions
+14 -19
View File
@@ -23,7 +23,7 @@
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(HelpPrinterOptions)` and `Help(HelpFunc)` - customising help](#helpoptionshelpprinteroptions-and-helphelpfunc---customising-help)
1. [`ConfigureHelp(HelpOptions)` and `Help(HelpFunc)` - customising help](#configurehelphelpoptions-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)
@@ -61,12 +61,12 @@ var CLI struct {
}
func main() {
cmd := kong.Parse(&CLI)
switch cmd {
ctx := kong.Parse(&CLI)
switch ctx.Command() {
case "rm <path>":
case "ls":
default:
panic(cmd)
panic(ctx.Command())
}
}
```
@@ -142,12 +142,12 @@ var CLI struct {
}
func main() {
cmd := kong.Parse(&CLI)
switch cmd {
ctx := kong.Parse(&CLI)
switch ctx.Command() {
case "rm <path>":
case "ls":
default:
panic(cmd)
panic(ctx.Command())
}
}
```
@@ -197,15 +197,10 @@ var cli struct {
}
func main() {
parser := kong.Must(&cli)
// Parse and apply the command-line.
ctx, err := parser.Parse(os.Args[1:])
parser.FatalIfErrorf(err)
ctx := kong.Parse(&cli)
// Call the Run() method of the selected parsed command.
err = ctx.Run(cli.Debug)
parser.FatalIfErrorf(err)
ctx.FatalIfErrorf(err)
}
```
@@ -350,7 +345,7 @@ Both can coexist with standard Tag parsing.
| `short:"X"` | Short name, if flag. |
| `required` | If present, flag/arg is required. |
| `optional` | If present, flag/arg is optional. |
| `hidden` | If present, flag is hidden. |
| `hidden` | If present, command or flag is hidden. |
| `format:"X"` | Format for parsing input, if supported. |
| `sep:"X"` | Separator for sequences (defaults to ","). May be `none` to disable splitting. |
@@ -406,11 +401,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(HelpPrinterOptions)` and `Help(HelpFunc)` - customising help
### `ConfigureHelp(HelpOptions)` and `Help(HelpFunc)` - customising help
The default help output is usually sufficient, but if not there are two solutions.
1. Use `HelpOptions(HelpPrinterOptions)` to configure how help is formatted (see [HelpPrinterOptions](https://godoc.org/github.com/alecthomas/kong#HelpPrinterOptions) for details).
1. Use `ConfigureHelp(HelpOptions)` to configure how help is formatted (see [HelpOptions](https://godoc.org/github.com/alecthomas/kong#HelpOptions) 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
@@ -426,7 +421,7 @@ app := kong.Must(&CLI, kong.Hook(&CLI.Debug, func(ctx *Context, path *Path) erro
}))
```
Note: it is generally more advisable to use an imperative approach to building command-lines, eg.
Note: it is generally less verbose to use an imperative approach to building command-lines, eg.
```go
if CLI.Debug {
@@ -434,7 +429,7 @@ if CLI.Debug {
}
```
But under some circumstances, hooks are the right choice.
But under some circumstances, hooks can be useful.
### Other options
+5 -9
View File
@@ -2,8 +2,6 @@
package main
import (
"os"
"github.com/alecthomas/kong"
)
@@ -68,20 +66,18 @@ type CLI struct {
func main() {
cli := CLI{}
parser := kong.Must(&cli,
ctx := kong.Parse(&cli,
kong.Name("docker"),
kong.Description("A self-sufficient runtime for containers"),
kong.UsageOnError(),
kong.HelpOptions(kong.HelpPrinterOptions{
kong.ConfigureHelp(kong.HelpOptions{
Compact: true,
}),
//
kong.Hook(&cli.VersionFlag, func(ctx *kong.Context, path *kong.Path) error {
ctx.App.Printf("1.0.0").Exit(0)
ctx.Printf("1.0.0").Exit(0)
return nil
}))
ctx, err := parser.Parse(os.Args[1:])
parser.FatalIfErrorf(err)
err = ctx.Run(&cli.Globals)
parser.FatalIfErrorf(err)
err := ctx.Run(&cli.Globals)
ctx.FatalIfErrorf(err)
}
+49
View File
@@ -0,0 +1,49 @@
// nolint: govet
package main
import (
"fmt"
"github.com/alecthomas/kong"
)
// Ensure the grammar compiles.
var _ = kong.Must(&grammar{})
// Server interface.
type grammar struct {
Help helpCmd `cmd help:"Show help."`
Question helpCmd `cmd hidden name:"?" help:"Show help."`
Status statusCmd `cmd help:"Show server status."`
}
type statusCmd struct {
Verbose bool `short:"v" help:"Show verbose status information."`
}
func (s *statusCmd) Run(ctx *kong.Context) error {
ctx.Printf("OK")
return nil
}
type helpCmd struct {
Command []string `arg optional help:"Show help on command."`
}
// Run shows help.
func (h *helpCmd) Run(realCtx *kong.Context) error {
ctx, err := kong.Trace(realCtx.Kong, h.Command)
if err != nil {
return err
}
if ctx.Error != nil {
return ctx.Error
}
err = ctx.PrintUsage(false)
if err != nil {
return err
}
fmt.Fprintln(realCtx.Stdout)
return nil
}
+154
View File
@@ -0,0 +1,154 @@
package main
import (
"errors"
"fmt"
"io"
"log"
"os"
"strings"
"golang.org/x/crypto/ssh/terminal"
"github.com/chzyer/readline"
"github.com/gliderlabs/ssh"
"github.com/google/shlex"
"github.com/kr/pty"
"github.com/alecthomas/colour"
"github.com/alecthomas/kong"
)
type context struct {
kong *kong.Context
rl *readline.Instance
}
func handle(log *log.Logger, s ssh.Session) error {
log.Printf("New SSH")
sshPty, _, isPty := s.Pty()
if !isPty {
return errors.New("No PTY requested")
}
log.Printf("Using TERM=%s width=%d height=%d", sshPty.Term, sshPty.Window.Width, sshPty.Window.Height)
cpty, tty, err := pty.Open()
if err != nil {
return err
}
defer tty.Close()
state, err := terminal.GetState(int(cpty.Fd()))
if err != nil {
return err
}
defer terminal.Restore(int(cpty.Fd()), state)
colour.Fprintln(tty, "^BWelcome!^R")
go io.Copy(cpty, s)
go io.Copy(s, cpty)
parser, err := buildShellParser(tty)
if err != nil {
return err
}
rl, err := readline.NewEx(&readline.Config{
Prompt: "> ",
Stderr: tty,
Stdout: tty,
Stdin: tty,
FuncOnWidthChanged: func(f func()) {},
FuncMakeRaw: func() error {
_, err := terminal.MakeRaw(int(cpty.Fd())) // nolint: govet
return err
},
FuncExitRaw: func() error { return nil },
})
if err != nil {
return err
}
log.Printf("Loop")
for {
tty.Sync()
var line string
line, err = rl.Readline()
if err != nil {
if err == io.EOF {
return nil
}
return err
}
var args []string
args, err := shlex.Split(string(line))
if err != nil {
parser.Errorf("%s", err)
continue
}
var ctx *kong.Context
ctx, err = parser.Parse(args)
if err != nil {
parser.Errorf("%s", err)
if err, ok := err.(*kong.ParseError); ok {
log.Println(err.Error())
err.Context.PrintUsage(false)
}
continue
}
err = ctx.Run(ctx)
if err != nil {
parser.Errorf("%s", err)
continue
}
}
}
func buildShellParser(tty *os.File) (*kong.Kong, error) {
parser, err := kong.New(&grammar{},
kong.Name(""),
kong.Description("Example using Kong for interactive command parsing."),
kong.Writers(tty, tty),
kong.Exit(func(int) {}),
kong.ConfigureHelp(kong.HelpOptions{
NoAppSummary: true,
}),
kong.NoDefaultHelp(),
)
return parser, err
}
func handlerWithError(handle func(log *log.Logger, s ssh.Session) error) ssh.Handler {
return func(s ssh.Session) {
prefix := fmt.Sprintf("%s->%s ", s.LocalAddr(), s.RemoteAddr())
l := log.New(os.Stdout, prefix, log.LstdFlags)
err := handle(l, s)
if err != nil {
log.Printf("error: %s", err)
s.Exit(1)
} else {
log.Printf("Bye")
s.Exit(0)
}
}
}
var cli struct {
HostKey string `type:"existingfile" help:"SSH host key to use." default:"./_examples/server/server_rsa_key"`
Bind string `help:"Bind address for server." default:"127.0.0.1:6740"`
}
func main() {
ctx := kong.Parse(&cli,
kong.Name("server"),
kong.Description("A network server using Kong for interacting with clients."))
ssh.Handle(handlerWithError(handle))
log.Printf("SSH listening on: %s", cli.Bind)
log.Printf("Using host key: %s", cli.HostKey)
log.Println()
parts := strings.Split(cli.Bind, ":")
log.Printf("Connect with: ssh -p %s %s", parts[1], parts[0])
log.Println()
err := ssh.ListenAndServe(cli.Bind, nil, ssh.HostKeyFile(cli.HostKey))
ctx.FatalIfErrorf(err)
}
+27
View File
@@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAxK1QbPQYibc0VFZdc6GHPka5oTeGnXsMZiyX4JbHUJl1oNDB
Xg9NATbft+q/6ZDjyVEQhq8xgLvYFkL8qBLt/6UAaOub0RtmPqQwmxoNLWuXFFwn
YlBKApQ4gf58/jOcGPpdEwfjkjwLb536Bni25XMU4cYrdIvQIhtMaK+Zqja/3MAC
V6ZRZZCd8hABJqaZu+3mRElnF1d7gfMvA/hhaq7Y5VYr8rUrBHHimrT/GEP6aCbf
Npo43SfRnUDu2+EAK7vA9cM8fg/O/mvNR1/9jzOWyr8ZDLD6R6iaZCQ2anEPcqim
MCOtibSeVOms07Zcn/TSsgmfGwa8rQkpXVJA5wIDAQABAoIBAQCTUccGdajTrzkx
WyfQ71NgoJV3XyIkYAEfn5N8FTTi+LAVb4kILanemP3mw55RE8isCV65pA0OgqYP
tsmOE+/WKAAwlxs1/LIPhekqpM7uEMMv6v9NMxrc562UIc36kyn/w7loAebCqNtg
FhMsOcu1/wfLPidau0eB5LTNTYtq5RuSKxoindvatk+Zmk0KjoA+f25MlwCEHQNr
ygpopclyTHVln2t3t0o97/a7dHa9+HlmVO4GxWvTTiqtcFErTGWtTUW8aeZFS83r
p+JZNxReSJ2MlM9bm15wJ0L86GTeYZQiaNuC1XETbFvX+9Ffkl+7EtsdYDLV1N6r
/eOP2f0hAoGBAOKVDHmnru7SVmbH5BI8HW5sd6IVztZM3+BKzy6AaPc4/FgG6MOr
bJyFbmN8S/gVi4OYOJXgfaeKcycYJFTjXUSnNRQ9eT0MseD9SxzEXV7RGtnvudiu
pbRmtBRtf3e4beaN9X4SfWk4+Frw7B8UsPXwV/09s7AW279cES565IkfAoGBAN42
TQSC/jQmJBpGSnqWfqQtKPTSKFoZ/JQbxoy9QckAMqVSFwBBgwQYr4MbI7WyjPRE
s43kpf+Sq/++fc3hyk5XAWBKscK0KLs0HBRZyLybQYI+f4/x2giVzKeRRNVa9nQa
VdIU8i+eO2AUzG690q89HGkRBsfXekjq5kXC9Cc5AoGAUY0b5F16FPMXrf6cFAQX
A7t+g5Qd0fvxSCUk1LPbE8Aq8vPpqyN0ABH2XVBLd4spn7+V/jvCfh7Su2txCCyd
USxtak+F53c+PqBr/HqgsJPKek5SMa8KbRfaENAoZMq4o5bMmQfGo6yhlvnHwpgL
6TkMMlWW6vYPOZzFglkxEDkCgYApT78Rz6ii2VRs7hR6pe/1Zc/vdAK8fYhPoLpQ
//5y9+5yfch467UH1e8LWMhSx1cdMoiPIKsb0JDZgviwhgGufs5qsHhL0mKgKxft
UKPZLKQJKsVcZYI7hl396Sv63mZjP2IlJG/CGpC/VB6NmAzLN3lIrzmrfYvmcoVN
AumRQQKBgB4Uznek3nq5ZQf93+ucvXpf0bLRqq1va7jYmRosyiCN52grbclj5uMq
vxr1uoqmgtCfqdgUbm0s+kVK6D4bPkz4HQOSXImXhLs8/KdixYfPLSarxYvTxZKg
mMF1XqcdRwSv3RZYtUbbF7dYQYsC1/ZKXvtPldeoDmTZ+U7b2qbE
-----END RSA PRIVATE KEY-----
+1
View File
@@ -0,0 +1 @@
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDErVBs9BiJtzRUVl1zoYc+RrmhN4adewxmLJfglsdQmXWg0MFeD00BNt+36r/pkOPJURCGrzGAu9gWQvyoEu3/pQBo65vRG2Y+pDCbGg0ta5cUXCdiUEoClDiB/nz+M5wY+l0TB+OSPAtvnfoGeLblcxThxit0i9AiG0xor5mqNr/cwAJXplFlkJ3yEAEmppm77eZESWcXV3uB8y8D+GFqrtjlVivytSsEceKatP8YQ/poJt82mjjdJ9GdQO7b4QAru8D1wzx+D87+a81HX/2PM5bKvxkMsPpHqJpkJDZqcQ9yqKYwI62JtJ5U6azTtlyf9NKyCZ8bBrytCSldUkDn
+5 -3
View File
@@ -23,13 +23,15 @@ var cli struct {
}
func main() {
cmd := kong.Parse(&cli, kong.Description("A shell-like example app."),
ctx := kong.Parse(&cli,
kong.Name("shell"),
kong.Description("A shell-like example app."),
kong.UsageOnError(),
kong.HelpOptions(kong.HelpPrinterOptions{
kong.ConfigureHelp(kong.HelpOptions{
Compact: true,
Summary: true,
}))
switch cmd {
switch ctx.Command() {
case "rm <path>":
fmt.Println(cli.Rm.Paths, cli.Rm.Force, cli.Rm.Recursive)
+5 -4
View File
@@ -24,7 +24,7 @@ func build(k *Kong, ast interface{}) (app *Application, err error) {
if len(node.Positional) > 0 && len(node.Children) > 0 {
return nil, fmt.Errorf("can't mix positional arguments and branching arguments on %T", ast)
}
app.Node = *node
app.Node = node
app.Node.Flags = append(extraFlags, app.Node.Flags...)
return app, nil
}
@@ -63,13 +63,13 @@ func buildNode(k *Kong, v reflect.Value, typ NodeType, seenFlags map[string]bool
ft := field.field
fv := field.value
name := ft.Tag.Get("name")
tag := parseTag(fv, ft)
name := tag.Name
if name == "" {
name = strings.ToLower(dashedString(ft.Name))
}
tag := parseTag(fv, ft)
// Nested structs are either commands or args.
if ft.Type.Kind() == reflect.Struct && (tag.Cmd || tag.Arg) {
typ := CommandNode
@@ -105,6 +105,7 @@ func buildChild(k *Kong, node *Node, typ NodeType, v reflect.Value, ft reflect.S
child := buildNode(k, fv, typ, seenFlags)
child.Parent = node
child.Help = tag.Help
child.Hidden = tag.Hidden
// A branching argument. This is a bit hairy, as we let buildNode() do the parsing, then check that
// a positional argument is provided to the child, and move it to the branching argument field.
+59 -15
View File
@@ -29,7 +29,7 @@ type Path struct {
func (p *Path) Node() *Node {
switch {
case p.App != nil:
return &p.App.Node
return p.App.Node
case p.Argument != nil:
return p.Argument
@@ -42,7 +42,7 @@ func (p *Path) Node() *Node {
// Context contains the current parse context.
type Context struct {
App *Kong
*Kong
// A trace through parsed nodes.
Path []*Path
// Original command-line arguments.
@@ -61,7 +61,7 @@ type Context struct {
// 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,
Kong: k,
Args: args,
Path: []*Path{
{App: k.Model, Flags: k.Model.Flags},
@@ -69,7 +69,7 @@ func Trace(k *Kong, args []string) (*Context, error) {
values: map[*Value]reflect.Value{},
scan: Scan(args...),
}
c.Error = c.trace(&c.App.Model.Node)
c.Error = c.trace(c.Model.Node)
return c, c.traceResolvers()
}
@@ -120,7 +120,7 @@ func (c *Context) Validate() error {
// Check the terminal node.
node := c.Selected()
if node == nil {
node = &c.App.Model.Node
node = c.Model.Node
}
// Find deepest positional argument so we can check if all required positionals have been provided.
@@ -216,7 +216,10 @@ func (c *Context) reset(node *Node) error {
func (c *Context) trace(node *Node) (err error) { // nolint: gocyclo
positional := 0
flags := append(c.Flags(), node.Flags...)
flags := []*Flag{}
for _, group := range node.AllFlags(false) {
flags = append(flags, group...)
}
for !c.scan.Peek().IsEOL() {
token := c.scan.Peek()
@@ -285,6 +288,8 @@ func (c *Context) trace(node *Node) (err error) { // nolint: gocyclo
return fmt.Errorf("unexpected flag argument %q", token.Value)
case PositionalArgumentToken:
candidates := []string{}
// Ensure we've consumed all positional arguments.
if positional < len(node.Positional) {
arg := node.Positional[positional]
@@ -302,6 +307,9 @@ func (c *Context) trace(node *Node) (err error) { // nolint: gocyclo
// After positional arguments have been consumed, check commands next...
for _, branch := range node.Children {
if branch.Type == CommandNode {
candidates = append(candidates, branch.Name)
}
if branch.Type == CommandNode && branch.Name == token.Value {
c.scan.Pop()
c.Path = append(c.Path, &Path{
@@ -327,8 +335,8 @@ func (c *Context) trace(node *Node) (err error) { // nolint: gocyclo
}
}
}
return fmt.Errorf("unexpected positional argument %s", token)
return findPotentialCandidates(token.Value, candidates, "unexpected argument %s", token)
default:
return fmt.Errorf("unexpected token %s", token)
}
@@ -336,9 +344,28 @@ func (c *Context) trace(node *Node) (err error) { // nolint: gocyclo
return nil
}
func findPotentialCandidates(needle string, haystack []string, format string, args ...interface{}) error {
if len(haystack) == 0 {
return fmt.Errorf(format, args...)
}
closestCandidates := []string{}
for _, candidate := range haystack {
if strings.HasPrefix(candidate, needle) || levenshtein(candidate, needle) <= 2 {
closestCandidates = append(closestCandidates, fmt.Sprintf("%q", candidate))
}
}
prefix := fmt.Sprintf(format, args...)
if len(closestCandidates) == 1 {
return fmt.Errorf("%s, did you mean %s?", prefix, closestCandidates[0])
} else if len(closestCandidates) > 1 {
return fmt.Errorf("%s, did you mean one of %s?", prefix, strings.Join(closestCandidates, ", "))
}
return fmt.Errorf("%s", prefix)
}
// Walk through flags from existing nodes in the path.
func (c *Context) traceResolvers() error {
if len(c.App.resolvers) == 0 {
if len(c.resolvers) == 0 {
return nil
}
@@ -349,7 +376,7 @@ func (c *Context) traceResolvers() error {
if _, ok := c.values[flag.Value]; ok {
continue
}
for _, resolver := range c.App.resolvers {
for _, resolver := range c.resolvers {
s, err := resolver(c, path, flag)
if err != nil {
return err
@@ -386,7 +413,7 @@ func (c *Context) getValue(value *Value) reflect.Value {
// Apply traced context to the target grammar.
func (c *Context) Apply() (string, error) {
err := c.reset(&c.App.Model.Node)
err := c.reset(c.Model.Node)
if err != nil {
return "", err
}
@@ -420,8 +447,15 @@ func (c *Context) Apply() (string, error) {
func (c *Context) parseFlag(flags []*Flag, match string) (err error) {
defer catch(&err)
candidates := []string{}
for _, flag := range flags {
if "-"+string(flag.Short) != match && "--"+flag.Name != match {
long := "--" + flag.Name
short := "-" + string(flag.Short)
candidates = append(candidates, long)
if flag.Short != 0 {
candidates = append(candidates, short)
}
if short != match && long != match {
continue
}
// Found a matching flag.
@@ -433,7 +467,7 @@ func (c *Context) parseFlag(flags []*Flag, match string) (err error) {
c.Path = append(c.Path, &Path{Flag: flag})
return nil
}
return fmt.Errorf("unknown flag %s", match)
return findPotentialCandidates(match, candidates, "unknown flag %s", match)
}
// Run executes the corresponding Run(params...) method on the target command selected by the parsed args.
@@ -441,7 +475,7 @@ func (c *Context) parseFlag(flags []*Flag, match string) (err error) {
// 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)
expectedRunSignature, err := c.validateRun(c.Model.Node, nil)
if err != nil {
return err
}
@@ -478,6 +512,16 @@ func (c *Context) Run(params ...interface{}) (err error) {
return result[0].Interface().(error)
}
// PrintUsage to Kong's stdout.
//
// If summary is true, a summarised version of the help will be output.
func (c *Context) PrintUsage(summary bool) error {
options := c.helpOptions
options.Summary = summary
_ = c.help(options, c)
return nil
}
// 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() {
@@ -554,12 +598,12 @@ func checkMissingChildren(node *Node) error {
}
if len(missing) == 1 {
return fmt.Errorf("%q should be followed by %s", node.Path(), missing[0])
return fmt.Errorf("expected %s", missing[0])
}
if len(missing) > 5 {
missing = append(missing[:5], "...")
}
return fmt.Errorf("%q should be followed by one of %s", node.Path(), strings.Join(missing, ", "))
return fmt.Errorf("expected one of %s", strings.Join(missing, ", "))
}
// If we're missing any positionals and they're required, return an error.
+2 -30
View File
@@ -4,41 +4,13 @@ import (
"os"
)
// App is the default global instance. It is populated by Parse().
var App *Kong
// Parse constructs a new parser and parses the default command-line.
func Parse(cli interface{}, options ...Option) string {
func Parse(cli interface{}, options ...Option) *Context {
parser, err := New(cli, options...)
if err != nil {
panic(err)
}
App = parser
ctx, err := parser.Parse(os.Args[1:])
parser.FatalIfErrorf(err)
return ctx.Command()
}
// FatalIfErrorf terminates with an error message if err != nil.
func FatalIfErrorf(err error, args ...interface{}) {
if App == nil {
panic("call kong.Parse() before using kong.FatalIfErrorf()")
}
App.FatalIfErrorf(err, args...)
}
// Errorf writes a message to Kong.Stderr with the application name prefixed.
func Errorf(format string, args ...interface{}) {
if App == nil {
panic("call kong.Parse() before using kong.Errorf()")
}
App.Errorf(format, args...)
}
// Printf writes a message to Kong.Stdout with the application name prefixed.
func Printf(format string, args ...interface{}) {
if App == nil {
panic("call kong.Parse() before using kong.Printf()")
}
App.Printf(format, args...)
return ctx
}
+3
View File
@@ -31,6 +31,9 @@ func guessWidth(w io.Writer) int {
uintptr(unsafe.Pointer(&dimensions)), // nolint: gas
0, 0, 0,
); err == 0 {
if dimensions[1] == 0 {
return 80
}
return int(dimensions[1])
}
}
+31 -25
View File
@@ -13,38 +13,43 @@ const (
defaultColumnPadding = 4
)
// HelpPrinterOptions for HelpPrinters.
type HelpPrinterOptions struct {
// HelpOptions for HelpPrinters.
type HelpOptions struct {
// Don't print top-level usage summary.
NoAppSummary bool
// Write a one-line summary of the context.
Summary bool
// Write help in a more compact form, but still fully-specified.
// Write help in a more compact, but still fully-specified, form.
Compact bool
}
// HelpPrinter is used to print context-sensitive help.
type HelpPrinter func(options HelpPrinterOptions, ctx *Context) error
type HelpPrinter func(options HelpOptions, ctx *Context) error
// DefaultHelpPrinter is the default HelpPrinter.
func DefaultHelpPrinter(options HelpPrinterOptions, ctx *Context) error {
func DefaultHelpPrinter(options HelpOptions, ctx *Context) error {
if ctx.Empty() {
options.Summary = false
}
w := newHelpWriter(ctx, options)
selected := ctx.Selected()
if selected == nil {
printApp(w, ctx.App.Model)
printApp(w, ctx.Model)
} else {
printCommand(w, ctx.App.Model, selected)
printCommand(w, ctx.Model, selected)
}
return w.Write(ctx.App.Stdout)
return w.Write(ctx.Stdout)
}
func printApp(w *helpWriter, app *Application) {
w.Printf("Usage: %s", app.Summary())
printNodeDetail(w, &app.Node)
cmds := app.Leaves()
if len(cmds) > 0 {
if !w.NoAppSummary {
w.Printf("Usage: %s%s", app.Name, app.Summary())
}
printNodeDetail(w, app.Node)
cmds := app.Leaves(true)
if len(cmds) > 0 && app.HelpFlag != nil {
w.Print("")
if w.Summary {
w.Printf(`Run "%s --help" for more information.`, app.Name)
@@ -55,11 +60,12 @@ func printApp(w *helpWriter, app *Application) {
}
func printCommand(w *helpWriter, app *Application, cmd *Command) {
w.Printf("Usage: %s %s", app.Name, cmd.Summary())
if !w.NoAppSummary {
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())
if w.Summary && app.HelpFlag != nil {
w.Printf(`Run "%s %s --help" for more information.`, app.Name, cmd.FullPath())
}
}
@@ -76,12 +82,12 @@ func printNodeDetail(w *helpWriter, node *Node) {
w.Print("Arguments:")
writePositionals(w.Indent(), node.Positional)
}
if flags := node.AllFlags(); len(flags) > 0 {
if flags := node.AllFlags(true); len(flags) > 0 {
w.Print("")
w.Print("Flags:")
writeFlags(w.Indent(), flags)
}
cmds := node.Leaves()
cmds := node.Leaves(true)
if len(cmds) > 0 {
w.Print("")
w.Print("Commands:")
@@ -114,16 +120,16 @@ type helpWriter struct {
indent string
width int
lines *[]string
HelpPrinterOptions
HelpOptions
}
func newHelpWriter(ctx *Context, options HelpPrinterOptions) *helpWriter {
func newHelpWriter(ctx *Context, options HelpOptions) *helpWriter {
lines := []string{}
w := &helpWriter{
indent: "",
width: guessWidth(ctx.App.Stdout),
lines: &lines,
HelpPrinterOptions: options,
indent: "",
width: guessWidth(ctx.Stdout),
lines: &lines,
HelpOptions: options,
}
return w
}
@@ -137,7 +143,7 @@ func (h *helpWriter) Print(text string) {
}
func (h *helpWriter) Indent() *helpWriter {
return &helpWriter{indent: h.indent + " ", lines: h.lines, width: h.width - 2, HelpPrinterOptions: h.HelpPrinterOptions}
return &helpWriter{indent: h.indent + " ", lines: h.lines, width: h.width - 2, HelpOptions: h.HelpOptions}
}
func (h *helpWriter) String() string {
+5 -4
View File
@@ -49,13 +49,13 @@ func TestHelp(t *testing.T) {
)
t.Run("Full", func(t *testing.T) {
require.Panics(t, func() {
require.PanicsWithValue(t, true, func() {
_, err := app.Parse([]string{"--help"})
require.NoError(t, err)
})
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.
@@ -85,17 +85,18 @@ Run "test-app <command> --help" for more information on a command.
t.Run("Selected", func(t *testing.T) {
exited = false
w.Truncate(0)
require.Panics(t, func() {
require.PanicsWithValue(t, true, func() {
_, err := app.Parse([]string{"two", "hello", "--help"})
require.NoError(t, err)
})
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.
Flags:
--help Show context-sensitive help.
--string=STRING A string flag.
--bool A bool flag with very long help that wraps a lot and is
verbose and is really verbose.
+24 -7
View File
@@ -42,13 +42,15 @@ type Kong struct {
Stdout io.Writer
Stderr io.Writer
before map[reflect.Value]HookFunc
resolvers []ResolverFunc
registry *Registry
before map[reflect.Value]HookFunc
resolvers []ResolverFunc
registry *Registry
noDefaultHelp bool
usageOnError bool
help HelpPrinter
helpOptions HelpPrinterOptions
helpOptions HelpOptions
helpFlag *Flag
// Set temporarily by Options. These are applied after build().
postBuildOptions []Option
@@ -83,6 +85,7 @@ func New(grammar interface{}, options ...Option) (*Kong, error) {
}
model.Name = filepath.Base(os.Args[0])
k.Model = model
k.Model.HelpFlag = k.helpFlag
for _, option := range k.postBuildOptions {
if err := option(k); err != nil {
@@ -121,6 +124,7 @@ func (k *Kong) extraFlags() []*Flag {
k.Exit(1)
return nil
})
k.helpFlag = helpFlag
_ = hook(k)
return []*Flag{helpFlag}
}
@@ -182,8 +186,15 @@ func (k *Kong) applyHooks(ctx *Context) error {
return nil
}
func formatMultilineMessage(w io.Writer, leader string, format string, args ...interface{}) {
func formatMultilineMessage(w io.Writer, leaders []string, format string, args ...interface{}) {
lines := strings.Split(fmt.Sprintf(format, args...), "\n")
leader := ""
for _, l := range leaders {
if l == "" {
continue
}
leader += l + ": "
}
fmt.Fprintf(w, "%s%s\n", leader, lines[0])
for _, line := range lines[1:] {
fmt.Fprintf(w, "%*s%s\n", len(leader), " ", line)
@@ -192,16 +203,22 @@ func formatMultilineMessage(w io.Writer, leader string, format string, args ...i
// Printf writes a message to Kong.Stdout with the application name prefixed.
func (k *Kong) Printf(format string, args ...interface{}) *Kong {
formatMultilineMessage(k.Stdout, k.Model.Name+": ", format, args...)
formatMultilineMessage(k.Stdout, []string{k.Model.Name}, format, args...)
return k
}
// Errorf writes a message to Kong.Stderr with the application name prefixed.
func (k *Kong) Errorf(format string, args ...interface{}) *Kong {
formatMultilineMessage(k.Stderr, k.Model.Name+": error: ", format, args...)
formatMultilineMessage(k.Stderr, []string{k.Model.Name, "error"}, format, args...)
return k
}
// Fatalf writes a message to Kong.Stderr with the application name prefixed then exits with a non-zero status.
func (k *Kong) Fatalf(format string, args ...interface{}) {
k.Errorf(format, args...)
k.Exit(1)
}
// FatalIfErrorf terminates with an error message if err != nil.
func (k *Kong) FatalIfErrorf(err error, args ...interface{}) {
if err == nil {
+22
View File
@@ -207,6 +207,28 @@ func TestOptionalArg(t *testing.T) {
require.NoError(t, err)
}
func TestOptionalArgWithDefault(t *testing.T) {
var cli struct {
Arg string `kong:"arg,optional,default='moo'"`
}
parser := mustNew(t, &cli)
_, err := parser.Parse([]string{})
require.NoError(t, err)
require.Equal(t, "moo", cli.Arg)
}
func TestArgWithDefaultIsOptional(t *testing.T) {
var cli struct {
Arg string `kong:"arg,default='moo'"`
}
parser := mustNew(t, &cli)
_, err := parser.Parse([]string{})
require.NoError(t, err)
require.Equal(t, "moo", cli.Arg)
}
func TestRequiredArg(t *testing.T) {
var cli struct {
Arg string `kong:"arg"`
+39
View File
@@ -0,0 +1,39 @@
package kong
import "unicode/utf8"
// https://en.wikibooks.org/wiki/Algorithm_Implementation/Strings/Levenshtein_distance#Go
// License: https://creativecommons.org/licenses/by-sa/3.0/
func levenshtein(a, b string) int {
f := make([]int, utf8.RuneCountInString(b)+1)
for j := range f {
f[j] = j
}
for _, ca := range a {
j := 1
fj1 := f[0] // fj1 is the value of f[j - 1] in last iteration
f[0]++
for _, cb := range b {
mn := min(f[j]+1, f[j-1]+1) // delete & insert
if cb != ca {
mn = min(mn, fj1+1) // change
} else {
mn = min(mn, fj1) // matched
}
fj1, f[j] = f[j], mn // save f[j] to fj1(j is about to increase), update f[j] to mn
j++
}
}
return f[len(f)-1]
}
func min(a, b int) int {
if a <= b {
return a
}
return b
}
+35 -11
View File
@@ -9,7 +9,8 @@ import (
// Application is the root of the Kong model.
type Application struct {
Node
*Node
// Help flag, if the NoDefaultHelp() option is not specified.
HelpFlag *Flag
}
@@ -35,6 +36,7 @@ type Node struct {
Parent *Node
Name string
Help string
Hidden bool
Flags []*Flag
Positional []*Positional
Children []*Node
@@ -72,20 +74,33 @@ func (n *Node) findNode(key reflect.Value) *Node {
}
// AllFlags returns flags from all ancestor branches encountered.
func (n *Node) AllFlags() (out [][]*Flag) {
//
// If "hide" is true hidden flags will be omitted.
func (n *Node) AllFlags(hide bool) (out [][]*Flag) {
if n.Parent != nil {
out = append(out, n.Parent.AllFlags()...)
out = append(out, n.Parent.AllFlags(hide)...)
}
if len(n.Flags) > 0 {
out = append(out, n.Flags)
group := []*Flag{}
for _, flag := range n.Flags {
if !hide || !flag.Hidden {
group = append(group, flag)
}
}
if len(group) > 0 {
out = append(out, group)
}
return
}
// Leaves returns the leaf commands/arguments under Node.
func (n *Node) Leaves() (out []*Node) {
//
// If "hidden" is true hidden leaves will be omitted.
func (n *Node) Leaves(hide bool) (out []*Node) {
var walk func(n *Node)
walk = func(n *Node) {
if hide && n.Hidden {
return
}
if len(n.Children) == 0 && n.Type != ApplicationNode {
out = append(out, n)
}
@@ -112,10 +127,10 @@ func (n *Node) Depth() int {
return depth
}
// Summary help string for the node.
// Summary help string for the node (not including application name).
func (n *Node) Summary() string {
summary := n.Path()
if flags := n.FlagSummary(); flags != "" {
if flags := n.FlagSummary(true); flags != "" {
summary += " " + flags
}
args := []string{}
@@ -131,10 +146,10 @@ func (n *Node) Summary() string {
}
// FlagSummary for the node.
func (n *Node) FlagSummary() string {
func (n *Node) FlagSummary(hide bool) string {
required := []string{}
count := 0
for _, group := range n.AllFlags() {
for _, group := range n.AllFlags(hide) {
for _, flag := range group {
count++
if flag.Required {
@@ -145,13 +160,22 @@ func (n *Node) FlagSummary() string {
return strings.Join(required, " ")
}
// FullPath is like Path() but includes the Application root node.
func (n *Node) FullPath() string {
root := n
for root.Parent != nil {
root = root.Parent
}
return strings.TrimSpace(root.Name + " " + n.Path())
}
// Path through ancestors to this Node.
func (n *Node) Path() (out string) {
if n.Parent != nil {
out += " " + n.Parent.Path()
}
switch n.Type {
case ApplicationNode, CommandNode:
case CommandNode:
out += " " + n.Name
case ArgumentNode:
out += " " + "<" + n.Name + ">"
+1 -1
View File
@@ -20,7 +20,7 @@ func TestModelApplicationCommands(t *testing.T) {
}
p := mustNew(t, &cli)
actual := []string{}
for _, cmd := range p.Model.Leaves() {
for _, cmd := range p.Model.Leaves(false) {
actual = append(actual, cmd.Path())
}
require.Equal(t, []string{"one two", "one three <four>"}, actual)
+2 -2
View File
@@ -121,8 +121,8 @@ func Help(help HelpPrinter) Option {
}
}
// HelpOptions sets the HelpPrinterOptions to use for printing help.
func HelpOptions(options HelpPrinterOptions) Option {
// ConfigureHelp sets the HelpOptions to use for printing help.
func ConfigureHelp(options HelpOptions) Option {
return func(k *Kong) error {
k.helpOptions = options
return nil
+6
View File
@@ -14,6 +14,7 @@ type Tag struct {
Arg bool
Required bool
Optional bool
Name string
Help string
Type string
Default string
@@ -125,6 +126,11 @@ func parseTag(fv reflect.Value, ft reflect.StructField) *Tag {
t.Required = required
t.Optional = optional
t.Default = t.Get("default")
// Arguments with defaults are always optional.
if t.Arg && t.Default != "" {
t.Optional = true
}
t.Name = t.Get("name")
t.Help = t.Get("help")
t.Type = t.Get("type")
t.Env = t.Get("env")