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 {
Help bool `kong:"help='Display help.'"`
Rm struct {
Debug bool `kong:"help='Debug mode.'"`
Output string `kong:"help='File to output to.',placeholder='FILE'"`
Rm struct {
Force bool `kong:"help='Force removal.'"`
Recursive bool `kong:"help='Recursively remove files.'"`
@@ -23,7 +25,7 @@ var CLI struct {
}
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:])
app.FatalIfErrorf(err)
s, _ := json.Marshal(&CLI)
+22 -13
View File
@@ -6,7 +6,7 @@ import (
"strings"
)
func build(ast interface{}) (app *Application, err error) {
func build(ast interface{}, extraFlags []*Flag) (app *Application, err error) {
defer catch(&err)
v := reflect.ValueOf(ast)
iv := reflect.Indirect(v)
@@ -15,11 +15,16 @@ func build(ast interface{}) (app *Application, err error) {
}
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 {
return nil, fmt.Errorf("can't mix positional arguments and branching arguments on %T", ast)
}
app.Node = *node
app.Node.Flags = append(extraFlags, app.Node.Flags...)
return app, nil
}
@@ -27,8 +32,9 @@ func dashedString(s string) string {
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{
Type: typ,
Target: v,
}
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.
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 {
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.
last := true
for _, p := range node.Positional {
for i, p := range node.Positional {
if !last && p.Required {
fail("argument %q can not be required after an optional", p.Name)
}
last = p.Required
p.Position = i
}
return node
}
func buildChild(node *Node, v reflect.Value, ft reflect.StructField, fv reflect.Value, tag *Tag, name string, seenFlags map[string]bool) {
child := buildNode(fv, seenFlags)
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, typ, seenFlags)
child.Parent = node
child.Help = tag.Help
// 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)
}
node.Children = append(node.Children, &Branch{Argument: &Argument{
Node: *child,
Argument: value,
}})
child.Argument = value
} else {
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 {
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{
Value: value,
Short: tag.Short,
Placeholder: tag.Placeholder,
PlaceHolder: tag.PlaceHolder,
Env: tag.Env,
})
}
+155 -99
View File
@@ -4,14 +4,17 @@ import (
"fmt"
"io"
"reflect"
"strconv"
"strings"
)
// Trace records the nodes and parsed values from the current command-line.
type Trace struct {
// Path records the nodes and parsed values from the current command-line.
type Path struct {
Parent *Node
// One of these will be non-nil.
App *Application
Positional *Value
Positional *Positional
Flag *Flag
Argument *Argument
Command *Command
@@ -24,35 +27,92 @@ type Trace struct {
}
type Context struct {
Trace []*Trace // A trace through parsed nodes.
Error error // Error that occurred during trace, if any.
App *Kong
Path []*Path // A trace through parsed nodes.
Error error // Error that occurred during trace, if any.
Stdout io.Writer
Stderr io.Writer
node *Node // Current node being parsed.
args []string
app *Application
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.
func (p *Context) Flags() (flags []*Flag) {
for _, trace := range p.Trace {
func (c *Context) Flags() (flags []*Flag) {
for _, trace := range c.Path {
flags = append(flags, trace.Flags...)
}
return
}
// Command returns the full command path.
func (p *Context) Command() (command []string) {
for _, trace := range p.Trace {
func (c *Context) Command() (command []string) {
for _, trace := range c.Path {
switch {
case trace.Positional != nil:
command = append(command, "<"+trace.Positional.Name+">")
case trace.Argument != nil:
command = append(command, "<"+trace.Argument.Name+">")
case trace.Command != nil:
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.
func (p *Context) FlagValue(flag *Flag) reflect.Value {
for _, trace := range p.Trace {
func (c *Context) FlagValue(flag *Flag) reflect.Value {
for _, trace := range c.Path {
if trace.Flag == flag {
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.
func (p *Context) reset(node *Node) error {
p.scan = Scan(p.args...)
func (c *Context) reset(node *Node) error {
c.scan = Scan(c.args...)
for _, flag := range node.Flags {
err := flag.Value.Reset()
if err != nil {
@@ -87,41 +147,36 @@ func (p *Context) reset(node *Node) error {
}
for _, branch := range node.Children {
if branch.Argument != nil {
arg := branch.Argument.Argument
arg := branch.Argument
err := arg.Reset()
if err != nil {
return err
}
err = p.reset(&branch.Argument.Node)
if err != nil {
return err
}
} else {
err := p.reset(branch.Command)
if err != nil {
return err
}
}
err := c.reset(branch)
if err != nil {
return err
}
}
return nil
}
func (p *Context) trace(node *Node) (err error) { // nolint: gocyclo
func (c *Context) trace(node *Node) (err error) { // nolint: gocyclo
positional := 0
p.node = node
flags := append(p.Flags(), node.Flags...)
for !p.scan.Peek().IsEOL() {
token := p.scan.Peek()
flags := append(c.Flags(), node.Flags...)
for !c.scan.Peek().IsEOL() {
token := c.scan.Peek()
switch token.Type {
case UntypedToken:
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 == "--":
p.scan.Pop()
c.scan.Pop()
args := []string{}
for {
token = p.scan.Pop()
token = c.scan.Pop()
if token.Type == EOLToken {
break
}
@@ -129,46 +184,46 @@ func (p *Context) trace(node *Node) (err error) { // nolint: gocyclo
}
// Note: tokens must be pushed in reverse order.
for i := range args {
p.scan.PushTyped(args[len(args)-1-i], PositionalArgumentToken)
c.scan.PushTyped(args[len(args)-1-i], PositionalArgumentToken)
}
// Long flag.
case strings.HasPrefix(token.Value, "--"):
p.scan.Pop()
c.scan.Pop()
// Parse it and push the tokens.
parts := strings.SplitN(token.Value[2:], "=", 2)
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.
case strings.HasPrefix(token.Value, "-"):
p.scan.Pop()
c.scan.Pop()
// Note: tokens must be pushed in reverse order.
p.scan.PushTyped(token.Value[2:], ShortFlagTailToken)
p.scan.PushTyped(token.Value[1:2], ShortFlagToken)
c.scan.PushTyped(token.Value[2:], ShortFlagTailToken)
c.scan.PushTyped(token.Value[1:2], ShortFlagToken)
default:
p.scan.Pop()
p.scan.PushTyped(token.Value, PositionalArgumentToken)
c.scan.Pop()
c.scan.PushTyped(token.Value, PositionalArgumentToken)
}
case ShortFlagTailToken:
p.scan.Pop()
c.scan.Pop()
// Note: tokens must be pushed in reverse order.
p.scan.PushTyped(token.Value[1:], ShortFlagTailToken)
p.scan.PushTyped(token.Value[0:1], ShortFlagToken)
c.scan.PushTyped(token.Value[1:], ShortFlagTailToken)
c.scan.PushTyped(token.Value[0:1], ShortFlagToken)
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
}); err != nil {
return err
}
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
}); err != nil {
return err
@@ -181,38 +236,45 @@ func (p *Context) trace(node *Node) (err error) { // nolint: gocyclo
// Ensure we've consumed all positional arguments.
if positional < len(node.Positional) {
arg := node.Positional[positional]
value, err := arg.Parse(p.scan)
value, err := arg.Parse(c.scan)
if err != nil {
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++
break
}
// After positional arguments have been consumed, handle commands and branching arguments.
for _, branch := range node.Children {
switch {
case branch.Command != nil:
if branch.Command.Name == token.Value {
p.scan.Pop()
p.Trace = append(p.Trace, &Trace{
Command: branch.Command,
switch branch.Type {
case CommandNode:
if branch.Name == token.Value {
c.scan.Pop()
c.Path = append(c.Path, &Path{
Parent: node,
Command: branch,
Value: branch.Target,
Flags: node.Flags,
Value: branch.Command.Target,
})
return p.trace(branch.Command)
return c.trace(branch)
}
case branch.Argument != nil:
arg := branch.Argument.Argument
if value, err := arg.Parse(p.scan); err == nil {
p.Trace = append(p.Trace, &Trace{
Argument: branch.Argument,
case ArgumentNode:
arg := branch.Argument
if value, err := arg.Parse(c.scan); err == nil {
c.Path = append(c.Path, &Path{
Parent: node,
Argument: branch,
Value: value,
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)
}
}
if err := checkMissingPositionals(positional, node.Positional); err != nil {
return err
}
if err := checkMissingChildren(node.Children); err != nil {
return err
}
return nil
}
// Apply traced context to the target grammar.
func (p *Context) Apply() (string, error) {
func (c *Context) Apply() (string, error) {
path := []string{}
for _, trace := range p.Trace {
for _, trace := range c.Path {
switch {
case trace.Argument != nil:
path = append(path, "<"+trace.Argument.Name+">")
@@ -254,6 +307,24 @@ func (p *Context) Apply() (string, error) {
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 {
missing := []string{}
for _, flag := range flags {
@@ -269,23 +340,26 @@ func checkMissingFlags(flags []*Flag) error {
return fmt.Errorf("missing flags: %s", strings.Join(missing, ", "))
}
func checkMissingChildren(children []*Branch) error {
func checkMissingChildren(node *Node) error {
missing := []string{}
for _, child := range children {
for _, child := range node.Children {
if child.Argument != nil {
if !child.Argument.Argument.Required {
if !child.Argument.Required {
continue
}
missing = append(missing, "<"+child.Argument.Name+">")
missing = append(missing, strconv.Quote("<"+child.Argument.Name+">"))
} else {
missing = append(missing, child.Command.Name)
missing = append(missing, strconv.Quote(child.Name))
}
}
if len(missing) == 0 {
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.
@@ -306,21 +380,3 @@ func checkMissingPositionals(positional int, values []*Value) error {
}
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
import (
"text/template"
"bytes"
"fmt"
"go/doc"
"io"
"reflect"
"strings"
"github.com/aymerick/raymond"
)
const defaultHelp = `{{- with .Application -}}
usage: {{.Name}}
const (
defaultIndent = 2
defaultTemplate = `
{{#with App}}
usage: {{Name}}
{{.Help}}
{{range .Context.Flags}}
--{{.Name}}
{{end}}
{{#wrap}}
{{Help}}
{{/wrap}}
{{- 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.
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{}{
"Application": app.Model,
"App": ctx.App,
"Context": ctx,
"Path": path,
}
for k, v := range tmplctx {
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 {
return err
}
app.Exit(0)
io.WriteString(ctx.App.Stdout, output)
ctx.App.Exit(0)
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"
"path/filepath"
"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.
type Error struct{ msg string }
@@ -30,53 +29,54 @@ func Must(ast interface{}, options ...Option) *Kong {
// Kong is the main parser type.
type Kong struct {
Model *Application
// Grammar model.
*Application
// Termination function (defaults to os.Exit)
Exit func(int)
Stdout io.Writer
Stderr io.Writer
help *template.Template
helpContext map[string]interface{}
helpFuncs template.FuncMap
hooks map[reflect.Value]HookFunction
noDefaultHelp bool
}
// New creates a new Kong parser into ast.
func New(ast interface{}, options ...Option) (*Kong, error) {
// New creates a new Kong parser on grammar.
//
// See the README (https://github.com/alecthomas/kong) for usage instructions.
func New(grammar interface{}, options ...Option) (*Kong, error) {
k := &Kong{
Exit: os.Exit,
Stdout: os.Stdout,
Stderr: os.Stderr,
help: defaultHelpTemplate,
helpContext: map[string]interface{}{},
helpFuncs: template.FuncMap{},
hooks: map[reflect.Value]HookFunction{},
Exit: os.Exit,
Stdout: os.Stdout,
Stderr: os.Stderr,
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 {
option(k)
}
if !k.noDefaultHelp {
k.integrateHelp()
model, err := build(grammar, k.extraFlags())
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
}
func (k *Kong) integrateHelp() {
// Provide additional builtin flags, if any.
func (k *Kong) extraFlags() []*Flag {
if k.noDefaultHelp {
return nil
}
helpValue := false
help := &Flag{
helpFlag := &Flag{
Value: Value{
Name: "help",
Help: "Show context-sensitive help.",
@@ -85,28 +85,14 @@ func (k *Kong) integrateHelp() {
Decoder: kindDecoders[reflect.Bool],
},
}
k.Model.Flags = append([]*Flag{help}, k.Model.Flags...)
Hook(&helpValue, Help(defaultHelpTemplate, nil))(k)
hook := Hook(&helpValue, Help(defaultHelpTemplate, nil))
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) {
p := &Context{
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
return Trace(k, args)
}
// Parse arguments into target.
@@ -125,11 +111,14 @@ func (k *Kong) Parse(args []string) (command string, err error) {
if ctx.Error != nil {
return "", ctx.Error
}
if err = ctx.Validate(); err != nil {
return "", err
}
return ctx.Apply()
}
func (k *Kong) applyHooks(ctx *Context) error {
for _, trace := range ctx.Trace {
for _, trace := range ctx.Path {
var key reflect.Value
switch {
case trace.App != nil:
@@ -143,13 +132,13 @@ func (k *Kong) applyHooks(ctx *Context) error {
case trace.Flag != nil:
key = trace.Flag.Value.Value
default:
panic("unsupported Trace")
panic("unsupported Path")
}
if key.IsValid() {
key = key.Addr()
}
if hook := k.hooks[key]; hook != nil {
if err := hook(k, ctx, trace); err != nil {
if err := hook(ctx, trace); err != nil {
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.
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.
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.
+8 -11
View File
@@ -9,9 +9,9 @@ import (
func mustNew(t *testing.T, cli interface{}, options ...Option) *Kong {
t.Helper()
options = append(options, ExitFunction(func(int) {
options = append([]Option{ExitFunction(func(int) {
t.Fatalf("unexpected exit()")
}))
})}, options...)
parser, err := New(cli, options...)
require.NoError(t, err)
return parser
@@ -353,10 +353,10 @@ func TestTraceErrorPartiallySucceeds(t *testing.T) {
} `kong:"cmd"`
}
p := mustNew(t, &cli)
trace, err := p.Trace([]string{"one", "bad"})
ctx, err := p.Trace([]string{"one", "bad"})
require.NoError(t, err)
require.Error(t, trace.Error)
require.Equal(t, []string{"one"}, trace.Command())
require.Error(t, ctx.Error)
require.Equal(t, []string{"one"}, ctx.Command())
}
func TestHooks(t *testing.T) {
@@ -382,9 +382,9 @@ func TestHooks(t *testing.T) {
{"Flag", "one --three=three", values{true, "", "three"}},
{"ArgAndFlag", "one two --three=three", values{true, "two", "three"}},
}
setOne := func(app *Kong, ctx *Context, trace *Trace) error { hooked.one = true; return nil }
setTwo := func(app *Kong, ctx *Context, trace *Trace) error { hooked.two = trace.Value.String(); return nil }
setThree := func(app *Kong, ctx *Context, trace *Trace) error { hooked.three = trace.Value.String(); return nil }
setOne := func(ctx *Context, path *Path) error { hooked.one = true; return nil }
setTwo := func(ctx *Context, path *Path) error { hooked.two = path.Value.String(); return nil }
setThree := func(ctx *Context, path *Path) error { hooked.three = path.Value.String(); return nil }
p := mustNew(t, &cli,
Hook(&cli.One, setOne),
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
import "reflect"
import (
"reflect"
"strconv"
"strings"
)
type Application struct {
Node
HelpFlag *Flag
}
// A Branch is a command or positional argument that results in a branch in the command tree.
type Branch struct {
Command *Command
Argument *Argument
// Leaves returns the leaf commands/arguments in the command-line grammar.
func (a *Application) Leaves() (out []*Node) {
var walk func(n *Node)
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 NodeType int
const (
ApplicationNode NodeType = iota
CommandNode
ArgumentNode
)
type Node struct {
Type NodeType
Parent *Node
Name string
Help string
Flags []*Flag
Positional []*Value
Children []*Branch
Target reflect.Value
Positional []*Positional
Children []*Node
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.
@@ -36,6 +79,11 @@ type Value struct {
Required bool
Set bool // Used with Required to test if a value has been given.
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.
@@ -69,14 +117,28 @@ func (v *Value) Reset() error {
type Positional = Value
type Argument struct {
Node
Argument *Value
}
type Flag struct {
Value
Placeholder string
PlaceHolder string
Env string
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 (
"io"
"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)
// 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.
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.
func Description(description string) Option {
return func(k *Kong) { k.Model.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 }
return func(k *Kong) {
if k.Application != nil {
k.Help = description
}
}
}
// 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{}
p, err := New(&cli, Name("name"), Description("description"), Writers(nil, nil), ExitFunction(nil))
require.NoError(t, err)
require.Equal(t, "name", p.Model.Name)
require.Equal(t, "description", p.Model.Help)
require.Equal(t, "name", p.Name)
require.Equal(t, "description", p.Help)
require.Nil(t, p.Stdout)
require.Nil(t, p.Stderr)
require.Nil(t, p.Exit)
+6 -4
View File
@@ -17,9 +17,10 @@ type Tag struct {
Type string
Default string
Format string
Placeholder string
PlaceHolder string
Env string
Short rune
Hidden bool
// Storage for all tag keys for arbitrary lookups.
items map[string]string
@@ -109,10 +110,11 @@ func parseTag(fv reflect.Value, s string) *Tag {
t.Type, _ = t.Get("type")
t.Env, _ = t.Get("env")
t.Short, _ = t.GetRune("short")
t.Hidden = t.Has("hidden")
t.Placeholder, _ = t.Get("placeholder")
if t.Placeholder == "" {
t.Placeholder = strings.ToUpper(dashedString(fv.Type().Name()))
t.PlaceHolder, _ = t.Get("placeholder")
if t.PlaceHolder == "" {
t.PlaceHolder = strings.ToUpper(dashedString(fv.Type().Name()))
}
return t