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:
committed by
Gerald Kaszuba
parent
afbb431641
commit
fdc7230e22
@@ -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)
|
||||||
|
|||||||
@@ -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
@@ -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)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
// +build appengine !linux,!freebsd,!darwin,!dragonfly,!netbsd,!openbsd
|
||||||
|
|
||||||
|
package kong
|
||||||
|
|
||||||
|
import "io"
|
||||||
|
|
||||||
|
func guessWidth(w io.Writer) int {
|
||||||
|
return 80
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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())
|
||||||
|
}
|
||||||
@@ -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
@@ -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) {
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user