Improved documentation and help.
This commit is contained in:
@@ -1,6 +1,34 @@
|
|||||||
# Kong is a command-line parser for Go [](https://circleci.com/gh/alecthomas/kong)
|
# Kong is a command-line parser for Go [](https://circleci.com/gh/alecthomas/kong)
|
||||||
|
|
||||||
It parses a command-line into a struct. eg.
|
<!-- MarkdownTOC -->
|
||||||
|
|
||||||
|
1. [Introduction](#introduction)
|
||||||
|
1. [Help](#help)
|
||||||
|
1. [Flags](#flags)
|
||||||
|
1. [Commands and sub-commands](#commands-and-sub-commands)
|
||||||
|
1. [Supported tags](#supported-tags)
|
||||||
|
1. [Configuring Kong](#configuring-kong)
|
||||||
|
1. [`*Mapper(...)` - customising how the command-line is mapped to Go values](#mapper---customising-how-the-command-line-is-mapped-to-go-values)
|
||||||
|
1. [`Help(HelpFunc)` - customising help](#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)
|
||||||
|
|
||||||
|
<!-- /MarkdownTOC -->
|
||||||
|
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
|
||||||
|
Kong aims to support arbitrarily complex command-line structures with as little developer effort as possible.
|
||||||
|
|
||||||
|
To achieve that, command-lines are expressed as Go types, with the structure and tags directing how the command line is mapped onto the struct.
|
||||||
|
|
||||||
|
For example, the following command-line:
|
||||||
|
|
||||||
|
```
|
||||||
|
shell rm [-f] [-r] <paths> ...
|
||||||
|
shell ls [<paths> ...]
|
||||||
|
```
|
||||||
|
|
||||||
|
Can be represented by the following command-line structure:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
package main
|
package main
|
||||||
@@ -9,15 +37,15 @@ import "github.com/alecthomas/kong"
|
|||||||
|
|
||||||
var CLI struct {
|
var CLI struct {
|
||||||
Rm struct {
|
Rm struct {
|
||||||
Force bool `kong:"help='Force removal.'"`
|
Force bool `help:"Force removal."`
|
||||||
Recursive bool `kong:"help='Recursively remove files.'"`
|
Recursive bool `help:"Recursively remove files."`
|
||||||
|
|
||||||
Paths []string `kong:"help='Paths to remove.',type='path'"`
|
Paths []string `arg help:"Paths to remove." type:"path"`
|
||||||
} `kong:"help='Remove files.'"`
|
} `cmd help:"Remove files."`
|
||||||
|
|
||||||
Ls struct {
|
Ls struct {
|
||||||
Paths []string `kong:"help='Paths to list.',type='path'"`
|
Paths []string `arg optional help:"Paths to list." type:"path"`
|
||||||
} `kong:"help='List paths.'"`
|
} `cmd help:"List paths."`
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -25,23 +53,157 @@ func main() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Decoders
|
## Help
|
||||||
|
|
||||||
Command-line arguments are mapped to Go values via the Decoder interface:
|
Help is automatically generated. With no other arguments provided, help will display a full summary of all available commands.
|
||||||
|
|
||||||
|
eg.
|
||||||
|
|
||||||
|
```
|
||||||
|
$ shell --help
|
||||||
|
usage: shell [<flags>]
|
||||||
|
|
||||||
|
A shell-like example app.
|
||||||
|
|
||||||
|
Flags:
|
||||||
|
--help Show context-sensitive help.
|
||||||
|
--debug Debug mode.
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
rm [<flags>] <paths> ...
|
||||||
|
Remove files.
|
||||||
|
|
||||||
|
ls [<flags>] [<paths> ...]
|
||||||
|
List paths.
|
||||||
|
```
|
||||||
|
|
||||||
|
If a command is provided, the help will show full detail on the command including all available flags.
|
||||||
|
|
||||||
|
eg.
|
||||||
|
|
||||||
|
```
|
||||||
|
$ shell --help rm
|
||||||
|
usage: shell rm [<flags>] <paths> ...
|
||||||
|
|
||||||
|
Remove files.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
<paths> ... Paths to remove.
|
||||||
|
|
||||||
|
Flags:
|
||||||
|
--debug Debug mode.
|
||||||
|
|
||||||
|
-f, --force Force removal.
|
||||||
|
-r, --recursive Recursively remove files.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Flags
|
||||||
|
|
||||||
|
Any field in the command structure *not* tagged with `cmd` or `arg` will be a flag. Flags are optional by default.
|
||||||
|
|
||||||
|
## Commands and sub-commands
|
||||||
|
|
||||||
|
Kong supports arbitrarily nested commands and positional arguments. Nested structs tagged with `cmd` will be treated as commands.
|
||||||
|
|
||||||
|
Arguments can also optionally have children, in order to support commands like the following:
|
||||||
|
|
||||||
|
```
|
||||||
|
app rename <name> to <name>
|
||||||
|
```
|
||||||
|
|
||||||
|
This is achieved by tagging a nested struct with `arg`, then including a positional argument field inside that struct with the same name. For example:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// A Decoder knows how to decode text into a Go value.
|
var CLI struct {
|
||||||
type Decoder interface {
|
Rename struct {
|
||||||
// Decode scan into target.
|
Name struct {
|
||||||
//
|
Name string `arg` // <-- NOTE: identical name to enclosing struct field.
|
||||||
// "ctx" contains context about the value being decoded that may be useful
|
To struct {
|
||||||
// to some decoders.
|
Name struct {
|
||||||
Decode(ctx *DecoderContext, scan *Scanner, target reflect.Value) error
|
Name string `arg`
|
||||||
|
} `arg`
|
||||||
|
} `cmd`
|
||||||
|
} `arg`
|
||||||
|
} `cmd`
|
||||||
|
}
|
||||||
|
```
|
||||||
|
This looks a little verbose in this contrived example, but typically this will not be the case.
|
||||||
|
|
||||||
|
## Supported tags
|
||||||
|
|
||||||
|
Tags can be in two forms:
|
||||||
|
|
||||||
|
1. Standard Go syntax, eg. `kong:"required,name='foo'"`.
|
||||||
|
2. Bare tags, eg. `required name:"foo"`
|
||||||
|
|
||||||
|
Both can coexist with standard Tag parsing.
|
||||||
|
|
||||||
|
| Tag | Description |
|
||||||
|
| -----------------------| ------------------------------------------- |
|
||||||
|
| `cmd` | If present, struct is a command. |
|
||||||
|
| `arg` | If present, field is an argument. |
|
||||||
|
| `type:"X"` | Specify named Mapper to use. |
|
||||||
|
| `help:"X"` | Help text. |
|
||||||
|
| `placeholder:"X"` | Placeholder text. |
|
||||||
|
| `default:"X"` | Default value. |
|
||||||
|
| `short:"X"` | Short name, if flag. |
|
||||||
|
| `name:"X"` | Long name, for overriding field name. |
|
||||||
|
| `required` | If present, flag/arg is required. |
|
||||||
|
| `optional` | If present, flag/arg is optional. |
|
||||||
|
| `hidden` | If present, flag is hidden. |
|
||||||
|
| `format:"X"` | Format for parsing input, if supported. |
|
||||||
|
| `sep:"X"` | Separator for sequences (defaults to ",") |
|
||||||
|
|
||||||
|
## Configuring Kong
|
||||||
|
|
||||||
|
Each Kong parser can be configured via functional options passed to `New(cli interface{}, options...Option)`. The full set of options can be found in `options.go`.
|
||||||
|
|
||||||
|
### `*Mapper(...)` - customising how the command-line is mapped to Go values
|
||||||
|
|
||||||
|
Command-line arguments are mapped to Go values via the Mapper interface:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// A Mapper knows how to map command-line input to Go.
|
||||||
|
type Mapper interface {
|
||||||
|
// Decode scan into target.
|
||||||
|
//
|
||||||
|
// "ctx" contains context about the value being decoded that may be useful
|
||||||
|
// to some mapperss.
|
||||||
|
Decode(ctx *MapperContext, scan *Scanner, target reflect.Value) error
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
All builtin Go types (as well as a bunch of useful stdlib types like `time.Time`) have decoders registered by default. Decoders for custom types can be added using `kong.RegisterDecoder(decoder)`. Decoders are mapped from fields in three ways:
|
All builtin Go types (as well as a bunch of useful stdlib types like `time.Time`) have mapperss registered by default. Mappers for custom types can be added using `kong.??Mapper(...)` options. Mappers are applied to fields in four ways:
|
||||||
|
|
||||||
1. By registering a `kong.NamedDecoder` and using the key `type='<name>'`.
|
1. `NamedMapper(string, Mapper)` and using the tag key `type:"<name>"`.
|
||||||
2. By registering a `kong.KindDecoder` with a `reflect.Kind`.
|
2. `KindMapper(reflect.Kind, Mapper)`.
|
||||||
3. By registering a `kong.TypeDecoder` with a `reflect.Type`.
|
3. `TypeMapper(reflect.Type, Mapper)`.
|
||||||
|
4. `ValueMapper(interface{}, Mapper)`, passing in a pointer to a field of the grammar.
|
||||||
|
|
||||||
|
|
||||||
|
### `Help(HelpFunc)` - customising help
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
Hooks are callback functions that are bound to a node in the command-line and executed at parse time, before structural validation and assignment.
|
||||||
|
|
||||||
|
eg.
|
||||||
|
|
||||||
|
```go
|
||||||
|
app := kong.Must(&CLI, kong.Hook(&CLI.Debug, func(ctx *Context, path *Path) error {
|
||||||
|
log.SetLevel(DEBUG)
|
||||||
|
return nil
|
||||||
|
}))
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: it is generally more advisable to use an imperative approach to building command-lines, eg.
|
||||||
|
|
||||||
|
```go
|
||||||
|
if CLI.Debug {
|
||||||
|
log.SetLevel(DEBUG)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
But under some circumstances, hooks are the right choice.
|
||||||
|
|||||||
@@ -8,20 +8,21 @@ import (
|
|||||||
"github.com/alecthomas/kong"
|
"github.com/alecthomas/kong"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// nolint: govet
|
||||||
var CLI struct {
|
var CLI struct {
|
||||||
Debug bool `kong:"help='Debug mode.'"`
|
Debug bool `help:"Debug mode."`
|
||||||
Output string `kong:"help='File to output to.',placeholder='FILE'"`
|
|
||||||
|
|
||||||
Rm struct {
|
Rm struct {
|
||||||
Force bool `kong:"help='Force removal.'"`
|
User string `help:"Run as user." short:"u"`
|
||||||
Recursive bool `kong:"help='Recursively remove files.'"`
|
Force bool `help:"Force removal." short:"f"`
|
||||||
|
Recursive bool `help:"Recursively remove files." short:"r"`
|
||||||
|
|
||||||
Paths []string `kong:"arg,help='Paths to remove.',type='path'"`
|
Paths []string `arg help:"Paths to remove." type:"path"`
|
||||||
} `kong:"cmd,help='Remove files.'"`
|
} `cmd help:"Remove files."`
|
||||||
|
|
||||||
Ls struct {
|
Ls struct {
|
||||||
Paths []string `kong:"help='Paths to list.',type='path'"`
|
Paths []string `arg optional help:"Paths to list." type:"path"`
|
||||||
} `kong:"cmd,help='List paths.'"`
|
} `cmd help:"List paths."`
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|||||||
@@ -92,8 +92,8 @@ func buildChild(k *Kong, node *Node, typ NodeType, v reflect.Value, ft reflect.S
|
|||||||
// 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.
|
||||||
if tag.Arg {
|
if tag.Arg {
|
||||||
if len(child.Positional) == 0 {
|
if len(child.Positional) == 0 {
|
||||||
fail("positional branch %s.%s must have at least one child positional argument",
|
fail("positional branch %s.%s must have at least one child positional argument named %q",
|
||||||
v.Type().Name(), ft.Name)
|
v.Type().Name(), ft.Name, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
value := child.Positional[0]
|
value := child.Positional[0]
|
||||||
@@ -122,14 +122,11 @@ func buildChild(k *Kong, node *Node, typ NodeType, v reflect.Value, ft reflect.S
|
|||||||
func buildField(k *Kong, node *Node, v reflect.Value, ft reflect.StructField, fv reflect.Value, tag *Tag, name string, seenFlags map[string]bool) {
|
func buildField(k *Kong, node *Node, v reflect.Value, ft reflect.StructField, fv reflect.Value, tag *Tag, name string, seenFlags map[string]bool) {
|
||||||
mapper := k.registry.ForNamedType(tag.Type, fv)
|
mapper := k.registry.ForNamedType(tag.Type, fv)
|
||||||
if mapper == nil {
|
if mapper == nil {
|
||||||
fail("no mapper for %s.%s (of type %s)", v.Type(), ft.Name, ft.Type)
|
fail("unsupported field type %s.%s (of type %s)", v.Type(), ft.Name, ft.Type)
|
||||||
}
|
}
|
||||||
|
|
||||||
flag := !tag.Arg
|
value := &Value{
|
||||||
|
|
||||||
value := Value{
|
|
||||||
Name: name,
|
Name: name,
|
||||||
Flag: flag,
|
|
||||||
Help: tag.Help,
|
Help: tag.Help,
|
||||||
Default: tag.Default,
|
Default: tag.Default,
|
||||||
Mapper: mapper,
|
Mapper: mapper,
|
||||||
@@ -137,22 +134,24 @@ func buildField(k *Kong, node *Node, v reflect.Value, ft reflect.StructField, fv
|
|||||||
Value: fv,
|
Value: fv,
|
||||||
|
|
||||||
// Flags are optional by default, and args are required by default.
|
// Flags are optional by default, and args are required by default.
|
||||||
Required: (flag && tag.Required) || (tag.Arg && !tag.Optional),
|
Required: (!tag.Arg && tag.Required) || (tag.Arg && !tag.Optional),
|
||||||
Format: tag.Format,
|
Format: tag.Format,
|
||||||
}
|
}
|
||||||
|
|
||||||
if tag.Arg {
|
if tag.Arg {
|
||||||
node.Positional = append(node.Positional, &value)
|
node.Positional = append(node.Positional, value)
|
||||||
} else {
|
} else {
|
||||||
if seenFlags[value.Name] {
|
if seenFlags[value.Name] {
|
||||||
fail("duplicate flag --%s", value.Name)
|
fail("duplicate flag --%s", value.Name)
|
||||||
}
|
}
|
||||||
seenFlags[value.Name] = true
|
seenFlags[value.Name] = true
|
||||||
node.Flags = append(node.Flags, &Flag{
|
flag := &Flag{
|
||||||
Value: value,
|
Value: value,
|
||||||
Short: tag.Short,
|
Short: tag.Short,
|
||||||
PlaceHolder: tag.PlaceHolder,
|
PlaceHolder: tag.PlaceHolder,
|
||||||
Env: tag.Env,
|
Env: tag.Env,
|
||||||
})
|
}
|
||||||
|
value.Flag = flag
|
||||||
|
node.Flags = append(node.Flags, flag)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-5
@@ -56,14 +56,14 @@ func Trace(k *Kong, args []string) (*Context, error) {
|
|||||||
App: k,
|
App: k,
|
||||||
args: args,
|
args: args,
|
||||||
Path: []*Path{
|
Path: []*Path{
|
||||||
{App: k.Application, Flags: k.Flags, Value: k.Target},
|
{App: k.Model, Flags: k.Model.Flags, Value: k.Model.Target},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
err := c.reset(&c.App.Node)
|
err := c.reset(&c.App.Model.Node)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
c.Error = c.trace(&c.App.Node)
|
c.Error = c.trace(&c.App.Model.Node)
|
||||||
return c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,6 +93,10 @@ func (c *Context) Validate() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case path.Argument != nil:
|
case path.Argument != nil:
|
||||||
|
value := path.Argument.Argument
|
||||||
|
if value.Required && !value.Set {
|
||||||
|
return fmt.Errorf("%s is required", path.Argument.Summary())
|
||||||
|
}
|
||||||
if err := checkMissingChildren(path.Argument); err != nil {
|
if err := checkMissingChildren(path.Argument); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -341,7 +345,7 @@ func checkMissingFlags(flags []*Flag) error {
|
|||||||
if !flag.Required || flag.Set {
|
if !flag.Required || flag.Set {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
missing = append(missing, flag.Name)
|
missing = append(missing, flag.Summary())
|
||||||
}
|
}
|
||||||
if len(missing) == 0 {
|
if len(missing) == 0 {
|
||||||
return nil
|
return nil
|
||||||
@@ -352,12 +356,17 @@ func checkMissingFlags(flags []*Flag) error {
|
|||||||
|
|
||||||
func checkMissingChildren(node *Node) error {
|
func checkMissingChildren(node *Node) error {
|
||||||
missing := []string{}
|
missing := []string{}
|
||||||
|
for _, arg := range node.Positional {
|
||||||
|
if arg.Required && !arg.Set {
|
||||||
|
missing = append(missing, strconv.Quote(arg.Summary()))
|
||||||
|
}
|
||||||
|
}
|
||||||
for _, child := range node.Children {
|
for _, child := range node.Children {
|
||||||
if child.Argument != nil {
|
if child.Argument != nil {
|
||||||
if !child.Argument.Required {
|
if !child.Argument.Required {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
missing = append(missing, strconv.Quote("<"+child.Argument.Name+">"))
|
missing = append(missing, strconv.Quote(child.Summary()))
|
||||||
} else {
|
} else {
|
||||||
missing = append(missing, strconv.Quote(child.Name))
|
missing = append(missing, strconv.Quote(child.Name))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,13 +13,14 @@ const (
|
|||||||
defaultIndent = 2
|
defaultIndent = 2
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// PrintHelp is the default help printer.
|
||||||
func PrintHelp(ctx *Context) error {
|
func PrintHelp(ctx *Context) error {
|
||||||
w := newHelpWriter(guessWidth(ctx.App.Stdout))
|
w := newHelpWriter(guessWidth(ctx.App.Stdout))
|
||||||
selected := ctx.Selected()
|
selected := ctx.Selected()
|
||||||
if selected == nil {
|
if selected == nil {
|
||||||
printApp(w, ctx.App.Application)
|
printApp(w, ctx.App.Model)
|
||||||
} else {
|
} else {
|
||||||
printCommand(w, ctx.App.Application, selected)
|
printCommand(w, ctx.App.Model, selected)
|
||||||
}
|
}
|
||||||
return w.Write(ctx.App.Stdout)
|
return w.Write(ctx.App.Stdout)
|
||||||
}
|
}
|
||||||
@@ -39,10 +40,15 @@ func printNodeDetail(w *helpWriter, node *Node) {
|
|||||||
w.Print("")
|
w.Print("")
|
||||||
w.Wrap(node.Help)
|
w.Wrap(node.Help)
|
||||||
}
|
}
|
||||||
if len(node.Flags) > 0 {
|
if len(node.Positional) > 0 {
|
||||||
w.Printf("")
|
w.Print("")
|
||||||
w.Printf("Flags:")
|
w.Print("Arguments:")
|
||||||
writeFlags(w.Indent(), node.Flags)
|
writePositionals(w.Indent(), node.Positional)
|
||||||
|
}
|
||||||
|
if flags := node.AllFlags(); len(flags) > 0 {
|
||||||
|
w.Print("")
|
||||||
|
w.Print("Flags:")
|
||||||
|
writeFlags(w.Indent(), flags)
|
||||||
}
|
}
|
||||||
cmds := node.Leaves()
|
cmds := node.Leaves()
|
||||||
if len(cmds) > 0 {
|
if len(cmds) > 0 {
|
||||||
@@ -114,18 +120,33 @@ func (h *helpWriter) Wrap(text string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeFlags(w *helpWriter, flags []*Flag) {
|
func writePositionals(w *helpWriter, args []*Positional) {
|
||||||
|
rows := [][2]string{}
|
||||||
|
for _, arg := range args {
|
||||||
|
rows = append(rows, [2]string{arg.Summary(), arg.Help})
|
||||||
|
}
|
||||||
|
writeTwoColumns(w, 2, rows)
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeFlags(w *helpWriter, groups [][]*Flag) {
|
||||||
rows := [][2]string{}
|
rows := [][2]string{}
|
||||||
haveShort := false
|
haveShort := false
|
||||||
for _, flag := range flags {
|
for _, group := range groups {
|
||||||
if flag.Short != 0 {
|
for _, flag := range group {
|
||||||
haveShort = true
|
if flag.Short != 0 {
|
||||||
break
|
haveShort = true
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, flag := range flags {
|
for i, group := range groups {
|
||||||
if !flag.Hidden {
|
if i > 0 {
|
||||||
rows = append(rows, [2]string{formatFlag(haveShort, flag), flag.Help})
|
rows = append(rows, [2]string{"", ""})
|
||||||
|
}
|
||||||
|
for _, flag := range group {
|
||||||
|
if !flag.Hidden {
|
||||||
|
rows = append(rows, [2]string{formatFlag(haveShort, flag), flag.Help})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
writeTwoColumns(w, 2, rows)
|
writeTwoColumns(w, 2, rows)
|
||||||
|
|||||||
+56
-19
@@ -9,29 +9,48 @@ import (
|
|||||||
|
|
||||||
func TestHelp(t *testing.T) {
|
func TestHelp(t *testing.T) {
|
||||||
var cli struct {
|
var cli struct {
|
||||||
String string `help:"A string flag."`
|
String string `help:"A string flag."`
|
||||||
Bool bool `help:"A bool flag with very long help that wraps a lot and is verbose and is really verbose."`
|
Bool bool `help:"A bool flag with very long help that wraps a lot and is verbose and is really verbose."`
|
||||||
|
Required bool `required help:"A required flag."`
|
||||||
|
|
||||||
One struct {
|
One struct {
|
||||||
Flag string `help:"Nested flag."`
|
Flag string `help:"Nested flag."`
|
||||||
} `cmd help:"A subcommand."`
|
} `cmd help:"A subcommand."`
|
||||||
|
|
||||||
Two struct {
|
Two struct {
|
||||||
Flag string `help:"Nested flag under two."`
|
Flag string `help:"Nested flag under two."`
|
||||||
|
RequiredTwo bool `required`
|
||||||
|
|
||||||
|
Three struct {
|
||||||
|
RequiredThree bool `required`
|
||||||
|
Three string `arg`
|
||||||
|
} `arg help:"Sub-sub-arg."`
|
||||||
|
|
||||||
|
Four struct {
|
||||||
|
} `cmd help:"Sub-sub-command."`
|
||||||
} `cmd help:"Another subcommand."`
|
} `cmd help:"Another subcommand."`
|
||||||
}
|
}
|
||||||
|
|
||||||
w := bytes.NewBuffer(nil)
|
w := bytes.NewBuffer(nil)
|
||||||
exited := false
|
exited := false
|
||||||
app := mustNew(t, &cli,
|
app := mustNew(t, &cli,
|
||||||
Name("test-app"),
|
Name("test-app"),
|
||||||
Description("A test app."),
|
Description("A test app."),
|
||||||
Writers(w, w),
|
Writers(w, w),
|
||||||
ExitFunction(func(int) { exited = true }),
|
ExitFunction(func(int) {
|
||||||
|
exited = true
|
||||||
|
panic(true) // Panic to fake "exit".
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
_, err := app.Parse([]string{"--help"})
|
|
||||||
require.NoError(t, err)
|
t.Run("Full", func(t *testing.T) {
|
||||||
require.True(t, exited)
|
require.Panics(t, func() {
|
||||||
require.Equal(t, `usage: test-app [<flags>]
|
_, err := app.Parse([]string{"--help"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
})
|
||||||
|
require.True(t, exited)
|
||||||
|
t.Log(w.String())
|
||||||
|
require.Equal(t, `usage: test-app --required [<flags>]
|
||||||
|
|
||||||
A test app.
|
A test app.
|
||||||
|
|
||||||
@@ -40,25 +59,43 @@ Flags:
|
|||||||
--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.
|
||||||
|
--required A required flag.
|
||||||
|
|
||||||
Commands:
|
Commands:
|
||||||
one [<flags>]
|
one --required [<flags>]
|
||||||
A subcommand.
|
A subcommand.
|
||||||
|
|
||||||
two [<flags>]
|
two <three> --required --required-two --required-three [<flags>]
|
||||||
Another subcommand.
|
Sub-sub-arg.
|
||||||
|
|
||||||
|
two four --required --required-two [<flags>]
|
||||||
|
Sub-sub-command.
|
||||||
`, w.String())
|
`, w.String())
|
||||||
|
})
|
||||||
|
|
||||||
exited = false
|
t.Run("Selected", func(t *testing.T) {
|
||||||
w.Truncate(0)
|
exited = false
|
||||||
_, err = app.Parse([]string{"one", "--help"})
|
w.Truncate(0)
|
||||||
require.NoError(t, err)
|
require.Panics(t, func() {
|
||||||
require.True(t, exited)
|
_, err := app.Parse([]string{"two", "hello", "--help"})
|
||||||
require.Equal(t, `usage: test-app one [<flags>]
|
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 [<flags>]
|
||||||
|
|
||||||
A subcommand.
|
Sub-sub-arg.
|
||||||
|
|
||||||
Flags:
|
Flags:
|
||||||
--flag=STRING Nested flag.
|
--string=STRING A string flag.
|
||||||
|
--bool A bool flag with very long help that wraps a lot and is
|
||||||
|
verbose and is really verbose.
|
||||||
|
--required A required flag.
|
||||||
|
|
||||||
|
--flag=STRING Nested flag under two.
|
||||||
|
--required-two
|
||||||
|
|
||||||
|
--required-three
|
||||||
`, w.String())
|
`, w.String())
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ func Must(ast interface{}, options ...Option) *Kong {
|
|||||||
// Kong is the main parser type.
|
// Kong is the main parser type.
|
||||||
type Kong struct {
|
type Kong struct {
|
||||||
// Grammar model.
|
// Grammar model.
|
||||||
*Application
|
Model *Application
|
||||||
|
|
||||||
// Termination function (defaults to os.Exit)
|
// Termination function (defaults to os.Exit)
|
||||||
Exit func(int)
|
Exit func(int)
|
||||||
@@ -36,7 +36,7 @@ type Kong struct {
|
|||||||
Stdout io.Writer
|
Stdout io.Writer
|
||||||
Stderr io.Writer
|
Stderr io.Writer
|
||||||
|
|
||||||
before map[reflect.Value]HookFunction
|
before map[reflect.Value]HookFunc
|
||||||
registry *Registry
|
registry *Registry
|
||||||
noDefaultHelp bool
|
noDefaultHelp bool
|
||||||
help func(*Context) error
|
help func(*Context) error
|
||||||
@@ -50,7 +50,7 @@ func New(grammar interface{}, options ...Option) (*Kong, error) {
|
|||||||
Exit: os.Exit,
|
Exit: os.Exit,
|
||||||
Stdout: os.Stdout,
|
Stdout: os.Stdout,
|
||||||
Stderr: os.Stderr,
|
Stderr: os.Stderr,
|
||||||
before: map[reflect.Value]HookFunction{},
|
before: map[reflect.Value]HookFunc{},
|
||||||
registry: NewRegistry().RegisterDefaults(),
|
registry: NewRegistry().RegisterDefaults(),
|
||||||
help: PrintHelp,
|
help: PrintHelp,
|
||||||
}
|
}
|
||||||
@@ -63,8 +63,8 @@ func New(grammar interface{}, options ...Option) (*Kong, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return k, err
|
return k, err
|
||||||
}
|
}
|
||||||
k.Application = model
|
model.Name = filepath.Base(os.Args[0])
|
||||||
k.Name = filepath.Base(os.Args[0])
|
k.Model = model
|
||||||
|
|
||||||
for _, option := range options {
|
for _, option := range options {
|
||||||
option(k)
|
option(k)
|
||||||
@@ -80,14 +80,14 @@ func (k *Kong) extraFlags() []*Flag {
|
|||||||
helpValue := false
|
helpValue := false
|
||||||
value := reflect.ValueOf(&helpValue).Elem()
|
value := reflect.ValueOf(&helpValue).Elem()
|
||||||
helpFlag := &Flag{
|
helpFlag := &Flag{
|
||||||
Value: Value{
|
Value: &Value{
|
||||||
Name: "help",
|
Name: "help",
|
||||||
Help: "Show context-sensitive help.",
|
Help: "Show context-sensitive help.",
|
||||||
Flag: true,
|
|
||||||
Value: value,
|
Value: value,
|
||||||
Mapper: k.registry.ForValue(value),
|
Mapper: k.registry.ForValue(value),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
helpFlag.Flag = helpFlag
|
||||||
hook := Hook(&helpValue, func(ctx *Context, path *Path) error {
|
hook := Hook(&helpValue, func(ctx *Context, path *Path) error {
|
||||||
err := PrintHelp(ctx)
|
err := PrintHelp(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -158,12 +158,12 @@ func (k *Kong) applyHooks(ctx *Context) error {
|
|||||||
|
|
||||||
// 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{}) {
|
func (k *Kong) Printf(format string, args ...interface{}) {
|
||||||
fmt.Fprintf(k.Stdout, k.Name+": "+format, args...)
|
fmt.Fprintf(k.Stdout, k.Model.Name+": "+format, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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{}) {
|
func (k *Kong) Errorf(format string, args ...interface{}) {
|
||||||
fmt.Fprintf(k.Stderr, k.Name+": "+format, args...)
|
fmt.Fprintf(k.Stderr, k.Model.Name+": "+format, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// FatalIfError terminates with an error message if err != nil.
|
// FatalIfError terminates with an error message if err != nil.
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package kong
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math/bits"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -113,16 +114,16 @@ func (d *Registry) RegisterValue(ptr interface{}, mapper Mapper) *Registry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *Registry) RegisterDefaults() *Registry {
|
func (d *Registry) RegisterDefaults() *Registry {
|
||||||
return d.RegisterKind(reflect.Int, MapperFunc(intDecoder)).
|
return d.RegisterKind(reflect.Int, intDecoder(bits.UintSize)).
|
||||||
RegisterKind(reflect.Int8, MapperFunc(intDecoder)).
|
RegisterKind(reflect.Int8, intDecoder(8)).
|
||||||
RegisterKind(reflect.Int16, MapperFunc(intDecoder)).
|
RegisterKind(reflect.Int16, intDecoder(16)).
|
||||||
RegisterKind(reflect.Int32, MapperFunc(intDecoder)).
|
RegisterKind(reflect.Int32, intDecoder(32)).
|
||||||
RegisterKind(reflect.Int64, MapperFunc(intDecoder)).
|
RegisterKind(reflect.Int64, intDecoder(64)).
|
||||||
RegisterKind(reflect.Uint, MapperFunc(uintDecoder)).
|
RegisterKind(reflect.Uint, uintDecoder(64)).
|
||||||
RegisterKind(reflect.Uint8, MapperFunc(uintDecoder)).
|
RegisterKind(reflect.Uint8, uintDecoder(bits.UintSize)).
|
||||||
RegisterKind(reflect.Uint16, MapperFunc(uintDecoder)).
|
RegisterKind(reflect.Uint16, uintDecoder(16)).
|
||||||
RegisterKind(reflect.Uint32, MapperFunc(uintDecoder)).
|
RegisterKind(reflect.Uint32, uintDecoder(32)).
|
||||||
RegisterKind(reflect.Uint64, MapperFunc(uintDecoder)).
|
RegisterKind(reflect.Uint64, uintDecoder(64)).
|
||||||
RegisterKind(reflect.Float32, floatDecoder(32)).
|
RegisterKind(reflect.Float32, floatDecoder(32)).
|
||||||
RegisterKind(reflect.Float64, floatDecoder(64)).
|
RegisterKind(reflect.Float64, floatDecoder(64)).
|
||||||
RegisterKind(reflect.String, MapperFunc(func(ctx *DecoderContext, scan *Scanner, target reflect.Value) error {
|
RegisterKind(reflect.String, MapperFunc(func(ctx *DecoderContext, scan *Scanner, target reflect.Value) error {
|
||||||
@@ -130,8 +131,8 @@ func (d *Registry) RegisterDefaults() *Registry {
|
|||||||
return nil
|
return nil
|
||||||
})).
|
})).
|
||||||
RegisterKind(reflect.Bool, boolMapper{}).
|
RegisterKind(reflect.Bool, boolMapper{}).
|
||||||
RegisterType(reflect.TypeOf(time.Time{}), MapperFunc(timeDecoder)).
|
RegisterType(reflect.TypeOf(time.Time{}), timeDecoder()).
|
||||||
RegisterType(reflect.TypeOf(time.Duration(0)), MapperFunc(durationDecoder)).
|
RegisterType(reflect.TypeOf(time.Duration(0)), durationDecoder()).
|
||||||
RegisterKind(reflect.Slice, sliceDecoder(d))
|
RegisterKind(reflect.Slice, sliceDecoder(d))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,46 +144,54 @@ func (boolMapper) Decode(ctx *DecoderContext, scan *Scanner, target reflect.Valu
|
|||||||
}
|
}
|
||||||
func (boolMapper) IsBool() bool { return true }
|
func (boolMapper) IsBool() bool { return true }
|
||||||
|
|
||||||
func durationDecoder(ctx *DecoderContext, scan *Scanner, target reflect.Value) error {
|
func durationDecoder() MapperFunc {
|
||||||
d, err := time.ParseDuration(scan.PopValue("duration"))
|
return func(ctx *DecoderContext, scan *Scanner, target reflect.Value) error {
|
||||||
if err != nil {
|
d, err := time.ParseDuration(scan.PopValue("duration"))
|
||||||
return err
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
target.Set(reflect.ValueOf(d))
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
target.Set(reflect.ValueOf(d))
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func timeDecoder(ctx *DecoderContext, scan *Scanner, target reflect.Value) error {
|
func timeDecoder() MapperFunc {
|
||||||
fmt := time.RFC3339
|
return func(ctx *DecoderContext, scan *Scanner, target reflect.Value) error {
|
||||||
if ctx.Value.Format != "" {
|
fmt := time.RFC3339
|
||||||
fmt = ctx.Value.Format
|
if ctx.Value.Format != "" {
|
||||||
|
fmt = ctx.Value.Format
|
||||||
|
}
|
||||||
|
t, err := time.Parse(fmt, scan.PopValue("time"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
target.Set(reflect.ValueOf(t))
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
t, err := time.Parse(fmt, scan.PopValue("time"))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
target.Set(reflect.ValueOf(t))
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func intDecoder(ctx *DecoderContext, scan *Scanner, target reflect.Value) error {
|
func intDecoder(bits int) MapperFunc {
|
||||||
value := scan.PopValue("int")
|
return func(ctx *DecoderContext, scan *Scanner, target reflect.Value) error {
|
||||||
n, err := strconv.ParseInt(value, 10, 64)
|
value := scan.PopValue("int")
|
||||||
if err != nil {
|
n, err := strconv.ParseInt(value, 10, bits)
|
||||||
return fmt.Errorf("invalid int %q", value)
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid int %q", value)
|
||||||
|
}
|
||||||
|
target.SetInt(n)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
target.SetInt(n)
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func uintDecoder(ctx *DecoderContext, scan *Scanner, target reflect.Value) error {
|
func uintDecoder(bits int) MapperFunc {
|
||||||
value := scan.PopValue("uint")
|
return func(ctx *DecoderContext, scan *Scanner, target reflect.Value) error {
|
||||||
n, err := strconv.ParseUint(value, 10, 64)
|
value := scan.PopValue("uint")
|
||||||
if err != nil {
|
n, err := strconv.ParseUint(value, 10, bits)
|
||||||
return fmt.Errorf("invalid uint %q", value)
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid uint %q", value)
|
||||||
|
}
|
||||||
|
target.SetUint(n)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
target.SetUint(n)
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func floatDecoder(bits int) MapperFunc {
|
func floatDecoder(bits int) MapperFunc {
|
||||||
@@ -200,12 +209,9 @@ func floatDecoder(bits int) MapperFunc {
|
|||||||
func sliceDecoder(d *Registry) MapperFunc {
|
func sliceDecoder(d *Registry) MapperFunc {
|
||||||
return func(ctx *DecoderContext, scan *Scanner, target reflect.Value) error {
|
return func(ctx *DecoderContext, scan *Scanner, target reflect.Value) error {
|
||||||
el := target.Type().Elem()
|
el := target.Type().Elem()
|
||||||
sep, ok := ctx.Value.Tag.Get("sep")
|
sep := ctx.Value.Tag.Sep
|
||||||
if !ok {
|
|
||||||
sep = ","
|
|
||||||
}
|
|
||||||
var childScanner *Scanner
|
var childScanner *Scanner
|
||||||
if ctx.Value.Flag {
|
if ctx.Value.Flag != nil {
|
||||||
// If decoding a flag, we need an argument.
|
// If decoding a flag, we need an argument.
|
||||||
childScanner = Scan(strings.Split(scan.PopValue("list"), sep)...)
|
childScanner = Scan(strings.Split(scan.PopValue("list"), sep)...)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package kong
|
|||||||
import (
|
import (
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
@@ -40,3 +41,26 @@ func (testMooMapper) Decode(ctx *DecoderContext, scan *Scanner, target reflect.V
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
func (testMooMapper) IsBool() bool { return true }
|
func (testMooMapper) IsBool() bool { return true }
|
||||||
|
|
||||||
|
func TestTimeMapper(t *testing.T) {
|
||||||
|
var cli struct {
|
||||||
|
Flag time.Time `format:"2006"`
|
||||||
|
}
|
||||||
|
k := mustNew(t, &cli)
|
||||||
|
_, err := k.Parse([]string{"--flag=2008"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
expected, err := time.Parse("2006", "2008")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 2008, expected.Year())
|
||||||
|
require.Equal(t, expected, cli.Flag)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDurationMapper(t *testing.T) {
|
||||||
|
var cli struct {
|
||||||
|
Flag time.Duration
|
||||||
|
}
|
||||||
|
k := mustNew(t, &cli)
|
||||||
|
_, err := k.Parse([]string{"--flag=5s"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, time.Second*5, cli.Flag)
|
||||||
|
}
|
||||||
|
|||||||
@@ -37,6 +37,16 @@ type Node struct {
|
|||||||
Argument *Value // Populated when Type is ArgumentNode.
|
Argument *Value // Populated when Type is ArgumentNode.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (n *Node) AllFlags() (out [][]*Flag) {
|
||||||
|
if n.Parent != nil {
|
||||||
|
out = append(out, n.Parent.AllFlags()...)
|
||||||
|
}
|
||||||
|
if len(n.Flags) > 0 {
|
||||||
|
out = append(out, n.Flags)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Leaves returns the leaf commands/arguments under Node.
|
// Leaves returns the leaf commands/arguments under Node.
|
||||||
func (n *Node) Leaves() (out []*Node) {
|
func (n *Node) Leaves() (out []*Node) {
|
||||||
var walk func(n *Node)
|
var walk func(n *Node)
|
||||||
@@ -70,21 +80,12 @@ func (n *Node) Depth() int {
|
|||||||
// Summary help string for the node.
|
// Summary help string for the node.
|
||||||
func (n *Node) Summary() string {
|
func (n *Node) Summary() string {
|
||||||
summary := n.Path()
|
summary := n.Path()
|
||||||
if n.Type == ArgumentNode {
|
|
||||||
summary = "<" + summary + ">"
|
|
||||||
}
|
|
||||||
if flags := n.FlagSummary(); flags != "" {
|
if flags := n.FlagSummary(); flags != "" {
|
||||||
summary += " " + flags
|
summary += " " + flags
|
||||||
}
|
}
|
||||||
args := []string{}
|
args := []string{}
|
||||||
for _, arg := range n.Positional {
|
for _, arg := range n.Positional {
|
||||||
if arg.Required {
|
args = append(args, arg.Summary())
|
||||||
argText := "<" + arg.Name + ">"
|
|
||||||
if arg.IsCumulative() {
|
|
||||||
argText += " ..."
|
|
||||||
}
|
|
||||||
args = append(args, argText)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if len(args) != 0 {
|
if len(args) != 0 {
|
||||||
summary += " " + strings.Join(args, " ")
|
summary += " " + strings.Join(args, " ")
|
||||||
@@ -96,13 +97,11 @@ func (n *Node) Summary() string {
|
|||||||
func (n *Node) FlagSummary() string {
|
func (n *Node) FlagSummary() string {
|
||||||
required := []string{}
|
required := []string{}
|
||||||
count := 0
|
count := 0
|
||||||
for _, flag := range n.Flags {
|
for _, group := range n.AllFlags() {
|
||||||
count++
|
for _, flag := range group {
|
||||||
if flag.Required {
|
count++
|
||||||
if flag.IsBool() {
|
if flag.Required {
|
||||||
required = append(required, fmt.Sprintf("--%s", flag.Name))
|
required = append(required, flag.Summary())
|
||||||
} else {
|
|
||||||
required = append(required, fmt.Sprintf("--%s=%s", flag.Name, flag.FormatPlaceHolder()))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -128,7 +127,7 @@ func (n *Node) Path() (out string) {
|
|||||||
|
|
||||||
// A Value is either a flag or a variable positional argument.
|
// A Value is either a flag or a variable positional argument.
|
||||||
type Value struct {
|
type Value struct {
|
||||||
Flag bool // True if flag, false if positional argument.
|
Flag *Flag
|
||||||
Name string
|
Name string
|
||||||
Help string
|
Help string
|
||||||
Default string
|
Default string
|
||||||
@@ -136,11 +135,28 @@ type Value struct {
|
|||||||
Tag *Tag
|
Tag *Tag
|
||||||
Value reflect.Value
|
Value reflect.Value
|
||||||
Required bool
|
Required bool
|
||||||
Set bool // Used with Required to test if a value has been given.
|
Set bool // Set to true when this value is set through some mechanism.
|
||||||
Format string // Formatting directive, if applicable.
|
Format string // Formatting directive, if applicable.
|
||||||
Position int // Position (for positional arguments).
|
Position int // Position (for positional arguments).
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (v *Value) Summary() string {
|
||||||
|
if v.Flag != nil {
|
||||||
|
if v.IsBool() {
|
||||||
|
return fmt.Sprintf("--%s", v.Name)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("--%s=%s", v.Name, v.Flag.FormatPlaceHolder())
|
||||||
|
}
|
||||||
|
argText := "<" + v.Name + ">"
|
||||||
|
if v.IsCumulative() {
|
||||||
|
argText += " ..."
|
||||||
|
}
|
||||||
|
if !v.Required {
|
||||||
|
argText = "[" + argText + "]"
|
||||||
|
}
|
||||||
|
return argText
|
||||||
|
}
|
||||||
|
|
||||||
func (v *Value) IsCumulative() bool {
|
func (v *Value) IsCumulative() bool {
|
||||||
return v.Value.Kind() == reflect.Slice
|
return v.Value.Kind() == reflect.Slice
|
||||||
}
|
}
|
||||||
@@ -184,7 +200,7 @@ func (v *Value) Reset() error {
|
|||||||
type Positional = Value
|
type Positional = Value
|
||||||
|
|
||||||
type Flag struct {
|
type Flag struct {
|
||||||
Value
|
*Value
|
||||||
PlaceHolder string
|
PlaceHolder string
|
||||||
Env string
|
Env string
|
||||||
Short rune
|
Short rune
|
||||||
|
|||||||
+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.Leaves() {
|
for _, cmd := range p.Model.Leaves() {
|
||||||
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)
|
||||||
|
|||||||
+7
-7
@@ -26,8 +26,8 @@ func NoDefaultHelp() Option {
|
|||||||
// Name overrides the application name.
|
// Name overrides the application name.
|
||||||
func Name(name string) Option {
|
func Name(name string) Option {
|
||||||
return func(k *Kong) {
|
return func(k *Kong) {
|
||||||
if k.Application != nil {
|
if k.Model != nil {
|
||||||
k.Name = name
|
k.Model.Name = name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -55,8 +55,8 @@ func NamedMapper(name string, mapper Mapper) Option {
|
|||||||
// Description sets the application description.
|
// Description sets the application description.
|
||||||
func Description(description string) Option {
|
func Description(description string) Option {
|
||||||
return func(k *Kong) {
|
return func(k *Kong) {
|
||||||
if k.Application != nil {
|
if k.Model != nil {
|
||||||
k.Help = description
|
k.Model.Help = description
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -69,13 +69,13 @@ func Writers(stdout, stderr io.Writer) Option {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// HookFunction is a callback tied to a field of the grammar, called before a value is applied.
|
// HookFunc is a callback tied to a field of the grammar, called before a value is applied.
|
||||||
type HookFunction func(ctx *Context, path *Path) error
|
type HookFunc func(ctx *Context, path *Path) error
|
||||||
|
|
||||||
// Hook to aply before a command, flag or positional argument is encountered.
|
// Hook to aply before a command, flag or positional argument is encountered.
|
||||||
//
|
//
|
||||||
// "ptr" is a pointer to a field of the grammar.
|
// "ptr" is a pointer to a field of the grammar.
|
||||||
func Hook(ptr interface{}, hook HookFunction) Option {
|
func Hook(ptr interface{}, hook HookFunc) Option {
|
||||||
key := reflect.ValueOf(ptr)
|
key := reflect.ValueOf(ptr)
|
||||||
if key.Kind() != reflect.Ptr {
|
if key.Kind() != reflect.Ptr {
|
||||||
panic("expected a pointer")
|
panic("expected a pointer")
|
||||||
|
|||||||
+2
-2
@@ -10,8 +10,8 @@ func TestOptions(t *testing.T) {
|
|||||||
var cli struct{}
|
var cli struct{}
|
||||||
p, err := New(&cli, Name("name"), Description("description"), Writers(nil, nil), ExitFunction(nil))
|
p, err := New(&cli, Name("name"), Description("description"), Writers(nil, nil), ExitFunction(nil))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, "name", p.Name)
|
require.Equal(t, "name", p.Model.Name)
|
||||||
require.Equal(t, "description", p.Help)
|
require.Equal(t, "description", p.Model.Help)
|
||||||
require.Nil(t, p.Stdout)
|
require.Nil(t, p.Stdout)
|
||||||
require.Nil(t, p.Stderr)
|
require.Nil(t, p.Stderr)
|
||||||
require.Nil(t, p.Exit)
|
require.Nil(t, p.Exit)
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ type Tag struct {
|
|||||||
Env string
|
Env string
|
||||||
Short rune
|
Short rune
|
||||||
Hidden bool
|
Hidden bool
|
||||||
|
Sep string
|
||||||
|
|
||||||
// Storage for all tag keys for arbitrary lookups.
|
// Storage for all tag keys for arbitrary lookups.
|
||||||
items map[string]string
|
items map[string]string
|
||||||
@@ -109,24 +110,32 @@ func getTagInfo(ft reflect.StructField) (string, tagChars) {
|
|||||||
func parseTag(fv reflect.Value, ft reflect.StructField) *Tag {
|
func parseTag(fv reflect.Value, ft reflect.StructField) *Tag {
|
||||||
s, chars := getTagInfo(ft)
|
s, chars := getTagInfo(ft)
|
||||||
t := &Tag{
|
t := &Tag{
|
||||||
items: map[string]string{},
|
items: parseTagItems(s, chars),
|
||||||
}
|
}
|
||||||
if s == "" {
|
|
||||||
return t
|
|
||||||
}
|
|
||||||
|
|
||||||
t.items = parseTagItems(s, chars)
|
|
||||||
|
|
||||||
t.Cmd = t.Has("cmd")
|
t.Cmd = t.Has("cmd")
|
||||||
t.Arg = t.Has("arg")
|
t.Arg = t.Has("arg")
|
||||||
t.Required = t.Has("required")
|
required := t.Has("required")
|
||||||
t.Optional = t.Has("optional")
|
optional := t.Has("optional")
|
||||||
|
if required && optional {
|
||||||
|
fail("can't specify both required and optional")
|
||||||
|
}
|
||||||
|
t.Required = required
|
||||||
|
t.Optional = optional
|
||||||
t.Default, _ = t.Get("default")
|
t.Default, _ = t.Get("default")
|
||||||
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")
|
||||||
t.Short, _ = t.GetRune("short")
|
t.Short, _ = t.GetRune("short")
|
||||||
t.Hidden = t.Has("hidden")
|
t.Hidden = t.Has("hidden")
|
||||||
|
t.Format, _ = t.Get("format")
|
||||||
|
t.Sep, _ = t.Get("sep")
|
||||||
|
if t.Sep == "" {
|
||||||
|
if t.Cmd || t.Arg {
|
||||||
|
t.Sep = " "
|
||||||
|
} else {
|
||||||
|
t.Sep = ","
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
t.PlaceHolder, _ = t.Get("placeholder")
|
t.PlaceHolder, _ = t.Get("placeholder")
|
||||||
if t.PlaceHolder == "" {
|
if t.PlaceHolder == "" {
|
||||||
|
|||||||
+1
-1
@@ -96,7 +96,7 @@ func TestBareTagsWithJsonTag(t *testing.T) {
|
|||||||
|
|
||||||
func TestManySeps(t *testing.T) {
|
func TestManySeps(t *testing.T) {
|
||||||
var cli struct {
|
var cli struct {
|
||||||
Arg string `arg optional default:"hi"`
|
Arg string `arg optional default:"hi"`
|
||||||
}
|
}
|
||||||
|
|
||||||
p := mustNew(t, &cli)
|
p := mustNew(t, &cli)
|
||||||
|
|||||||
Reference in New Issue
Block a user