diff --git a/README.md b/README.md index 3ab481a..1b3a449 100644 --- a/README.md +++ b/README.md @@ -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 ": 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 ": 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 diff --git a/_examples/docker/main.go b/_examples/docker/main.go index f472230..e678bf4 100644 --- a/_examples/docker/main.go +++ b/_examples/docker/main.go @@ -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) } diff --git a/_examples/server/console.go b/_examples/server/console.go new file mode 100644 index 0000000..6b7a86c --- /dev/null +++ b/_examples/server/console.go @@ -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 +} diff --git a/_examples/server/main.go b/_examples/server/main.go new file mode 100644 index 0000000..f681a25 --- /dev/null +++ b/_examples/server/main.go @@ -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) +} diff --git a/_examples/server/server_rsa_key b/_examples/server/server_rsa_key new file mode 100644 index 0000000..008f812 --- /dev/null +++ b/_examples/server/server_rsa_key @@ -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----- diff --git a/_examples/server/server_rsa_key.pub b/_examples/server/server_rsa_key.pub new file mode 100644 index 0000000..aba46a1 --- /dev/null +++ b/_examples/server/server_rsa_key.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDErVBs9BiJtzRUVl1zoYc+RrmhN4adewxmLJfglsdQmXWg0MFeD00BNt+36r/pkOPJURCGrzGAu9gWQvyoEu3/pQBo65vRG2Y+pDCbGg0ta5cUXCdiUEoClDiB/nz+M5wY+l0TB+OSPAtvnfoGeLblcxThxit0i9AiG0xor5mqNr/cwAJXplFlkJ3yEAEmppm77eZESWcXV3uB8y8D+GFqrtjlVivytSsEceKatP8YQ/poJt82mjjdJ9GdQO7b4QAru8D1wzx+D87+a81HX/2PM5bKvxkMsPpHqJpkJDZqcQ9yqKYwI62JtJ5U6azTtlyf9NKyCZ8bBrytCSldUkDn diff --git a/_examples/shell/main.go b/_examples/shell/main.go index 9aa3652..6a26f01 100644 --- a/_examples/shell/main.go +++ b/_examples/shell/main.go @@ -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 ": fmt.Println(cli.Rm.Paths, cli.Rm.Force, cli.Rm.Recursive) diff --git a/build.go b/build.go index 41229fd..3bea4ae 100644 --- a/build.go +++ b/build.go @@ -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. diff --git a/context.go b/context.go index 1109809..8d60d98 100644 --- a/context.go +++ b/context.go @@ -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. diff --git a/global.go b/global.go index 2c4a960..d4b3cb5 100644 --- a/global.go +++ b/global.go @@ -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 } diff --git a/guesswidth_unix.go b/guesswidth_unix.go index b549ed4..41d54e6 100644 --- a/guesswidth_unix.go +++ b/guesswidth_unix.go @@ -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]) } } diff --git a/help.go b/help.go index cf81059..99e6033 100644 --- a/help.go +++ b/help.go @@ -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 { diff --git a/help_test.go b/help_test.go index 0cca946..8b5bca9 100644 --- a/help_test.go +++ b/help_test.go @@ -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 + require.Equal(t, `Usage: test-app --required A test app. @@ -85,17 +85,18 @@ Run "test-app --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 --required --required-two --required-three + require.Equal(t, `Usage: test-app two --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. diff --git a/kong.go b/kong.go index eb6bb53..9d2c5ca 100644 --- a/kong.go +++ b/kong.go @@ -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 { diff --git a/kong_test.go b/kong_test.go index 4e49d21..596833b 100644 --- a/kong_test.go +++ b/kong_test.go @@ -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"` diff --git a/levenshtein.go b/levenshtein.go new file mode 100644 index 0000000..1816f30 --- /dev/null +++ b/levenshtein.go @@ -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 +} diff --git a/model.go b/model.go index 8e75177..40c7030 100644 --- a/model.go +++ b/model.go @@ -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 + ">" diff --git a/model_test.go b/model_test.go index 9249d25..b128aaf 100644 --- a/model_test.go +++ b/model_test.go @@ -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 "}, actual) diff --git a/options.go b/options.go index 71d918a..ef78f82 100644 --- a/options.go +++ b/options.go @@ -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 diff --git a/tag.go b/tag.go index d07fd2f..2904d44 100644 --- a/tag.go +++ b/tag.go @@ -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")