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 {
|
||||
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)
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
"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
@@ -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) {
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
"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
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user