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:
@@ -23,7 +23,7 @@
|
|||||||
1. [`Configuration(loader, paths...)` - load defaults from configuration files](#configurationloader-paths---load-defaults-from-configuration-files)
|
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. [`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. [`*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. [`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)
|
1. [Other options](#other-options)
|
||||||
|
|
||||||
@@ -61,12 +61,12 @@ var CLI struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
cmd := kong.Parse(&CLI)
|
ctx := kong.Parse(&CLI)
|
||||||
switch cmd {
|
switch ctx.Command() {
|
||||||
case "rm <path>":
|
case "rm <path>":
|
||||||
case "ls":
|
case "ls":
|
||||||
default:
|
default:
|
||||||
panic(cmd)
|
panic(ctx.Command())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -142,12 +142,12 @@ var CLI struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
cmd := kong.Parse(&CLI)
|
ctx := kong.Parse(&CLI)
|
||||||
switch cmd {
|
switch ctx.Command() {
|
||||||
case "rm <path>":
|
case "rm <path>":
|
||||||
case "ls":
|
case "ls":
|
||||||
default:
|
default:
|
||||||
panic(cmd)
|
panic(ctx.Command())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -197,15 +197,10 @@ var cli struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
parser := kong.Must(&cli)
|
ctx := kong.Parse(&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.
|
// Call the Run() method of the selected parsed command.
|
||||||
err = ctx.Run(cli.Debug)
|
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. |
|
| `short:"X"` | Short name, if flag. |
|
||||||
| `required` | If present, flag/arg is required. |
|
| `required` | If present, flag/arg is required. |
|
||||||
| `optional` | If present, flag/arg is optional. |
|
| `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. |
|
| `format:"X"` | Format for parsing input, if supported. |
|
||||||
| `sep:"X"` | Separator for sequences (defaults to ","). May be `none` to disable splitting. |
|
| `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)`.
|
3. `TypeMapper(reflect.Type, Mapper)`.
|
||||||
4. `ValueMapper(interface{}, Mapper)`, passing in a pointer to a field of the grammar.
|
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.
|
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.
|
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
|
### `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
|
```go
|
||||||
if CLI.Debug {
|
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
|
### Other options
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,6 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/alecthomas/kong"
|
"github.com/alecthomas/kong"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -68,20 +66,18 @@ type CLI struct {
|
|||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
cli := CLI{}
|
cli := CLI{}
|
||||||
parser := kong.Must(&cli,
|
ctx := kong.Parse(&cli,
|
||||||
kong.Name("docker"),
|
kong.Name("docker"),
|
||||||
kong.Description("A self-sufficient runtime for containers"),
|
kong.Description("A self-sufficient runtime for containers"),
|
||||||
kong.UsageOnError(),
|
kong.UsageOnError(),
|
||||||
kong.HelpOptions(kong.HelpPrinterOptions{
|
kong.ConfigureHelp(kong.HelpOptions{
|
||||||
Compact: true,
|
Compact: true,
|
||||||
}),
|
}),
|
||||||
//
|
//
|
||||||
kong.Hook(&cli.VersionFlag, func(ctx *kong.Context, path *kong.Path) error {
|
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
|
return nil
|
||||||
}))
|
}))
|
||||||
ctx, err := parser.Parse(os.Args[1:])
|
err := ctx.Run(&cli.Globals)
|
||||||
parser.FatalIfErrorf(err)
|
ctx.FatalIfErrorf(err)
|
||||||
err = ctx.Run(&cli.Globals)
|
|
||||||
parser.FatalIfErrorf(err)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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-----
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDErVBs9BiJtzRUVl1zoYc+RrmhN4adewxmLJfglsdQmXWg0MFeD00BNt+36r/pkOPJURCGrzGAu9gWQvyoEu3/pQBo65vRG2Y+pDCbGg0ta5cUXCdiUEoClDiB/nz+M5wY+l0TB+OSPAtvnfoGeLblcxThxit0i9AiG0xor5mqNr/cwAJXplFlkJ3yEAEmppm77eZESWcXV3uB8y8D+GFqrtjlVivytSsEceKatP8YQ/poJt82mjjdJ9GdQO7b4QAru8D1wzx+D87+a81HX/2PM5bKvxkMsPpHqJpkJDZqcQ9yqKYwI62JtJ5U6azTtlyf9NKyCZ8bBrytCSldUkDn
|
||||||
@@ -23,13 +23,15 @@ var cli struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
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.UsageOnError(),
|
||||||
kong.HelpOptions(kong.HelpPrinterOptions{
|
kong.ConfigureHelp(kong.HelpOptions{
|
||||||
Compact: true,
|
Compact: true,
|
||||||
Summary: true,
|
Summary: true,
|
||||||
}))
|
}))
|
||||||
switch cmd {
|
switch ctx.Command() {
|
||||||
case "rm <path>":
|
case "rm <path>":
|
||||||
fmt.Println(cli.Rm.Paths, cli.Rm.Force, cli.Rm.Recursive)
|
fmt.Println(cli.Rm.Paths, cli.Rm.Force, cli.Rm.Recursive)
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ func build(k *Kong, ast interface{}) (app *Application, err error) {
|
|||||||
if len(node.Positional) > 0 && len(node.Children) > 0 {
|
if len(node.Positional) > 0 && len(node.Children) > 0 {
|
||||||
return nil, fmt.Errorf("can't mix positional arguments and branching arguments on %T", ast)
|
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...)
|
app.Node.Flags = append(extraFlags, app.Node.Flags...)
|
||||||
return app, nil
|
return app, nil
|
||||||
}
|
}
|
||||||
@@ -63,13 +63,13 @@ func buildNode(k *Kong, v reflect.Value, typ NodeType, seenFlags map[string]bool
|
|||||||
ft := field.field
|
ft := field.field
|
||||||
fv := field.value
|
fv := field.value
|
||||||
|
|
||||||
name := ft.Tag.Get("name")
|
tag := parseTag(fv, ft)
|
||||||
|
|
||||||
|
name := tag.Name
|
||||||
if name == "" {
|
if name == "" {
|
||||||
name = strings.ToLower(dashedString(ft.Name))
|
name = strings.ToLower(dashedString(ft.Name))
|
||||||
}
|
}
|
||||||
|
|
||||||
tag := parseTag(fv, ft)
|
|
||||||
|
|
||||||
// Nested structs are either commands or args.
|
// Nested structs are either commands or args.
|
||||||
if ft.Type.Kind() == reflect.Struct && (tag.Cmd || tag.Arg) {
|
if ft.Type.Kind() == reflect.Struct && (tag.Cmd || tag.Arg) {
|
||||||
typ := CommandNode
|
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 := buildNode(k, fv, typ, seenFlags)
|
||||||
child.Parent = node
|
child.Parent = node
|
||||||
child.Help = tag.Help
|
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 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.
|
// a positional argument is provided to the child, and move it to the branching argument field.
|
||||||
|
|||||||
+59
-15
@@ -29,7 +29,7 @@ type Path struct {
|
|||||||
func (p *Path) Node() *Node {
|
func (p *Path) Node() *Node {
|
||||||
switch {
|
switch {
|
||||||
case p.App != nil:
|
case p.App != nil:
|
||||||
return &p.App.Node
|
return p.App.Node
|
||||||
|
|
||||||
case p.Argument != nil:
|
case p.Argument != nil:
|
||||||
return p.Argument
|
return p.Argument
|
||||||
@@ -42,7 +42,7 @@ func (p *Path) Node() *Node {
|
|||||||
|
|
||||||
// Context contains the current parse context.
|
// Context contains the current parse context.
|
||||||
type Context struct {
|
type Context struct {
|
||||||
App *Kong
|
*Kong
|
||||||
// A trace through parsed nodes.
|
// A trace through parsed nodes.
|
||||||
Path []*Path
|
Path []*Path
|
||||||
// Original command-line arguments.
|
// 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.
|
// Note that this will not modify the target grammar. Call Apply() to do so.
|
||||||
func Trace(k *Kong, args []string) (*Context, error) {
|
func Trace(k *Kong, args []string) (*Context, error) {
|
||||||
c := &Context{
|
c := &Context{
|
||||||
App: k,
|
Kong: k,
|
||||||
Args: args,
|
Args: args,
|
||||||
Path: []*Path{
|
Path: []*Path{
|
||||||
{App: k.Model, Flags: k.Model.Flags},
|
{App: k.Model, Flags: k.Model.Flags},
|
||||||
@@ -69,7 +69,7 @@ func Trace(k *Kong, args []string) (*Context, error) {
|
|||||||
values: map[*Value]reflect.Value{},
|
values: map[*Value]reflect.Value{},
|
||||||
scan: Scan(args...),
|
scan: Scan(args...),
|
||||||
}
|
}
|
||||||
c.Error = c.trace(&c.App.Model.Node)
|
c.Error = c.trace(c.Model.Node)
|
||||||
return c, c.traceResolvers()
|
return c, c.traceResolvers()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,7 +120,7 @@ func (c *Context) Validate() error {
|
|||||||
// Check the terminal node.
|
// Check the terminal node.
|
||||||
node := c.Selected()
|
node := c.Selected()
|
||||||
if node == nil {
|
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.
|
// 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
|
func (c *Context) trace(node *Node) (err error) { // nolint: gocyclo
|
||||||
positional := 0
|
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() {
|
for !c.scan.Peek().IsEOL() {
|
||||||
token := c.scan.Peek()
|
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)
|
return fmt.Errorf("unexpected flag argument %q", token.Value)
|
||||||
|
|
||||||
case PositionalArgumentToken:
|
case PositionalArgumentToken:
|
||||||
|
candidates := []string{}
|
||||||
|
|
||||||
// Ensure we've consumed all positional arguments.
|
// Ensure we've consumed all positional arguments.
|
||||||
if positional < len(node.Positional) {
|
if positional < len(node.Positional) {
|
||||||
arg := node.Positional[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...
|
// After positional arguments have been consumed, check commands next...
|
||||||
for _, branch := range node.Children {
|
for _, branch := range node.Children {
|
||||||
|
if branch.Type == CommandNode {
|
||||||
|
candidates = append(candidates, branch.Name)
|
||||||
|
}
|
||||||
if branch.Type == CommandNode && branch.Name == token.Value {
|
if branch.Type == CommandNode && branch.Name == token.Value {
|
||||||
c.scan.Pop()
|
c.scan.Pop()
|
||||||
c.Path = append(c.Path, &Path{
|
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:
|
default:
|
||||||
return fmt.Errorf("unexpected token %s", token)
|
return fmt.Errorf("unexpected token %s", token)
|
||||||
}
|
}
|
||||||
@@ -336,9 +344,28 @@ func (c *Context) trace(node *Node) (err error) { // nolint: gocyclo
|
|||||||
return nil
|
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.
|
// Walk through flags from existing nodes in the path.
|
||||||
func (c *Context) traceResolvers() error {
|
func (c *Context) traceResolvers() error {
|
||||||
if len(c.App.resolvers) == 0 {
|
if len(c.resolvers) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -349,7 +376,7 @@ func (c *Context) traceResolvers() error {
|
|||||||
if _, ok := c.values[flag.Value]; ok {
|
if _, ok := c.values[flag.Value]; ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for _, resolver := range c.App.resolvers {
|
for _, resolver := range c.resolvers {
|
||||||
s, err := resolver(c, path, flag)
|
s, err := resolver(c, path, flag)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -386,7 +413,7 @@ func (c *Context) getValue(value *Value) reflect.Value {
|
|||||||
|
|
||||||
// Apply traced context to the target grammar.
|
// Apply traced context to the target grammar.
|
||||||
func (c *Context) Apply() (string, error) {
|
func (c *Context) Apply() (string, error) {
|
||||||
err := c.reset(&c.App.Model.Node)
|
err := c.reset(c.Model.Node)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@@ -420,8 +447,15 @@ func (c *Context) Apply() (string, error) {
|
|||||||
|
|
||||||
func (c *Context) parseFlag(flags []*Flag, match string) (err error) {
|
func (c *Context) parseFlag(flags []*Flag, match string) (err error) {
|
||||||
defer catch(&err)
|
defer catch(&err)
|
||||||
|
candidates := []string{}
|
||||||
for _, flag := range flags {
|
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
|
continue
|
||||||
}
|
}
|
||||||
// Found a matching flag.
|
// 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})
|
c.Path = append(c.Path, &Path{Flag: flag})
|
||||||
return nil
|
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.
|
// 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".
|
// The target Run() method must exist and have the type signature "Run(params...) error".
|
||||||
func (c *Context) Run(params ...interface{}) (err error) {
|
func (c *Context) Run(params ...interface{}) (err error) {
|
||||||
defer catch(&err)
|
defer catch(&err)
|
||||||
expectedRunSignature, err := c.validateRun(&c.App.Model.Node, nil)
|
expectedRunSignature, err := c.validateRun(c.Model.Node, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -478,6 +512,16 @@ func (c *Context) Run(params ...interface{}) (err error) {
|
|||||||
return result[0].Interface().(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.
|
// 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) {
|
func (c *Context) validateRun(node *Node, signature reflect.Type) (reflect.Type, error) {
|
||||||
if node.Leaf() {
|
if node.Leaf() {
|
||||||
@@ -554,12 +598,12 @@ func checkMissingChildren(node *Node) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(missing) == 1 {
|
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 {
|
if len(missing) > 5 {
|
||||||
missing = append(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.
|
// If we're missing any positionals and they're required, return an error.
|
||||||
|
|||||||
@@ -4,41 +4,13 @@ import (
|
|||||||
"os"
|
"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.
|
// 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...)
|
parser, err := New(cli, options...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
App = parser
|
|
||||||
ctx, err := parser.Parse(os.Args[1:])
|
ctx, err := parser.Parse(os.Args[1:])
|
||||||
parser.FatalIfErrorf(err)
|
parser.FatalIfErrorf(err)
|
||||||
return ctx.Command()
|
return ctx
|
||||||
}
|
|
||||||
|
|
||||||
// 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...)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,9 @@ func guessWidth(w io.Writer) int {
|
|||||||
uintptr(unsafe.Pointer(&dimensions)), // nolint: gas
|
uintptr(unsafe.Pointer(&dimensions)), // nolint: gas
|
||||||
0, 0, 0,
|
0, 0, 0,
|
||||||
); err == 0 {
|
); err == 0 {
|
||||||
|
if dimensions[1] == 0 {
|
||||||
|
return 80
|
||||||
|
}
|
||||||
return int(dimensions[1])
|
return int(dimensions[1])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,38 +13,43 @@ const (
|
|||||||
defaultColumnPadding = 4
|
defaultColumnPadding = 4
|
||||||
)
|
)
|
||||||
|
|
||||||
// HelpPrinterOptions for HelpPrinters.
|
// HelpOptions for HelpPrinters.
|
||||||
type HelpPrinterOptions struct {
|
type HelpOptions struct {
|
||||||
|
// Don't print top-level usage summary.
|
||||||
|
NoAppSummary bool
|
||||||
|
|
||||||
// Write a one-line summary of the context.
|
// Write a one-line summary of the context.
|
||||||
Summary bool
|
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
|
Compact bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// HelpPrinter is used to print context-sensitive help.
|
// 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.
|
// DefaultHelpPrinter is the default HelpPrinter.
|
||||||
func DefaultHelpPrinter(options HelpPrinterOptions, ctx *Context) error {
|
func DefaultHelpPrinter(options HelpOptions, ctx *Context) error {
|
||||||
if ctx.Empty() {
|
if ctx.Empty() {
|
||||||
options.Summary = false
|
options.Summary = false
|
||||||
}
|
}
|
||||||
w := newHelpWriter(ctx, options)
|
w := newHelpWriter(ctx, options)
|
||||||
selected := ctx.Selected()
|
selected := ctx.Selected()
|
||||||
if selected == nil {
|
if selected == nil {
|
||||||
printApp(w, ctx.App.Model)
|
printApp(w, ctx.Model)
|
||||||
} else {
|
} 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) {
|
func printApp(w *helpWriter, app *Application) {
|
||||||
w.Printf("Usage: %s", app.Summary())
|
if !w.NoAppSummary {
|
||||||
printNodeDetail(w, &app.Node)
|
w.Printf("Usage: %s%s", app.Name, app.Summary())
|
||||||
cmds := app.Leaves()
|
}
|
||||||
if len(cmds) > 0 {
|
printNodeDetail(w, app.Node)
|
||||||
|
cmds := app.Leaves(true)
|
||||||
|
if len(cmds) > 0 && app.HelpFlag != nil {
|
||||||
w.Print("")
|
w.Print("")
|
||||||
if w.Summary {
|
if w.Summary {
|
||||||
w.Printf(`Run "%s --help" for more information.`, app.Name)
|
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) {
|
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)
|
printNodeDetail(w, cmd)
|
||||||
if w.Summary {
|
if w.Summary && app.HelpFlag != nil {
|
||||||
w.Print("")
|
w.Printf(`Run "%s %s --help" for more information.`, app.Name, cmd.FullPath())
|
||||||
w.Printf(`Run "%s %s --help" for more information.`, app.Name, cmd.Path())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,12 +82,12 @@ func printNodeDetail(w *helpWriter, node *Node) {
|
|||||||
w.Print("Arguments:")
|
w.Print("Arguments:")
|
||||||
writePositionals(w.Indent(), node.Positional)
|
writePositionals(w.Indent(), node.Positional)
|
||||||
}
|
}
|
||||||
if flags := node.AllFlags(); len(flags) > 0 {
|
if flags := node.AllFlags(true); len(flags) > 0 {
|
||||||
w.Print("")
|
w.Print("")
|
||||||
w.Print("Flags:")
|
w.Print("Flags:")
|
||||||
writeFlags(w.Indent(), flags)
|
writeFlags(w.Indent(), flags)
|
||||||
}
|
}
|
||||||
cmds := node.Leaves()
|
cmds := node.Leaves(true)
|
||||||
if len(cmds) > 0 {
|
if len(cmds) > 0 {
|
||||||
w.Print("")
|
w.Print("")
|
||||||
w.Print("Commands:")
|
w.Print("Commands:")
|
||||||
@@ -114,16 +120,16 @@ type helpWriter struct {
|
|||||||
indent string
|
indent string
|
||||||
width int
|
width int
|
||||||
lines *[]string
|
lines *[]string
|
||||||
HelpPrinterOptions
|
HelpOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
func newHelpWriter(ctx *Context, options HelpPrinterOptions) *helpWriter {
|
func newHelpWriter(ctx *Context, options HelpOptions) *helpWriter {
|
||||||
lines := []string{}
|
lines := []string{}
|
||||||
w := &helpWriter{
|
w := &helpWriter{
|
||||||
indent: "",
|
indent: "",
|
||||||
width: guessWidth(ctx.App.Stdout),
|
width: guessWidth(ctx.Stdout),
|
||||||
lines: &lines,
|
lines: &lines,
|
||||||
HelpPrinterOptions: options,
|
HelpOptions: options,
|
||||||
}
|
}
|
||||||
return w
|
return w
|
||||||
}
|
}
|
||||||
@@ -137,7 +143,7 @@ func (h *helpWriter) Print(text string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *helpWriter) Indent() *helpWriter {
|
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 {
|
func (h *helpWriter) String() string {
|
||||||
|
|||||||
+5
-4
@@ -49,13 +49,13 @@ func TestHelp(t *testing.T) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
t.Run("Full", func(t *testing.T) {
|
t.Run("Full", func(t *testing.T) {
|
||||||
require.Panics(t, func() {
|
require.PanicsWithValue(t, true, func() {
|
||||||
_, err := app.Parse([]string{"--help"})
|
_, err := app.Parse([]string{"--help"})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
})
|
})
|
||||||
require.True(t, exited)
|
require.True(t, exited)
|
||||||
t.Log(w.String())
|
t.Log(w.String())
|
||||||
require.Equal(t, `Usage: test-app --required <command>
|
require.Equal(t, `Usage: test-app --required <command>
|
||||||
|
|
||||||
A test app.
|
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) {
|
t.Run("Selected", func(t *testing.T) {
|
||||||
exited = false
|
exited = false
|
||||||
w.Truncate(0)
|
w.Truncate(0)
|
||||||
require.Panics(t, func() {
|
require.PanicsWithValue(t, true, func() {
|
||||||
_, err := app.Parse([]string{"two", "hello", "--help"})
|
_, err := app.Parse([]string{"two", "hello", "--help"})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
})
|
})
|
||||||
require.True(t, exited)
|
require.True(t, exited)
|
||||||
t.Log(w.String())
|
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.
|
Sub-sub-arg.
|
||||||
|
|
||||||
Flags:
|
Flags:
|
||||||
|
--help Show context-sensitive help.
|
||||||
--string=STRING A string flag.
|
--string=STRING A string flag.
|
||||||
--bool A bool flag with very long help that wraps a lot and is
|
--bool A bool flag with very long help that wraps a lot and is
|
||||||
verbose and is really verbose.
|
verbose and is really verbose.
|
||||||
|
|||||||
@@ -42,13 +42,15 @@ type Kong struct {
|
|||||||
Stdout io.Writer
|
Stdout io.Writer
|
||||||
Stderr io.Writer
|
Stderr io.Writer
|
||||||
|
|
||||||
before map[reflect.Value]HookFunc
|
before map[reflect.Value]HookFunc
|
||||||
resolvers []ResolverFunc
|
resolvers []ResolverFunc
|
||||||
registry *Registry
|
registry *Registry
|
||||||
|
|
||||||
noDefaultHelp bool
|
noDefaultHelp bool
|
||||||
usageOnError bool
|
usageOnError bool
|
||||||
help HelpPrinter
|
help HelpPrinter
|
||||||
helpOptions HelpPrinterOptions
|
helpOptions HelpOptions
|
||||||
|
helpFlag *Flag
|
||||||
|
|
||||||
// Set temporarily by Options. These are applied after build().
|
// Set temporarily by Options. These are applied after build().
|
||||||
postBuildOptions []Option
|
postBuildOptions []Option
|
||||||
@@ -83,6 +85,7 @@ func New(grammar interface{}, options ...Option) (*Kong, error) {
|
|||||||
}
|
}
|
||||||
model.Name = filepath.Base(os.Args[0])
|
model.Name = filepath.Base(os.Args[0])
|
||||||
k.Model = model
|
k.Model = model
|
||||||
|
k.Model.HelpFlag = k.helpFlag
|
||||||
|
|
||||||
for _, option := range k.postBuildOptions {
|
for _, option := range k.postBuildOptions {
|
||||||
if err := option(k); err != nil {
|
if err := option(k); err != nil {
|
||||||
@@ -121,6 +124,7 @@ func (k *Kong) extraFlags() []*Flag {
|
|||||||
k.Exit(1)
|
k.Exit(1)
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
k.helpFlag = helpFlag
|
||||||
_ = hook(k)
|
_ = hook(k)
|
||||||
return []*Flag{helpFlag}
|
return []*Flag{helpFlag}
|
||||||
}
|
}
|
||||||
@@ -182,8 +186,15 @@ func (k *Kong) applyHooks(ctx *Context) error {
|
|||||||
return nil
|
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")
|
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])
|
fmt.Fprintf(w, "%s%s\n", leader, lines[0])
|
||||||
for _, line := range lines[1:] {
|
for _, line := range lines[1:] {
|
||||||
fmt.Fprintf(w, "%*s%s\n", len(leader), " ", line)
|
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.
|
// Printf writes a message to Kong.Stdout with the application name prefixed.
|
||||||
func (k *Kong) Printf(format string, args ...interface{}) *Kong {
|
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
|
return k
|
||||||
}
|
}
|
||||||
|
|
||||||
// Errorf writes a message to Kong.Stderr with the application name prefixed.
|
// Errorf writes a message to Kong.Stderr with the application name prefixed.
|
||||||
func (k *Kong) Errorf(format string, args ...interface{}) *Kong {
|
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
|
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.
|
// FatalIfErrorf terminates with an error message if err != nil.
|
||||||
func (k *Kong) FatalIfErrorf(err error, args ...interface{}) {
|
func (k *Kong) FatalIfErrorf(err error, args ...interface{}) {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
|||||||
@@ -207,6 +207,28 @@ func TestOptionalArg(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
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) {
|
func TestRequiredArg(t *testing.T) {
|
||||||
var cli struct {
|
var cli struct {
|
||||||
Arg string `kong:"arg"`
|
Arg string `kong:"arg"`
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -9,7 +9,8 @@ import (
|
|||||||
|
|
||||||
// Application is the root of the Kong model.
|
// Application is the root of the Kong model.
|
||||||
type Application struct {
|
type Application struct {
|
||||||
Node
|
*Node
|
||||||
|
// Help flag, if the NoDefaultHelp() option is not specified.
|
||||||
HelpFlag *Flag
|
HelpFlag *Flag
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,6 +36,7 @@ type Node struct {
|
|||||||
Parent *Node
|
Parent *Node
|
||||||
Name string
|
Name string
|
||||||
Help string
|
Help string
|
||||||
|
Hidden bool
|
||||||
Flags []*Flag
|
Flags []*Flag
|
||||||
Positional []*Positional
|
Positional []*Positional
|
||||||
Children []*Node
|
Children []*Node
|
||||||
@@ -72,20 +74,33 @@ func (n *Node) findNode(key reflect.Value) *Node {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// AllFlags returns flags from all ancestor branches encountered.
|
// 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 {
|
if n.Parent != nil {
|
||||||
out = append(out, n.Parent.AllFlags()...)
|
out = append(out, n.Parent.AllFlags(hide)...)
|
||||||
}
|
}
|
||||||
if len(n.Flags) > 0 {
|
group := []*Flag{}
|
||||||
out = append(out, n.Flags)
|
for _, flag := range n.Flags {
|
||||||
|
if !hide || !flag.Hidden {
|
||||||
|
group = append(group, flag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(group) > 0 {
|
||||||
|
out = append(out, group)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Leaves returns the leaf commands/arguments under Node.
|
// 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)
|
var walk func(n *Node)
|
||||||
walk = func(n *Node) {
|
walk = func(n *Node) {
|
||||||
|
if hide && n.Hidden {
|
||||||
|
return
|
||||||
|
}
|
||||||
if len(n.Children) == 0 && n.Type != ApplicationNode {
|
if len(n.Children) == 0 && n.Type != ApplicationNode {
|
||||||
out = append(out, n)
|
out = append(out, n)
|
||||||
}
|
}
|
||||||
@@ -112,10 +127,10 @@ func (n *Node) Depth() int {
|
|||||||
return depth
|
return depth
|
||||||
}
|
}
|
||||||
|
|
||||||
// Summary help string for the node.
|
// Summary help string for the node (not including application name).
|
||||||
func (n *Node) Summary() string {
|
func (n *Node) Summary() string {
|
||||||
summary := n.Path()
|
summary := n.Path()
|
||||||
if flags := n.FlagSummary(); flags != "" {
|
if flags := n.FlagSummary(true); flags != "" {
|
||||||
summary += " " + flags
|
summary += " " + flags
|
||||||
}
|
}
|
||||||
args := []string{}
|
args := []string{}
|
||||||
@@ -131,10 +146,10 @@ func (n *Node) Summary() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// FlagSummary for the node.
|
// FlagSummary for the node.
|
||||||
func (n *Node) FlagSummary() string {
|
func (n *Node) FlagSummary(hide bool) string {
|
||||||
required := []string{}
|
required := []string{}
|
||||||
count := 0
|
count := 0
|
||||||
for _, group := range n.AllFlags() {
|
for _, group := range n.AllFlags(hide) {
|
||||||
for _, flag := range group {
|
for _, flag := range group {
|
||||||
count++
|
count++
|
||||||
if flag.Required {
|
if flag.Required {
|
||||||
@@ -145,13 +160,22 @@ func (n *Node) FlagSummary() string {
|
|||||||
return strings.Join(required, " ")
|
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.
|
// Path through ancestors to this Node.
|
||||||
func (n *Node) Path() (out string) {
|
func (n *Node) Path() (out string) {
|
||||||
if n.Parent != nil {
|
if n.Parent != nil {
|
||||||
out += " " + n.Parent.Path()
|
out += " " + n.Parent.Path()
|
||||||
}
|
}
|
||||||
switch n.Type {
|
switch n.Type {
|
||||||
case ApplicationNode, CommandNode:
|
case CommandNode:
|
||||||
out += " " + n.Name
|
out += " " + n.Name
|
||||||
case ArgumentNode:
|
case ArgumentNode:
|
||||||
out += " " + "<" + n.Name + ">"
|
out += " " + "<" + n.Name + ">"
|
||||||
|
|||||||
+1
-1
@@ -20,7 +20,7 @@ func TestModelApplicationCommands(t *testing.T) {
|
|||||||
}
|
}
|
||||||
p := mustNew(t, &cli)
|
p := mustNew(t, &cli)
|
||||||
actual := []string{}
|
actual := []string{}
|
||||||
for _, cmd := range p.Model.Leaves() {
|
for _, cmd := range p.Model.Leaves(false) {
|
||||||
actual = append(actual, cmd.Path())
|
actual = append(actual, cmd.Path())
|
||||||
}
|
}
|
||||||
require.Equal(t, []string{"one two", "one three <four>"}, actual)
|
require.Equal(t, []string{"one two", "one three <four>"}, actual)
|
||||||
|
|||||||
+2
-2
@@ -121,8 +121,8 @@ func Help(help HelpPrinter) Option {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// HelpOptions sets the HelpPrinterOptions to use for printing help.
|
// ConfigureHelp sets the HelpOptions to use for printing help.
|
||||||
func HelpOptions(options HelpPrinterOptions) Option {
|
func ConfigureHelp(options HelpOptions) Option {
|
||||||
return func(k *Kong) error {
|
return func(k *Kong) error {
|
||||||
k.helpOptions = options
|
k.helpOptions = options
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ type Tag struct {
|
|||||||
Arg bool
|
Arg bool
|
||||||
Required bool
|
Required bool
|
||||||
Optional bool
|
Optional bool
|
||||||
|
Name string
|
||||||
Help string
|
Help string
|
||||||
Type string
|
Type string
|
||||||
Default string
|
Default string
|
||||||
@@ -125,6 +126,11 @@ func parseTag(fv reflect.Value, ft reflect.StructField) *Tag {
|
|||||||
t.Required = required
|
t.Required = required
|
||||||
t.Optional = optional
|
t.Optional = optional
|
||||||
t.Default = t.Get("default")
|
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.Help = t.Get("help")
|
||||||
t.Type = t.Get("type")
|
t.Type = t.Get("type")
|
||||||
t.Env = t.Get("env")
|
t.Env = t.Get("env")
|
||||||
|
|||||||
Reference in New Issue
Block a user