Separate validation into a distinct step.

This allows help to be called even when the parse trace is invalid.
Without this, the command-line would have to be valid in order to use
help at all, which defeats the purpose.
This commit is contained in:
Alec Thomas
2018-05-29 16:42:53 +10:00
committed by Gerald Kaszuba
parent afbb431641
commit fdc7230e22
14 changed files with 569 additions and 225 deletions
+5 -3
View File
@@ -9,8 +9,10 @@ import (
) )
var CLI struct { var CLI struct {
Help bool `kong:"help='Display help.'"` Debug bool `kong:"help='Debug mode.'"`
Rm struct { Output string `kong:"help='File to output to.',placeholder='FILE'"`
Rm struct {
Force bool `kong:"help='Force removal.'"` Force bool `kong:"help='Force removal.'"`
Recursive bool `kong:"help='Recursively remove files.'"` Recursive bool `kong:"help='Recursively remove files.'"`
@@ -23,7 +25,7 @@ var CLI struct {
} }
func main() { func main() {
app := kong.Must(&CLI).Hook(&CLI.Help, kong.Help(nil, nil)) app := kong.Must(&CLI, kong.Description("A shell-like example app."))
cmd, err := app.Parse(os.Args[1:]) cmd, err := app.Parse(os.Args[1:])
app.FatalIfErrorf(err) app.FatalIfErrorf(err)
s, _ := json.Marshal(&CLI) s, _ := json.Marshal(&CLI)
+22 -13
View File
@@ -6,7 +6,7 @@ import (
"strings" "strings"
) )
func build(ast interface{}) (app *Application, err error) { func build(ast interface{}, extraFlags []*Flag) (app *Application, err error) {
defer catch(&err) defer catch(&err)
v := reflect.ValueOf(ast) v := reflect.ValueOf(ast)
iv := reflect.Indirect(v) iv := reflect.Indirect(v)
@@ -15,11 +15,16 @@ func build(ast interface{}) (app *Application, err error) {
} }
app = &Application{} app = &Application{}
node := buildNode(iv, map[string]bool{}) seenFlags := map[string]bool{}
for _, flag := range extraFlags {
seenFlags[flag.Name] = true
}
node := buildNode(iv, ApplicationNode, seenFlags)
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...)
return app, nil return app, nil
} }
@@ -27,8 +32,9 @@ func dashedString(s string) string {
return strings.Join(camelCase(s), "-") return strings.Join(camelCase(s), "-")
} }
func buildNode(v reflect.Value, seenFlags map[string]bool) *Node { func buildNode(v reflect.Value, typ NodeType, seenFlags map[string]bool) *Node {
node := &Node{ node := &Node{
Type: typ,
Target: v, Target: v,
} }
for i := 0; i < v.NumField(); i++ { for i := 0; i < v.NumField(); i++ {
@@ -47,7 +53,11 @@ func buildNode(v reflect.Value, seenFlags map[string]bool) *Node {
// 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) {
buildChild(node, v, ft, fv, tag, name, seenFlags) typ := CommandNode
if tag.Arg {
typ = ArgumentNode
}
buildChild(node, typ, v, ft, fv, tag, name, seenFlags)
} else { } else {
buildField(node, v, ft, fv, tag, name, seenFlags) buildField(node, v, ft, fv, tag, name, seenFlags)
} }
@@ -60,19 +70,21 @@ func buildNode(v reflect.Value, seenFlags map[string]bool) *Node {
// Scan through argument positionals to ensure optional is never before a required. // Scan through argument positionals to ensure optional is never before a required.
last := true last := true
for _, p := range node.Positional { for i, p := range node.Positional {
if !last && p.Required { if !last && p.Required {
fail("argument %q can not be required after an optional", p.Name) fail("argument %q can not be required after an optional", p.Name)
} }
last = p.Required last = p.Required
p.Position = i
} }
return node return node
} }
func buildChild(node *Node, v reflect.Value, ft reflect.StructField, fv reflect.Value, tag *Tag, name string, seenFlags map[string]bool) { func buildChild(node *Node, typ NodeType, v reflect.Value, ft reflect.StructField, fv reflect.Value, tag *Tag, name string, seenFlags map[string]bool) {
child := buildNode(fv, seenFlags) child := buildNode(fv, typ, seenFlags)
child.Parent = node
child.Help = tag.Help child.Help = tag.Help
// 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
@@ -95,14 +107,11 @@ func buildChild(node *Node, v reflect.Value, ft reflect.StructField, fv reflect.
v.Type().Name(), ft.Name, child.Name) v.Type().Name(), ft.Name, child.Name)
} }
node.Children = append(node.Children, &Branch{Argument: &Argument{ child.Argument = value
Node: *child,
Argument: value,
}})
} else { } else {
child.Name = name child.Name = name
node.Children = append(node.Children, &Branch{Command: child})
} }
node.Children = append(node.Children, child)
if len(child.Positional) > 0 && len(child.Children) > 0 { if len(child.Positional) > 0 && len(child.Children) > 0 {
fail("can't mix positional arguments and branching arguments on %s.%s", v.Type().Name(), ft.Name) fail("can't mix positional arguments and branching arguments on %s.%s", v.Type().Name(), ft.Name)
@@ -141,7 +150,7 @@ func buildField(node *Node, v reflect.Value, ft reflect.StructField, fv reflect.
node.Flags = append(node.Flags, &Flag{ node.Flags = append(node.Flags, &Flag{
Value: value, Value: value,
Short: tag.Short, Short: tag.Short,
Placeholder: tag.Placeholder, PlaceHolder: tag.PlaceHolder,
Env: tag.Env, Env: tag.Env,
}) })
} }
+155 -99
View File
@@ -4,14 +4,17 @@ import (
"fmt" "fmt"
"io" "io"
"reflect" "reflect"
"strconv"
"strings" "strings"
) )
// Trace records the nodes and parsed values from the current command-line. // Path records the nodes and parsed values from the current command-line.
type Trace struct { type Path struct {
Parent *Node
// One of these will be non-nil. // One of these will be non-nil.
App *Application App *Application
Positional *Value Positional *Positional
Flag *Flag Flag *Flag
Argument *Argument Argument *Argument
Command *Command Command *Command
@@ -24,35 +27,92 @@ type Trace struct {
} }
type Context struct { type Context struct {
Trace []*Trace // A trace through parsed nodes. App *Kong
Error error // Error that occurred during trace, if any. Path []*Path // A trace through parsed nodes.
Error error // Error that occurred during trace, if any.
Stdout io.Writer Stdout io.Writer
Stderr io.Writer Stderr io.Writer
node *Node // Current node being parsed.
args []string args []string
app *Application
scan *Scanner scan *Scanner
} }
// Trace path of "args" through the gammar tree.
//
// The returned Context will include a Path of all commands, arguments, positionals and flags.
func Trace(k *Kong, args []string) (*Context, error) {
c := &Context{
App: k,
args: args,
Path: []*Path{
{App: k.Application, Flags: k.Flags, Value: k.Target},
},
}
err := c.reset(&c.App.Node)
if err != nil {
return nil, err
}
c.Error = c.trace(&c.App.Node)
return c, nil
}
func (c *Context) Validate() error {
for _, path := range c.Path {
if err := checkMissingFlags(path.Flags); err != nil {
return err
}
}
// Check the terminal node.
path := c.Path[len(c.Path)-1]
switch {
case path.App != nil:
if err := checkMissingChildren(&path.App.Node); err != nil {
return err
}
if err := checkMissingPositionals(0, path.App.Positional); err != nil {
return err
}
case path.Command != nil:
if err := checkMissingChildren(path.Command); err != nil {
return err
}
if err := checkMissingPositionals(0, path.Parent.Positional); err != nil {
return err
}
case path.Argument != nil:
if err := checkMissingChildren(path.Argument); err != nil {
return err
}
case path.Positional != nil:
if err := checkMissingPositionals(path.Positional.Position+1, path.Parent.Positional); err != nil {
return err
}
}
return nil
}
// Flags returns the accumulated available flags. // Flags returns the accumulated available flags.
func (p *Context) Flags() (flags []*Flag) { func (c *Context) Flags() (flags []*Flag) {
for _, trace := range p.Trace { for _, trace := range c.Path {
flags = append(flags, trace.Flags...) flags = append(flags, trace.Flags...)
} }
return return
} }
// Command returns the full command path. // Command returns the full command path.
func (p *Context) Command() (command []string) { func (c *Context) Command() (command []string) {
for _, trace := range p.Trace { for _, trace := range c.Path {
switch { switch {
case trace.Positional != nil: case trace.Positional != nil:
command = append(command, "<"+trace.Positional.Name+">") command = append(command, "<"+trace.Positional.Name+">")
case trace.Argument != nil: case trace.Argument != nil:
command = append(command, "<"+trace.Argument.Name+">") command = append(command, "<"+trace.Argument.Name+">")
case trace.Command != nil: case trace.Command != nil:
command = append(command, trace.Command.Name) command = append(command, trace.Command.Name)
} }
@@ -61,8 +121,8 @@ func (p *Context) Command() (command []string) {
} }
// FlagValue returns the set value of a flag, if it was encountered and exists. // FlagValue returns the set value of a flag, if it was encountered and exists.
func (p *Context) FlagValue(flag *Flag) reflect.Value { func (c *Context) FlagValue(flag *Flag) reflect.Value {
for _, trace := range p.Trace { for _, trace := range c.Path {
if trace.Flag == flag { if trace.Flag == flag {
return trace.Value return trace.Value
} }
@@ -71,8 +131,8 @@ func (p *Context) FlagValue(flag *Flag) reflect.Value {
} }
// Recursively reset values to defaults (as specified in the grammar) or the zero value. // Recursively reset values to defaults (as specified in the grammar) or the zero value.
func (p *Context) reset(node *Node) error { func (c *Context) reset(node *Node) error {
p.scan = Scan(p.args...) c.scan = Scan(c.args...)
for _, flag := range node.Flags { for _, flag := range node.Flags {
err := flag.Value.Reset() err := flag.Value.Reset()
if err != nil { if err != nil {
@@ -87,41 +147,36 @@ func (p *Context) reset(node *Node) error {
} }
for _, branch := range node.Children { for _, branch := range node.Children {
if branch.Argument != nil { if branch.Argument != nil {
arg := branch.Argument.Argument arg := branch.Argument
err := arg.Reset() err := arg.Reset()
if err != nil { if err != nil {
return err return err
} }
err = p.reset(&branch.Argument.Node) }
if err != nil { err := c.reset(branch)
return err if err != nil {
} return err
} else {
err := p.reset(branch.Command)
if err != nil {
return err
}
} }
} }
return nil return nil
} }
func (p *Context) trace(node *Node) (err error) { // nolint: gocyclo func (c *Context) trace(node *Node) (err error) { // nolint: gocyclo
positional := 0 positional := 0
p.node = node
flags := append(p.Flags(), node.Flags...)
for !p.scan.Peek().IsEOL() { flags := append(c.Flags(), node.Flags...)
token := p.scan.Peek()
for !c.scan.Peek().IsEOL() {
token := c.scan.Peek()
switch token.Type { switch token.Type {
case UntypedToken: case UntypedToken:
switch { switch {
// -- indicates end of parsing. All remaining arguments are treated as positional arguments only. // Indicates end of parsing. All remaining arguments are treated as positional arguments only.
case token.Value == "--": case token.Value == "--":
p.scan.Pop() c.scan.Pop()
args := []string{} args := []string{}
for { for {
token = p.scan.Pop() token = c.scan.Pop()
if token.Type == EOLToken { if token.Type == EOLToken {
break break
} }
@@ -129,46 +184,46 @@ func (p *Context) trace(node *Node) (err error) { // nolint: gocyclo
} }
// Note: tokens must be pushed in reverse order. // Note: tokens must be pushed in reverse order.
for i := range args { for i := range args {
p.scan.PushTyped(args[len(args)-1-i], PositionalArgumentToken) c.scan.PushTyped(args[len(args)-1-i], PositionalArgumentToken)
} }
// Long flag. // Long flag.
case strings.HasPrefix(token.Value, "--"): case strings.HasPrefix(token.Value, "--"):
p.scan.Pop() c.scan.Pop()
// Parse it and push the tokens. // Parse it and push the tokens.
parts := strings.SplitN(token.Value[2:], "=", 2) parts := strings.SplitN(token.Value[2:], "=", 2)
if len(parts) > 1 { if len(parts) > 1 {
p.scan.PushTyped(parts[1], FlagValueToken) c.scan.PushTyped(parts[1], FlagValueToken)
} }
p.scan.PushTyped(parts[0], FlagToken) c.scan.PushTyped(parts[0], FlagToken)
// Short flag. // Short flag.
case strings.HasPrefix(token.Value, "-"): case strings.HasPrefix(token.Value, "-"):
p.scan.Pop() c.scan.Pop()
// Note: tokens must be pushed in reverse order. // Note: tokens must be pushed in reverse order.
p.scan.PushTyped(token.Value[2:], ShortFlagTailToken) c.scan.PushTyped(token.Value[2:], ShortFlagTailToken)
p.scan.PushTyped(token.Value[1:2], ShortFlagToken) c.scan.PushTyped(token.Value[1:2], ShortFlagToken)
default: default:
p.scan.Pop() c.scan.Pop()
p.scan.PushTyped(token.Value, PositionalArgumentToken) c.scan.PushTyped(token.Value, PositionalArgumentToken)
} }
case ShortFlagTailToken: case ShortFlagTailToken:
p.scan.Pop() c.scan.Pop()
// Note: tokens must be pushed in reverse order. // Note: tokens must be pushed in reverse order.
p.scan.PushTyped(token.Value[1:], ShortFlagTailToken) c.scan.PushTyped(token.Value[1:], ShortFlagTailToken)
p.scan.PushTyped(token.Value[0:1], ShortFlagToken) c.scan.PushTyped(token.Value[0:1], ShortFlagToken)
case FlagToken: case FlagToken:
if err := p.matchFlags(flags, func(f *Flag) bool { if err := c.matchFlags(flags, func(f *Flag) bool {
return f.Name == token.Value return f.Name == token.Value
}); err != nil { }); err != nil {
return err return err
} }
case ShortFlagToken: case ShortFlagToken:
if err := p.matchFlags(flags, func(f *Flag) bool { if err := c.matchFlags(flags, func(f *Flag) bool {
return string(f.Name) == token.Value return string(f.Name) == token.Value
}); err != nil { }); err != nil {
return err return err
@@ -181,38 +236,45 @@ func (p *Context) trace(node *Node) (err error) { // nolint: gocyclo
// 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]
value, err := arg.Parse(p.scan) value, err := arg.Parse(c.scan)
if err != nil { if err != nil {
return err return err
} }
p.Trace = append(p.Trace, &Trace{Positional: arg, Value: value, Flags: node.Flags}) c.Path = append(c.Path, &Path{
Parent: node,
Positional: arg,
Value: value,
Flags: node.Flags,
})
positional++ positional++
break break
} }
// After positional arguments have been consumed, handle commands and branching arguments. // After positional arguments have been consumed, handle commands and branching arguments.
for _, branch := range node.Children { for _, branch := range node.Children {
switch { switch branch.Type {
case branch.Command != nil: case CommandNode:
if branch.Command.Name == token.Value { if branch.Name == token.Value {
p.scan.Pop() c.scan.Pop()
p.Trace = append(p.Trace, &Trace{ c.Path = append(c.Path, &Path{
Command: branch.Command, Parent: node,
Command: branch,
Value: branch.Target,
Flags: node.Flags, Flags: node.Flags,
Value: branch.Command.Target,
}) })
return p.trace(branch.Command) return c.trace(branch)
} }
case branch.Argument != nil: case ArgumentNode:
arg := branch.Argument.Argument arg := branch.Argument
if value, err := arg.Parse(p.scan); err == nil { if value, err := arg.Parse(c.scan); err == nil {
p.Trace = append(p.Trace, &Trace{ c.Path = append(c.Path, &Path{
Argument: branch.Argument, Parent: node,
Argument: branch,
Value: value, Value: value,
Flags: node.Flags, Flags: node.Flags,
}) })
return p.trace(&branch.Argument.Node) return c.trace(branch)
} }
} }
} }
@@ -222,22 +284,13 @@ func (p *Context) trace(node *Node) (err error) { // nolint: gocyclo
return fmt.Errorf("unexpected token %s", token) return fmt.Errorf("unexpected token %s", token)
} }
} }
if err := checkMissingPositionals(positional, node.Positional); err != nil {
return err
}
if err := checkMissingChildren(node.Children); err != nil {
return err
}
return nil return nil
} }
// Apply traced context to the target grammar. // Apply traced context to the target grammar.
func (p *Context) Apply() (string, error) { func (c *Context) Apply() (string, error) {
path := []string{} path := []string{}
for _, trace := range p.Trace { for _, trace := range c.Path {
switch { switch {
case trace.Argument != nil: case trace.Argument != nil:
path = append(path, "<"+trace.Argument.Name+">") path = append(path, "<"+trace.Argument.Name+">")
@@ -254,6 +307,24 @@ func (p *Context) Apply() (string, error) {
return strings.Join(path, " "), nil return strings.Join(path, " "), nil
} }
func (c *Context) matchFlags(flags []*Flag, matcher func(f *Flag) bool) (err error) {
defer catch(&err)
token := c.scan.Peek()
for _, flag := range flags {
// Found a matching flag.
if flag.Name == token.Value {
c.scan.Pop()
value, err := flag.Parse(c.scan)
if err != nil {
return err
}
c.Path = append(c.Path, &Path{Flag: flag, Value: value})
return nil
}
}
return fmt.Errorf("unknown flag --%s", token.Value)
}
func checkMissingFlags(flags []*Flag) error { func checkMissingFlags(flags []*Flag) error {
missing := []string{} missing := []string{}
for _, flag := range flags { for _, flag := range flags {
@@ -269,23 +340,26 @@ func checkMissingFlags(flags []*Flag) error {
return fmt.Errorf("missing flags: %s", strings.Join(missing, ", ")) return fmt.Errorf("missing flags: %s", strings.Join(missing, ", "))
} }
func checkMissingChildren(children []*Branch) error { func checkMissingChildren(node *Node) error {
missing := []string{} missing := []string{}
for _, child := range children { for _, child := range node.Children {
if child.Argument != nil { if child.Argument != nil {
if !child.Argument.Argument.Required { if !child.Argument.Required {
continue continue
} }
missing = append(missing, "<"+child.Argument.Name+">") missing = append(missing, strconv.Quote("<"+child.Argument.Name+">"))
} else { } else {
missing = append(missing, child.Command.Name) missing = append(missing, strconv.Quote(child.Name))
} }
} }
if len(missing) == 0 { if len(missing) == 0 {
return nil return nil
} }
return fmt.Errorf("expected one of %s", strings.Join(missing, ", ")) if len(missing) == 1 {
return fmt.Errorf("%q should be followed by %s", node.Path(), missing[0])
}
return fmt.Errorf("%q should be followed by one of %s", node.Path(), 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.
@@ -306,21 +380,3 @@ func checkMissingPositionals(positional int, values []*Value) error {
} }
return fmt.Errorf("missing positional arguments %s", strings.Join(missing, " ")) return fmt.Errorf("missing positional arguments %s", strings.Join(missing, " "))
} }
func (p *Context) matchFlags(flags []*Flag, matcher func(f *Flag) bool) (err error) {
defer catch(&err)
token := p.scan.Peek()
for _, flag := range flags {
// Found a matching flag.
if flag.Name == token.Value {
p.scan.Pop()
value, err := flag.Parse(p.scan)
if err != nil {
return err
}
p.Trace = append(p.Trace, &Trace{Flag: flag, Value: value})
return nil
}
}
return fmt.Errorf("unknown flag --%s", token.Value)
}
+9
View File
@@ -0,0 +1,9 @@
// +build appengine !linux,!freebsd,!darwin,!dragonfly,!netbsd,!openbsd
package kong
import "io"
func guessWidth(w io.Writer) int {
return 80
}
+38
View File
@@ -0,0 +1,38 @@
// +build !appengine,linux freebsd darwin dragonfly netbsd openbsd
package kong
import (
"io"
"os"
"strconv"
"syscall"
"unsafe"
)
func guessWidth(w io.Writer) int {
// check if COLUMNS env is set to comply with
// http://pubs.opengroup.org/onlinepubs/009604499/basedefs/xbd_chap08.html
colsStr := os.Getenv("COLUMNS")
if colsStr != "" {
if cols, err := strconv.Atoi(colsStr); err == nil {
return cols
}
}
if t, ok := w.(*os.File); ok {
fd := t.Fd()
var dimensions [4]uint16
if _, _, err := syscall.Syscall6(
syscall.SYS_IOCTL,
uintptr(fd),
uintptr(syscall.TIOCGWINSZ),
uintptr(unsafe.Pointer(&dimensions)),
0, 0, 0,
); err == 0 {
return int(dimensions[1])
}
}
return 80
}
+137 -14
View File
@@ -1,36 +1,159 @@
package kong package kong
import ( import (
"text/template" "bytes"
"fmt"
"go/doc"
"io"
"reflect"
"strings"
"github.com/aymerick/raymond"
) )
const defaultHelp = `{{- with .Application -}} const (
usage: {{.Name}} defaultIndent = 2
defaultTemplate = `
{{#with App}}
usage: {{Name}}
{{.Help}} {{#wrap}}
{{range .Context.Flags}} {{Help}}
--{{.Name}} {{/wrap}}
{{end}}
{{- end -}} Flags:
{{#indent}}
{{formatFlags Flags}}
{{/indent}}
{{#if Children}}
{{#indent}}
{{#each Children}}
{{Name}}
{{/each}}
{{/indent}}
{{/if}}
{{/with}}
` `
)
var defaultHelpTemplate = template.Must(template.New("help").Parse(defaultHelp)) var defaultHelpTemplate = raymond.MustParse(strings.TrimSpace(defaultTemplate))
func init() {
defaultHelpTemplate.RegisterHelpers(map[string]interface{}{
"indent": func(options *raymond.Options) string {
indent, ok := options.HashProp("depth").(int)
if !ok {
indent = 2
}
width := options.Data("width").(int)
frame := options.NewDataFrame()
frame.Set("width", width-indent)
indentStr := strings.Repeat(" ", indent)
lines := strings.Split(options.FnData(frame), "\n")
for i, line := range lines {
lines[i] = indentStr + line
}
return strings.Join(lines, "\n")
},
"formatFlags": func(flags []*Flag, options *raymond.Options) string {
rows := [][2]string{}
haveShort := false
for _, flag := range flags {
if flag.Short != 0 {
haveShort = true
break
}
}
for _, flag := range flags {
if !flag.Hidden {
rows = append(rows, [2]string{formatFlag(haveShort, flag), flag.Help})
}
}
w := bytes.NewBuffer(nil)
formatTwoColumns(w, 0, 2, options.Data("width").(int), rows)
return w.String()
},
"wrap": func(options *raymond.Options) string {
w := bytes.NewBuffer(nil)
doc.ToText(w, options.Fn(), "", " ", options.Data("width").(int))
return w.String()
},
})
}
// Help returns a Hook that will display help and exit. // Help returns a Hook that will display help and exit.
func Help(tmpl *template.Template, tmplctx map[string]interface{}) HookFunction { //
return func(app *Kong, ctx *Context, trace *Trace) error { // tmpl receives a context with several top-level values, in addition to those passed through tmplctx:
// .Context which is of type *Context and .Path which is of type *Path.
func Help(tmpl *raymond.Template, tmplctx map[string]interface{}) HookFunction {
return func(ctx *Context, path *Path) error {
merged := map[string]interface{}{ merged := map[string]interface{}{
"Application": app.Model, "App": ctx.App,
"Context": ctx,
"Path": path,
} }
for k, v := range tmplctx { for k, v := range tmplctx {
merged[k] = v merged[k] = v
} }
err := tmpl.Execute(app.Stdout, merged) frame := raymond.NewDataFrame()
frame.Set("width", guessWidth(ctx.App.Stdout))
output, err := tmpl.ExecWith(merged, frame)
if err != nil { if err != nil {
return err return err
} }
app.Exit(0) io.WriteString(ctx.App.Stdout, output)
ctx.App.Exit(0)
return nil return nil
} }
} }
func formatTwoColumns(w io.Writer, indent, padding, width int, rows [][2]string) {
// Find size of first column.
s := 0
for _, row := range rows {
if c := len(row[0]); c > s && c < 30 {
s = c
}
}
indentStr := strings.Repeat(" ", indent)
offsetStr := strings.Repeat(" ", s+padding)
for _, row := range rows {
buf := bytes.NewBuffer(nil)
doc.ToText(buf, row[1], "", strings.Repeat(" ", defaultIndent), width-s-padding-indent)
lines := strings.Split(strings.TrimRight(buf.String(), "\n"), "\n")
fmt.Fprintf(w, "%s%-*s%*s", indentStr, s, row[0], padding, "")
if len(row[0]) >= 30 {
fmt.Fprintf(w, "\n%s%s", indentStr, offsetStr)
}
fmt.Fprintf(w, "%s\n", lines[0])
for _, line := range lines[1:] {
fmt.Fprintf(w, "%s%s%s\n", indentStr, offsetStr, line)
}
}
}
// haveShort will be true if there are short flags present at all in the help. Useful for column alignment.
func formatFlag(haveShort bool, flag *Flag) string {
flagString := ""
name := flag.Name
isBool := flag.IsBool()
if flag.Short != 0 {
flagString += fmt.Sprintf("-%c, --%s", flag.Short, name)
} else {
if haveShort {
flagString += fmt.Sprintf(" --%s", name)
} else {
flagString += fmt.Sprintf("--%s", name)
}
}
if !isBool {
flagString += fmt.Sprintf("=%s", flag.FormatPlaceHolder())
}
if flag.Value.Value.Kind() == reflect.Slice {
flagString += " ..."
}
return flagString
}
+31
View File
@@ -0,0 +1,31 @@
package kong
import (
"bytes"
"fmt"
"testing"
"github.com/stretchr/testify/require"
)
func TestHelp(t *testing.T) {
var cli struct {
String string `kong:"help='A string flag.'"`
Bool bool `kong:"help='A bool flag.'"`
One struct {
} `kong:"cmd"`
}
w := bytes.NewBuffer(nil)
exited := false
app := mustNew(t, &cli,
Name("test-app"),
Description("A test app."),
Writers(w, w),
ExitFunction(func(int) { exited = true }),
)
_, err := app.Parse([]string{"--help"})
require.NoError(t, err)
require.True(t, exited)
fmt.Println(w.String())
}
+39 -50
View File
@@ -6,10 +6,9 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"reflect" "reflect"
"text/template"
) )
type HookFunction func(app *Kong, ctx *Context, trace *Trace) error type HookFunction func(ctx *Context, path *Path) error
// Error reported by Kong. // Error reported by Kong.
type Error struct{ msg string } type Error struct{ msg string }
@@ -30,53 +29,54 @@ 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 {
Model *Application // Grammar model.
*Application
// Termination function (defaults to os.Exit) // Termination function (defaults to os.Exit)
Exit func(int) Exit func(int)
Stdout io.Writer Stdout io.Writer
Stderr io.Writer Stderr io.Writer
help *template.Template
helpContext map[string]interface{}
helpFuncs template.FuncMap
hooks map[reflect.Value]HookFunction hooks map[reflect.Value]HookFunction
noDefaultHelp bool noDefaultHelp bool
} }
// New creates a new Kong parser into ast. // New creates a new Kong parser on grammar.
func New(ast interface{}, options ...Option) (*Kong, error) { //
// See the README (https://github.com/alecthomas/kong) for usage instructions.
func New(grammar interface{}, options ...Option) (*Kong, error) {
k := &Kong{ k := &Kong{
Exit: os.Exit, Exit: os.Exit,
Stdout: os.Stdout, Stdout: os.Stdout,
Stderr: os.Stderr, Stderr: os.Stderr,
help: defaultHelpTemplate, hooks: map[reflect.Value]HookFunction{},
helpContext: map[string]interface{}{},
helpFuncs: template.FuncMap{},
hooks: map[reflect.Value]HookFunction{},
} }
model, err := build(ast)
if err != nil {
return k, err
}
k.Model = model
k.Model.Name = filepath.Base(os.Args[0])
for _, option := range options { for _, option := range options {
option(k) option(k)
} }
if !k.noDefaultHelp { model, err := build(grammar, k.extraFlags())
k.integrateHelp() if err != nil {
return k, err
} }
k.Application = model
k.Name = filepath.Base(os.Args[0])
for _, option := range options {
option(k)
}
return k, nil return k, nil
} }
func (k *Kong) integrateHelp() { // Provide additional builtin flags, if any.
func (k *Kong) extraFlags() []*Flag {
if k.noDefaultHelp {
return nil
}
helpValue := false helpValue := false
help := &Flag{ helpFlag := &Flag{
Value: Value{ Value: Value{
Name: "help", Name: "help",
Help: "Show context-sensitive help.", Help: "Show context-sensitive help.",
@@ -85,28 +85,14 @@ func (k *Kong) integrateHelp() {
Decoder: kindDecoders[reflect.Bool], Decoder: kindDecoders[reflect.Bool],
}, },
} }
k.Model.Flags = append([]*Flag{help}, k.Model.Flags...) hook := Hook(&helpValue, Help(defaultHelpTemplate, nil))
Hook(&helpValue, Help(defaultHelpTemplate, nil))(k) hook(k)
return []*Flag{helpFlag}
} }
// Trace parses the command-line, validating and collecting matching grammar nodes. // Path parses the command-line, validating and collecting matching grammar nodes.
func (k *Kong) Trace(args []string) (*Context, error) { func (k *Kong) Trace(args []string) (*Context, error) {
p := &Context{ return Trace(k, args)
app: k.Model,
args: args,
Trace: []*Trace{
{App: k.Model, Flags: append([]*Flag{}, k.Model.Flags...), Value: k.Model.Target},
},
}
err := p.reset(&p.app.Node)
if err != nil {
return nil, err
}
p.Error = p.trace(&p.app.Node)
if err = checkMissingFlags(p.Flags()); err != nil {
return nil, err
}
return p, nil
} }
// Parse arguments into target. // Parse arguments into target.
@@ -125,11 +111,14 @@ func (k *Kong) Parse(args []string) (command string, err error) {
if ctx.Error != nil { if ctx.Error != nil {
return "", ctx.Error return "", ctx.Error
} }
if err = ctx.Validate(); err != nil {
return "", err
}
return ctx.Apply() return ctx.Apply()
} }
func (k *Kong) applyHooks(ctx *Context) error { func (k *Kong) applyHooks(ctx *Context) error {
for _, trace := range ctx.Trace { for _, trace := range ctx.Path {
var key reflect.Value var key reflect.Value
switch { switch {
case trace.App != nil: case trace.App != nil:
@@ -143,13 +132,13 @@ func (k *Kong) applyHooks(ctx *Context) error {
case trace.Flag != nil: case trace.Flag != nil:
key = trace.Flag.Value.Value key = trace.Flag.Value.Value
default: default:
panic("unsupported Trace") panic("unsupported Path")
} }
if key.IsValid() { if key.IsValid() {
key = key.Addr() key = key.Addr()
} }
if hook := k.hooks[key]; hook != nil { if hook := k.hooks[key]; hook != nil {
if err := hook(k, ctx, trace); err != nil { if err := hook(ctx, trace); err != nil {
return err return err
} }
} }
@@ -159,12 +148,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.Model.Name+": "+format, args...) fmt.Fprintf(k.Stdout, k.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.Model.Name+": "+format, args...) fmt.Fprintf(k.Stderr, k.Name+": "+format, args...)
} }
// FatalIfError terminates with an error message if err != nil. // FatalIfError terminates with an error message if err != nil.
+8 -11
View File
@@ -9,9 +9,9 @@ import (
func mustNew(t *testing.T, cli interface{}, options ...Option) *Kong { func mustNew(t *testing.T, cli interface{}, options ...Option) *Kong {
t.Helper() t.Helper()
options = append(options, ExitFunction(func(int) { options = append([]Option{ExitFunction(func(int) {
t.Fatalf("unexpected exit()") t.Fatalf("unexpected exit()")
})) })}, options...)
parser, err := New(cli, options...) parser, err := New(cli, options...)
require.NoError(t, err) require.NoError(t, err)
return parser return parser
@@ -353,10 +353,10 @@ func TestTraceErrorPartiallySucceeds(t *testing.T) {
} `kong:"cmd"` } `kong:"cmd"`
} }
p := mustNew(t, &cli) p := mustNew(t, &cli)
trace, err := p.Trace([]string{"one", "bad"}) ctx, err := p.Trace([]string{"one", "bad"})
require.NoError(t, err) require.NoError(t, err)
require.Error(t, trace.Error) require.Error(t, ctx.Error)
require.Equal(t, []string{"one"}, trace.Command()) require.Equal(t, []string{"one"}, ctx.Command())
} }
func TestHooks(t *testing.T) { func TestHooks(t *testing.T) {
@@ -382,9 +382,9 @@ func TestHooks(t *testing.T) {
{"Flag", "one --three=three", values{true, "", "three"}}, {"Flag", "one --three=three", values{true, "", "three"}},
{"ArgAndFlag", "one two --three=three", values{true, "two", "three"}}, {"ArgAndFlag", "one two --three=three", values{true, "two", "three"}},
} }
setOne := func(app *Kong, ctx *Context, trace *Trace) error { hooked.one = true; return nil } setOne := func(ctx *Context, path *Path) error { hooked.one = true; return nil }
setTwo := func(app *Kong, ctx *Context, trace *Trace) error { hooked.two = trace.Value.String(); return nil } setTwo := func(ctx *Context, path *Path) error { hooked.two = path.Value.String(); return nil }
setThree := func(app *Kong, ctx *Context, trace *Trace) error { hooked.three = trace.Value.String(); return nil } setThree := func(ctx *Context, path *Path) error { hooked.three = path.Value.String(); return nil }
p := mustNew(t, &cli, p := mustNew(t, &cli,
Hook(&cli.One, setOne), Hook(&cli.One, setOne),
Hook(&cli.One.Two, setTwo), Hook(&cli.One.Two, setTwo),
@@ -399,6 +399,3 @@ func TestHooks(t *testing.T) {
}) })
} }
} }
func TestHelp(t *testing.T) {
}
+76 -14
View File
@@ -1,27 +1,70 @@
package kong package kong
import "reflect" import (
"reflect"
"strconv"
"strings"
)
type Application struct { type Application struct {
Node Node
HelpFlag *Flag HelpFlag *Flag
} }
// A Branch is a command or positional argument that results in a branch in the command tree. // Leaves returns the leaf commands/arguments in the command-line grammar.
type Branch struct { func (a *Application) Leaves() (out []*Node) {
Command *Command var walk func(n *Node)
Argument *Argument walk = func(n *Node) {
if len(n.Children) == 0 {
out = append(out, n)
}
for _, child := range n.Children {
if child.Type == CommandNode || child.Type == ArgumentNode {
walk(child)
}
}
}
walk(&a.Node)
return
} }
type Argument = Node
type Command = Node type Command = Node
type NodeType int
const (
ApplicationNode NodeType = iota
CommandNode
ArgumentNode
)
type Node struct { type Node struct {
Type NodeType
Parent *Node
Name string Name string
Help string Help string
Flags []*Flag Flags []*Flag
Positional []*Value Positional []*Positional
Children []*Branch Children []*Node
Target reflect.Value Target reflect.Value // Pointer to the value in the grammar that this Node is associated with.
Argument *Value // Populated when Type is ArgumentNode.
}
// 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:
out += " " + n.Name
case ArgumentNode:
out += " " + "<" + n.Name + ">"
}
return strings.TrimSpace(out)
} }
// A Value is either a flag or a variable positional argument. // A Value is either a flag or a variable positional argument.
@@ -36,6 +79,11 @@ type Value struct {
Required bool Required bool
Set bool // Used with Required to test if a value has been given. Set bool // Used with Required to test if a value has been given.
Format string // Formatting directive, if applicable. Format string // Formatting directive, if applicable.
Position int // Position (for positional arguments).
}
func (v *Value) IsBool() bool {
return v.Value.Kind() == reflect.Bool
} }
// Parse tokens into value, parse, and validate, but do not write to the field. // Parse tokens into value, parse, and validate, but do not write to the field.
@@ -69,14 +117,28 @@ func (v *Value) Reset() error {
type Positional = Value type Positional = Value
type Argument struct {
Node
Argument *Value
}
type Flag struct { type Flag struct {
Value Value
Placeholder string PlaceHolder string
Env string Env string
Short rune Short rune
Hidden bool
}
func (f *Flag) FormatPlaceHolder() string {
if f.PlaceHolder != "" {
return f.PlaceHolder
}
if f.Default != "" {
ellipsis := ""
if len(f.Default) > 1 {
ellipsis = "..."
}
if f.Value.Value.Kind() == reflect.String {
return strconv.Quote(f.Default) + ellipsis
}
return f.Default + ellipsis
}
return strings.ToUpper(f.Name)
} }
+27
View File
@@ -0,0 +1,27 @@
package kong
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestModelApplicationCommands(t *testing.T) {
var cli struct {
One struct {
Two struct {
} `kong:"cmd"`
Three struct {
Four struct {
Four string `kong:"arg"`
} `kong:"arg"`
} `kong:"cmd"`
} `kong:"cmd"`
}
p := mustNew(t, &cli)
actual := []string{}
for _, cmd := range p.Leaves() {
actual = append(actual, cmd.Path())
}
require.Equal(t, []string{"one two", "one three <four>"}, actual)
}
+14 -15
View File
@@ -3,9 +3,12 @@ package kong
import ( import (
"io" "io"
"reflect" "reflect"
"text/template"
) )
// Options apply optional changes to the Kong application.
//
// Note that Options are applied twice: once just prior to the grammar is constructed and once after. In the
// former case, Kong.Application will be nil.
type Option func(k *Kong) type Option func(k *Kong)
// ExitFunction overrides the function used to terminate. This is useful for testing or interactive use. // ExitFunction overrides the function used to terminate. This is useful for testing or interactive use.
@@ -22,24 +25,20 @@ 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) { k.Model.Name = name } return func(k *Kong) {
if k.Application != nil {
k.Name = name
}
}
} }
// Description sets the application description. // Description sets the application description.
func Description(description string) Option { func Description(description string) Option {
return func(k *Kong) { k.Model.Help = description } return func(k *Kong) {
} if k.Application != nil {
k.Help = description
// HelpTemplate overrides the default help template. }
func HelpTemplate(template *template.Template) Option { }
return func(k *Kong) { k.help = template }
}
// HelpContext sets extra context in the help template.
//
// The key "Application" will always be available and is the root of the application model.
func HelpContext(context map[string]interface{}) Option {
return func(k *Kong) { k.helpContext = context }
} }
// Writers overrides the default writers. Useful for testing or interactive use. // Writers overrides the default writers. Useful for testing or interactive use.
+2 -2
View File
@@ -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.Model.Name) require.Equal(t, "name", p.Name)
require.Equal(t, "description", p.Model.Help) require.Equal(t, "description", p.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)
+6 -4
View File
@@ -17,9 +17,10 @@ type Tag struct {
Type string Type string
Default string Default string
Format string Format string
Placeholder string PlaceHolder string
Env string Env string
Short rune Short rune
Hidden bool
// 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,10 +110,11 @@ func parseTag(fv reflect.Value, s string) *Tag {
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.Placeholder, _ = t.Get("placeholder") t.PlaceHolder, _ = t.Get("placeholder")
if t.Placeholder == "" { if t.PlaceHolder == "" {
t.Placeholder = strings.ToUpper(dashedString(fv.Type().Name())) t.PlaceHolder = strings.ToUpper(dashedString(fv.Type().Name()))
} }
return t return t