Use "kong" as tag keys fixes #9 (#10)

This commit is contained in:
Gerald Kaszuba
2018-05-21 23:25:19 +10:00
committed by Alec Thomas
parent 184735e689
commit 3eb5e285ed
8 changed files with 304 additions and 98 deletions
+6 -6
View File
@@ -9,15 +9,15 @@ import "github.com/alecthomas/kong"
var CLI struct {
Rm struct {
Force bool `help:"Force removal."`
Recursive bool `help:"Recursively remove files."`
Force bool `kong:"help='Force removal.'"`
Recursive bool `kong:"help='Recursively remove files.'"`
Paths []string `help:"Paths to remove." type:"path"`
} `help:"Remove files."`
Paths []string `kong:"help='Paths to remove.',type='path'"`
} `kong:"help='Remove files.'"`
Ls struct {
Paths []string `help:"Paths to list." type:"path"`
} `help:"List paths."`
Paths []string `kong:"help='Paths to list.',type='path'"`
} `kong:"help='List paths.'"`
}
func main() {
+16 -8
View File
@@ -1,20 +1,28 @@
package main
import "github.com/alecthomas/kong"
import (
"encoding/json"
"fmt"
"github.com/alecthomas/kong"
)
var CLI struct {
Rm struct {
Force bool `help:"Force removal."`
Recursive bool `help:"Recursively remove files."`
Force bool `kong:"help='Force removal.'"`
Recursive bool `kong:"help='Recursively remove files.'"`
Paths []string `help:"Paths to remove." type:"path"`
} `help:"Remove files."`
Paths []string `kong:"help='Paths to remove.',type='path'"`
} `kong:"help='Remove files.'"`
Ls struct {
Paths []string `help:"Paths to list." type:"path"`
} `help:"List paths."`
Paths []string `kong:"help='Paths to list.',type='path'"`
} `kong:"help='List paths.'"`
}
func main() {
kong.Parse(&CLI)
cmd := kong.Parse(&CLI)
s, _ := json.Marshal(&CLI)
fmt.Println(cmd)
fmt.Println(string(s))
}
+35 -33
View File
@@ -4,7 +4,6 @@ import (
"fmt"
"reflect"
"strings"
"unicode/utf8"
)
func build(ast interface{}) (app *Application, err error) {
@@ -23,14 +22,21 @@ func build(ast interface{}) (app *Application, err error) {
return nil, fmt.Errorf("expected a pointer to a struct but got %T", ast)
}
node := buildNode(iv, true)
node, err := buildNode(iv, true)
if err != nil {
return node, err
}
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 node, nil
}
func buildNode(v reflect.Value, cmd bool) *Node {
func dashedString(s string) string {
return strings.Join(camelCase(s), "-")
}
func buildNode(v reflect.Value, cmd bool) (*Node, error) {
node := &Node{}
for i := 0; i < v.NumField(); i++ {
ft := v.Type().Field(i)
@@ -41,38 +47,34 @@ func buildNode(v reflect.Value, cmd bool) *Node {
name := ft.Tag.Get("name")
if name == "" {
name = strings.ToLower(strings.Join(camelCase(ft.Name), "-"))
name = strings.ToLower(dashedString(ft.Name))
}
decoder := DecoderForField(ft)
help, _ := ft.Tag.Lookup("help")
dflt := ft.Tag.Get("default")
placeholder := ft.Tag.Get("placeholder")
if placeholder == "" {
placeholder = strings.ToUpper(strings.Join(camelCase(fv.Type().Name()), "-"))
tag, err := parseTag(fv, ft.Tag.Get("kong"))
if err != nil {
return nil, err
}
short, _ := utf8.DecodeRuneInString(ft.Tag.Get("short"))
if short == utf8.RuneError {
short = 0
}
// group := ft.Tag.Get("group")
_, required := ft.Tag.Lookup("required")
_, optional := ft.Tag.Lookup("optional")
// Force field to be an argument, not a flag.
_, arg := ft.Tag.Lookup("arg")
decoder := DecoderForField(tag.Type, ft)
if !cmd {
_, cmd = ft.Tag.Lookup("cmd")
cmd = tag.Cmd
}
env := ft.Tag.Get("env")
format := ft.Tag.Get("format")
// Nested structs are either commands or args.
if ft.Type.Kind() == reflect.Struct && (cmd || arg) {
child := buildNode(fv, false)
child.Help = help
if ft.Type.Kind() == reflect.Struct && (cmd || tag.Arg) {
child, err := buildNode(fv, false)
if err != nil {
return nil, err
}
child.Help = tag.Help
// A branching argument. This is a bit hairy, as we let buildNode() do the parsing, then check that
// a positional argument is provided to the child, and move it to the branching argument field.
if arg {
if tag.Arg {
if len(child.Positional) == 0 {
fail("positional branch %s.%s must have at least one child positional argument",
v.Type().Name(), ft.Name)
@@ -104,35 +106,35 @@ func buildNode(v reflect.Value, cmd bool) *Node {
fail("no decoder for %s.%s (of type %s)", v.Type(), ft.Name, ft.Type)
}
flag := !arg
flag := !tag.Arg
value := Value{
Name: name,
Flag: flag,
Help: help,
Default: dflt,
Help: tag.Help,
Default: tag.Default,
Decoder: decoder,
Value: fv,
Field: ft,
// Flags are optional by default, and args are required by default.
Required: (flag && required) || (arg && !optional),
Required: (flag && tag.Required) || (tag.Arg && !tag.Optional),
Format: format,
}
if arg {
if tag.Arg {
node.Positional = append(node.Positional, &value)
} else {
node.Flags = append(node.Flags, &Flag{
Value: value,
Short: short,
Placeholder: placeholder,
Short: tag.Short,
Placeholder: tag.Placeholder,
Env: env,
})
}
}
}
// 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
for _, p := range node.Positional {
if !last && p.Required {
@@ -142,5 +144,5 @@ func buildNode(v reflect.Value, cmd bool) *Node {
last = p.Required
}
return node
return node, nil
}
+4 -7
View File
@@ -70,7 +70,7 @@ var _ KindDecoder = &kindDecoder{}
//
// eg.
//
// Field string `type:"colour"`
// Field string `kong:"type='colour'`
// kong.RegisterDecoder(kong.NewNamedDecoder("colour", ...))
type NamedDecoder interface {
Name() string
@@ -99,12 +99,9 @@ var (
// DecoderForField finds a decoder for a struct field.
//
// Will return nil if a decoder can not be determined.
func DecoderForField(field reflect.StructField) Decoder {
name, ok := field.Tag.Lookup("type")
if ok {
if decoder, ok := namedDecoders[name]; ok {
return decoder
}
func DecoderForField(name string, field reflect.StructField) Decoder {
if decoder, ok := namedDecoders[name]; ok {
return decoder
}
return DecoderForType(field.Type)
}
+3 -2
View File
@@ -3,9 +3,10 @@ package kong
import "os"
// Parse constructs a new parser and parses the default command-line.
func Parse(cli interface{}, options ...Option) {
func Parse(cli interface{}, options ...Option) string {
parser, err := New(cli, options...)
parser.FatalIfErrorf(err)
_, err = parser.Parse(os.Args[1:])
cmd, err := parser.Parse(os.Args[1:])
parser.FatalIfErrorf(err)
return cmd
}
+15 -9
View File
@@ -33,13 +33,7 @@ type Kong struct {
// New creates a new Kong parser into ast.
func New(ast interface{}, options ...Option) (*Kong, error) {
model, err := build(ast)
if err != nil {
return nil, err
}
model.Name = filepath.Base(os.Args[0])
k := &Kong{
Model: model,
terminate: os.Exit,
stdout: os.Stdout,
stderr: os.Stderr,
@@ -47,6 +41,14 @@ func New(ast interface{}, options ...Option) (*Kong, error) {
helpContext: map[string]interface{}{},
helpFuncs: template.FuncMap{},
}
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)
}
@@ -91,7 +93,11 @@ func (k *Kong) reset(node *Node) {
}
func (k *Kong) Errorf(format string, args ...interface{}) {
fmt.Fprintf(os.Stderr, k.Model.Name+": "+format, args...)
if k.Model != nil {
fmt.Fprintf(os.Stderr, k.Model.Name+": "+format, args...)
} else {
fmt.Fprintf(os.Stderr, format, args...)
}
}
func (k *Kong) FatalIfErrorf(err error, args ...interface{}) {
@@ -99,9 +105,9 @@ func (k *Kong) FatalIfErrorf(err error, args ...interface{}) {
return
}
msg := err.Error()
if len(args) == 0 {
if len(args) > 0 {
msg = fmt.Sprintf(args[0].(string), args...) + ": " + err.Error()
}
k.Errorf("%s", msg)
k.Errorf("%s\n", msg)
k.terminate(1)
}
+89 -33
View File
@@ -17,11 +17,11 @@ func TestPositionalArguments(t *testing.T) {
var cli struct {
User struct {
Create struct {
ID int `arg:""`
First string `arg:""`
Last string `arg:""`
} `cmd:""`
} `cmd:""`
ID int `kong:"arg"`
First string `kong:"arg"`
Last string `kong:"arg"`
} `kong:"cmd"`
} `kong:"cmd"`
}
p := mustNew(t, &cli)
cmd, err := p.Parse([]string{"user", "create", "10", "Alec", "Thomas"})
@@ -43,21 +43,21 @@ func TestBranchingArgument(t *testing.T) {
var cli struct {
User struct {
Create struct {
ID string `arg:""`
First string `arg:""`
Last string `arg:""`
} `cmd:""`
ID string `kong:"arg"`
First string `kong:"arg"`
Last string `kong:"arg"`
} `kong:"cmd"`
// Branching argument.
ID struct {
ID int `arg:""`
ID int `kong:"arg"`
Flag int
Delete struct{} `cmd:""`
Delete struct{} `kong:"cmd"`
Rename struct {
To string
} `cmd:""`
} `arg:""`
} `cmd:"" help:"User management."`
} `kong:"cmd"`
} `kong:"arg"`
} `kong:"cmd,help='User management.'"`
}
p := mustNew(t, &cli)
cmd, err := p.Parse([]string{"user", "10", "delete"})
@@ -73,7 +73,7 @@ func TestBranchingArgument(t *testing.T) {
func TestResetWithDefaults(t *testing.T) {
var cli struct {
Flag string
FlagWithDefault string `default:"default" `
FlagWithDefault string `kong:"default='default'"`
}
cli.Flag = "BLAH"
cli.FlagWithDefault = "BLAH"
@@ -96,7 +96,7 @@ func TestFlagSlice(t *testing.T) {
func TestArgSlice(t *testing.T) {
var cli struct {
Slice []int `arg:""`
Slice []int `kong:"arg"`
Flag bool
}
parser := mustNew(t, &cli)
@@ -117,8 +117,8 @@ func TestUnsupportedFieldErrors(t *testing.T) {
func TestMatchingArgField(t *testing.T) {
var cli struct {
ID struct {
NotID int `arg:""`
} `arg:""`
NotID int `kong:"arg"`
} `kong:"arg"`
}
_, err := New(&cli)
@@ -127,9 +127,9 @@ func TestMatchingArgField(t *testing.T) {
func TestCantMixPositionalAndBranches(t *testing.T) {
var cli struct {
Arg string `arg:""`
Arg string `kong:"arg"`
Command struct {
} `cmd:""`
} `kong:"cmd"`
}
_, err := New(&cli)
require.Error(t, err)
@@ -140,8 +140,8 @@ func TestPropagatedFlags(t *testing.T) {
Flag1 string
Command1 struct {
Flag2 bool
Command2 struct{} `cmd:""`
} `cmd:""`
Command2 struct{} `kong:"cmd"`
} `kong:"cmd"`
}
parser := mustNew(t, &cli)
@@ -153,7 +153,7 @@ func TestPropagatedFlags(t *testing.T) {
func TestRequiredFlag(t *testing.T) {
var cli struct {
Flag string `required:""`
Flag string `kong:"required"`
}
parser := mustNew(t, &cli)
@@ -163,7 +163,7 @@ func TestRequiredFlag(t *testing.T) {
func TestOptionalArg(t *testing.T) {
var cli struct {
Arg string `arg:"" optional:""`
Arg string `kong:"arg,optional"`
}
parser := mustNew(t, &cli)
@@ -173,7 +173,7 @@ func TestOptionalArg(t *testing.T) {
func TestRequiredArg(t *testing.T) {
var cli struct {
Arg string `arg:""`
Arg string `kong:"arg"`
}
parser := mustNew(t, &cli)
@@ -183,8 +183,8 @@ func TestRequiredArg(t *testing.T) {
func TestInvalidRequiredAfterOptional(t *testing.T) {
var cli struct {
ID int `arg:"" optional:""`
Name string `arg:""`
ID int `kong:"arg,optional"`
Name string `kong:"arg"`
}
_, err := New(&cli)
@@ -194,9 +194,9 @@ func TestInvalidRequiredAfterOptional(t *testing.T) {
func TestOptionalStructArg(t *testing.T) {
var cli struct {
Name struct {
Name string `arg:"" optional:""`
Name string `kong:"arg,optional"`
Enabled bool
} `arg:"" optional:""`
} `kong:"arg,optional"`
}
parser := mustNew(t, &cli)
@@ -222,8 +222,8 @@ func TestOptionalStructArg(t *testing.T) {
func TestMixedRequiredArgs(t *testing.T) {
var cli struct {
Name string `arg:""`
ID int `arg:"" optional:""`
Name string `kong:"arg"`
ID int `kong:"arg,optional"`
}
parser := mustNew(t, &cli)
@@ -244,10 +244,66 @@ func TestMixedRequiredArgs(t *testing.T) {
func TestDefaultValueForOptionalArg(t *testing.T) {
var cli struct {
Arg string `arg:"" optional:"" default:"default"`
Arg string `kong:"arg,optional,default='👌'"`
}
p := mustNew(t, &cli)
_, err := p.Parse(nil)
require.NoError(t, err)
require.Equal(t, "default", cli.Arg)
require.Equal(t, "👌", cli.Arg)
}
func TestNoValueInTag(t *testing.T) {
var cli struct {
Empty1 string `kong:"default"`
Empty2 string `kong:"default="`
}
p := mustNew(t, &cli)
_, err := p.Parse(nil)
require.NoError(t, err)
require.Equal(t, "", cli.Empty1)
require.Equal(t, "", cli.Empty2)
}
func TestCommaInQuotes(t *testing.T) {
var cli struct {
Numbers string `kong:"default='1,2'"`
}
p := mustNew(t, &cli)
_, err := p.Parse(nil)
require.NoError(t, err)
require.Equal(t, "1,2", cli.Numbers)
}
func TestUnknownKey(t *testing.T) {
var cli struct {
Numbers string `kong:"gak='hi'"`
}
_, err := New(&cli)
require.Error(t, err)
}
func TestBadString(t *testing.T) {
var cli struct {
Numbers string `kong:"default='yay'n"`
}
_, err := New(&cli)
require.Error(t, err)
}
func TestNoQuoteEnd(t *testing.T) {
var cli struct {
Numbers string `kong:"default='yay"`
}
_, err := New(&cli)
require.Error(t, err)
}
func TestEscapedQuote(t *testing.T) {
var cli struct {
DoYouKnow string `kong:"default='i don\\'t know'"`
}
p := mustNew(t, &cli)
_, err := p.Parse(nil)
require.NoError(t, err)
require.Equal(t, "i don't know", cli.DoYouKnow)
}
+136
View File
@@ -0,0 +1,136 @@
package kong
import (
"fmt"
"reflect"
"strings"
"unicode/utf8"
)
type Tag struct {
Cmd bool
Arg bool
Required bool
Optional bool
Help string
Type string
Default string
Format string
Placeholder string
Env string
Short rune
}
func parseCSV(s string) ([]string, error) {
num := 0
parts := []string{}
current := []rune{}
add := func() {
parts = append(parts, string(current))
current = []rune{}
num++
}
quotes := false
runes := []rune(s)
for idx := 0; idx < len(runes); idx++ {
r := runes[idx]
next := rune(0)
eof := false
if idx < len(runes)-1 {
next = runes[idx+1]
} else {
eof = true
}
if !quotes && r == ',' {
add()
continue
}
if r == '\\' {
if next == '\'' {
idx++
r = '\''
}
} else if r == '\'' {
if quotes {
quotes = false
if next == ',' || eof {
continue
}
return parts, fmt.Errorf("%v has an unexpected char at pos %v", s, idx)
} else {
quotes = true
continue
}
}
current = append(current, r)
}
if quotes {
return parts, fmt.Errorf("%v is not quoted properly", s)
}
add()
return parts, nil
}
func parseTag(fv reflect.Value, s string) (*Tag, error) {
t := &Tag{}
if s == "" {
return t, nil
}
parts, err := parseCSV(s)
if err != nil {
return t, err
}
for _, part := range parts {
is := func(m string) bool { return part == m }
value := func(m string) (string, bool) {
split := strings.SplitN(part, "=", 2)
if split[0] != m {
return "", false
}
if len(split) == 1 {
return "", true
}
return split[1], true
}
if is("cmd") {
t.Cmd = true
} else if is("arg") {
t.Arg = true
} else if is("required") {
t.Required = true
} else if is("optional") {
t.Optional = true
} else if v, ok := value("default"); ok {
t.Default = v
} else if v, ok := value("help"); ok {
t.Help = v
} else if v, ok := value("type"); ok {
t.Type = v
} else if v, ok := value("placeholder"); ok {
t.Placeholder = v
} else if v, ok := value("env"); ok {
t.Env = v
} else if v, ok := value("rune"); ok {
t.Short, _ = utf8.DecodeRuneInString(v)
if t.Short == utf8.RuneError {
t.Short = 0
}
} else {
return t, fmt.Errorf("%v is an unknown kong key", part)
}
}
if t.Placeholder == "" {
t.Placeholder = strings.ToUpper(dashedString(fv.Type().Name()))
}
return t, nil
}