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 { var CLI struct {
Rm struct { Rm struct {
Force bool `help:"Force removal."` Force bool `kong:"help='Force removal.'"`
Recursive bool `help:"Recursively remove files."` Recursive bool `kong:"help='Recursively remove files.'"`
Paths []string `help:"Paths to remove." type:"path"` Paths []string `kong:"help='Paths to remove.',type='path'"`
} `help:"Remove files."` } `kong:"help='Remove files.'"`
Ls struct { Ls struct {
Paths []string `help:"Paths to list." type:"path"` Paths []string `kong:"help='Paths to list.',type='path'"`
} `help:"List paths."` } `kong:"help='List paths.'"`
} }
func main() { func main() {
+16 -8
View File
@@ -1,20 +1,28 @@
package main package main
import "github.com/alecthomas/kong" import (
"encoding/json"
"fmt"
"github.com/alecthomas/kong"
)
var CLI struct { var CLI struct {
Rm struct { Rm struct {
Force bool `help:"Force removal."` Force bool `kong:"help='Force removal.'"`
Recursive bool `help:"Recursively remove files."` Recursive bool `kong:"help='Recursively remove files.'"`
Paths []string `help:"Paths to remove." type:"path"` Paths []string `kong:"help='Paths to remove.',type='path'"`
} `help:"Remove files."` } `kong:"help='Remove files.'"`
Ls struct { Ls struct {
Paths []string `help:"Paths to list." type:"path"` Paths []string `kong:"help='Paths to list.',type='path'"`
} `help:"List paths."` } `kong:"help='List paths.'"`
} }
func main() { 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" "fmt"
"reflect" "reflect"
"strings" "strings"
"unicode/utf8"
) )
func build(ast interface{}) (app *Application, err error) { 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) 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 { 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)
} }
return node, nil 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{} node := &Node{}
for i := 0; i < v.NumField(); i++ { for i := 0; i < v.NumField(); i++ {
ft := v.Type().Field(i) ft := v.Type().Field(i)
@@ -41,38 +47,34 @@ func buildNode(v reflect.Value, cmd bool) *Node {
name := ft.Tag.Get("name") name := ft.Tag.Get("name")
if 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") tag, err := parseTag(fv, ft.Tag.Get("kong"))
dflt := ft.Tag.Get("default") if err != nil {
placeholder := ft.Tag.Get("placeholder") return nil, err
if placeholder == "" {
placeholder = strings.ToUpper(strings.Join(camelCase(fv.Type().Name()), "-"))
} }
short, _ := utf8.DecodeRuneInString(ft.Tag.Get("short"))
if short == utf8.RuneError { decoder := DecoderForField(tag.Type, ft)
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")
if !cmd { if !cmd {
_, cmd = ft.Tag.Lookup("cmd") cmd = tag.Cmd
} }
env := ft.Tag.Get("env") env := ft.Tag.Get("env")
format := ft.Tag.Get("format") format := ft.Tag.Get("format")
// Nested structs are either commands or args. // Nested structs are either commands or args.
if ft.Type.Kind() == reflect.Struct && (cmd || arg) { if ft.Type.Kind() == reflect.Struct && (cmd || tag.Arg) {
child := buildNode(fv, false) child, err := buildNode(fv, false)
child.Help = help 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 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. // 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 { if len(child.Positional) == 0 {
fail("positional branch %s.%s must have at least one child positional argument", fail("positional branch %s.%s must have at least one child positional argument",
v.Type().Name(), ft.Name) 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) fail("no decoder for %s.%s (of type %s)", v.Type(), ft.Name, ft.Type)
} }
flag := !arg flag := !tag.Arg
value := Value{ value := Value{
Name: name, Name: name,
Flag: flag, Flag: flag,
Help: help, Help: tag.Help,
Default: dflt, Default: tag.Default,
Decoder: decoder, Decoder: decoder,
Value: fv, Value: fv,
Field: ft, Field: ft,
// Flags are optional by default, and args are required by default. // 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, Format: format,
} }
if arg { if tag.Arg {
node.Positional = append(node.Positional, &value) node.Positional = append(node.Positional, &value)
} else { } else {
node.Flags = append(node.Flags, &Flag{ node.Flags = append(node.Flags, &Flag{
Value: value, Value: value,
Short: short, Short: tag.Short,
Placeholder: placeholder, Placeholder: tag.Placeholder,
Env: env, 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 last := true
for _, p := range node.Positional { for _, p := range node.Positional {
if !last && p.Required { if !last && p.Required {
@@ -142,5 +144,5 @@ func buildNode(v reflect.Value, cmd bool) *Node {
last = p.Required last = p.Required
} }
return node return node, nil
} }
+4 -7
View File
@@ -70,7 +70,7 @@ var _ KindDecoder = &kindDecoder{}
// //
// eg. // eg.
// //
// Field string `type:"colour"` // Field string `kong:"type='colour'`
// kong.RegisterDecoder(kong.NewNamedDecoder("colour", ...)) // kong.RegisterDecoder(kong.NewNamedDecoder("colour", ...))
type NamedDecoder interface { type NamedDecoder interface {
Name() string Name() string
@@ -99,12 +99,9 @@ var (
// DecoderForField finds a decoder for a struct field. // DecoderForField finds a decoder for a struct field.
// //
// Will return nil if a decoder can not be determined. // Will return nil if a decoder can not be determined.
func DecoderForField(field reflect.StructField) Decoder { func DecoderForField(name string, field reflect.StructField) Decoder {
name, ok := field.Tag.Lookup("type") if decoder, ok := namedDecoders[name]; ok {
if ok { return decoder
if decoder, ok := namedDecoders[name]; ok {
return decoder
}
} }
return DecoderForType(field.Type) return DecoderForType(field.Type)
} }
+3 -2
View File
@@ -3,9 +3,10 @@ package kong
import "os" import "os"
// Parse constructs a new parser and parses the default command-line. // 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, err := New(cli, options...)
parser.FatalIfErrorf(err) parser.FatalIfErrorf(err)
_, err = parser.Parse(os.Args[1:]) cmd, err := parser.Parse(os.Args[1:])
parser.FatalIfErrorf(err) parser.FatalIfErrorf(err)
return cmd
} }
+15 -9
View File
@@ -33,13 +33,7 @@ type Kong struct {
// New creates a new Kong parser into ast. // New creates a new Kong parser into ast.
func New(ast interface{}, options ...Option) (*Kong, error) { 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{ k := &Kong{
Model: model,
terminate: os.Exit, terminate: os.Exit,
stdout: os.Stdout, stdout: os.Stdout,
stderr: os.Stderr, stderr: os.Stderr,
@@ -47,6 +41,14 @@ func New(ast interface{}, options ...Option) (*Kong, error) {
helpContext: map[string]interface{}{}, helpContext: map[string]interface{}{},
helpFuncs: template.FuncMap{}, 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 { for _, option := range options {
option(k) option(k)
} }
@@ -91,7 +93,11 @@ func (k *Kong) reset(node *Node) {
} }
func (k *Kong) Errorf(format string, args ...interface{}) { 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{}) { func (k *Kong) FatalIfErrorf(err error, args ...interface{}) {
@@ -99,9 +105,9 @@ func (k *Kong) FatalIfErrorf(err error, args ...interface{}) {
return return
} }
msg := err.Error() msg := err.Error()
if len(args) == 0 { if len(args) > 0 {
msg = fmt.Sprintf(args[0].(string), args...) + ": " + err.Error() msg = fmt.Sprintf(args[0].(string), args...) + ": " + err.Error()
} }
k.Errorf("%s", msg) k.Errorf("%s\n", msg)
k.terminate(1) k.terminate(1)
} }
+89 -33
View File
@@ -17,11 +17,11 @@ func TestPositionalArguments(t *testing.T) {
var cli struct { var cli struct {
User struct { User struct {
Create struct { Create struct {
ID int `arg:""` ID int `kong:"arg"`
First string `arg:""` First string `kong:"arg"`
Last string `arg:""` Last string `kong:"arg"`
} `cmd:""` } `kong:"cmd"`
} `cmd:""` } `kong:"cmd"`
} }
p := mustNew(t, &cli) p := mustNew(t, &cli)
cmd, err := p.Parse([]string{"user", "create", "10", "Alec", "Thomas"}) cmd, err := p.Parse([]string{"user", "create", "10", "Alec", "Thomas"})
@@ -43,21 +43,21 @@ func TestBranchingArgument(t *testing.T) {
var cli struct { var cli struct {
User struct { User struct {
Create struct { Create struct {
ID string `arg:""` ID string `kong:"arg"`
First string `arg:""` First string `kong:"arg"`
Last string `arg:""` Last string `kong:"arg"`
} `cmd:""` } `kong:"cmd"`
// Branching argument. // Branching argument.
ID struct { ID struct {
ID int `arg:""` ID int `kong:"arg"`
Flag int Flag int
Delete struct{} `cmd:""` Delete struct{} `kong:"cmd"`
Rename struct { Rename struct {
To string To string
} `cmd:""` } `kong:"cmd"`
} `arg:""` } `kong:"arg"`
} `cmd:"" help:"User management."` } `kong:"cmd,help='User management.'"`
} }
p := mustNew(t, &cli) p := mustNew(t, &cli)
cmd, err := p.Parse([]string{"user", "10", "delete"}) cmd, err := p.Parse([]string{"user", "10", "delete"})
@@ -73,7 +73,7 @@ func TestBranchingArgument(t *testing.T) {
func TestResetWithDefaults(t *testing.T) { func TestResetWithDefaults(t *testing.T) {
var cli struct { var cli struct {
Flag string Flag string
FlagWithDefault string `default:"default" ` FlagWithDefault string `kong:"default='default'"`
} }
cli.Flag = "BLAH" cli.Flag = "BLAH"
cli.FlagWithDefault = "BLAH" cli.FlagWithDefault = "BLAH"
@@ -96,7 +96,7 @@ func TestFlagSlice(t *testing.T) {
func TestArgSlice(t *testing.T) { func TestArgSlice(t *testing.T) {
var cli struct { var cli struct {
Slice []int `arg:""` Slice []int `kong:"arg"`
Flag bool Flag bool
} }
parser := mustNew(t, &cli) parser := mustNew(t, &cli)
@@ -117,8 +117,8 @@ func TestUnsupportedFieldErrors(t *testing.T) {
func TestMatchingArgField(t *testing.T) { func TestMatchingArgField(t *testing.T) {
var cli struct { var cli struct {
ID struct { ID struct {
NotID int `arg:""` NotID int `kong:"arg"`
} `arg:""` } `kong:"arg"`
} }
_, err := New(&cli) _, err := New(&cli)
@@ -127,9 +127,9 @@ func TestMatchingArgField(t *testing.T) {
func TestCantMixPositionalAndBranches(t *testing.T) { func TestCantMixPositionalAndBranches(t *testing.T) {
var cli struct { var cli struct {
Arg string `arg:""` Arg string `kong:"arg"`
Command struct { Command struct {
} `cmd:""` } `kong:"cmd"`
} }
_, err := New(&cli) _, err := New(&cli)
require.Error(t, err) require.Error(t, err)
@@ -140,8 +140,8 @@ func TestPropagatedFlags(t *testing.T) {
Flag1 string Flag1 string
Command1 struct { Command1 struct {
Flag2 bool Flag2 bool
Command2 struct{} `cmd:""` Command2 struct{} `kong:"cmd"`
} `cmd:""` } `kong:"cmd"`
} }
parser := mustNew(t, &cli) parser := mustNew(t, &cli)
@@ -153,7 +153,7 @@ func TestPropagatedFlags(t *testing.T) {
func TestRequiredFlag(t *testing.T) { func TestRequiredFlag(t *testing.T) {
var cli struct { var cli struct {
Flag string `required:""` Flag string `kong:"required"`
} }
parser := mustNew(t, &cli) parser := mustNew(t, &cli)
@@ -163,7 +163,7 @@ func TestRequiredFlag(t *testing.T) {
func TestOptionalArg(t *testing.T) { func TestOptionalArg(t *testing.T) {
var cli struct { var cli struct {
Arg string `arg:"" optional:""` Arg string `kong:"arg,optional"`
} }
parser := mustNew(t, &cli) parser := mustNew(t, &cli)
@@ -173,7 +173,7 @@ func TestOptionalArg(t *testing.T) {
func TestRequiredArg(t *testing.T) { func TestRequiredArg(t *testing.T) {
var cli struct { var cli struct {
Arg string `arg:""` Arg string `kong:"arg"`
} }
parser := mustNew(t, &cli) parser := mustNew(t, &cli)
@@ -183,8 +183,8 @@ func TestRequiredArg(t *testing.T) {
func TestInvalidRequiredAfterOptional(t *testing.T) { func TestInvalidRequiredAfterOptional(t *testing.T) {
var cli struct { var cli struct {
ID int `arg:"" optional:""` ID int `kong:"arg,optional"`
Name string `arg:""` Name string `kong:"arg"`
} }
_, err := New(&cli) _, err := New(&cli)
@@ -194,9 +194,9 @@ func TestInvalidRequiredAfterOptional(t *testing.T) {
func TestOptionalStructArg(t *testing.T) { func TestOptionalStructArg(t *testing.T) {
var cli struct { var cli struct {
Name struct { Name struct {
Name string `arg:"" optional:""` Name string `kong:"arg,optional"`
Enabled bool Enabled bool
} `arg:"" optional:""` } `kong:"arg,optional"`
} }
parser := mustNew(t, &cli) parser := mustNew(t, &cli)
@@ -222,8 +222,8 @@ func TestOptionalStructArg(t *testing.T) {
func TestMixedRequiredArgs(t *testing.T) { func TestMixedRequiredArgs(t *testing.T) {
var cli struct { var cli struct {
Name string `arg:""` Name string `kong:"arg"`
ID int `arg:"" optional:""` ID int `kong:"arg,optional"`
} }
parser := mustNew(t, &cli) parser := mustNew(t, &cli)
@@ -244,10 +244,66 @@ func TestMixedRequiredArgs(t *testing.T) {
func TestDefaultValueForOptionalArg(t *testing.T) { func TestDefaultValueForOptionalArg(t *testing.T) {
var cli struct { var cli struct {
Arg string `arg:"" optional:"" default:"default"` Arg string `kong:"arg,optional,default='👌'"`
} }
p := mustNew(t, &cli) p := mustNew(t, &cli)
_, err := p.Parse(nil) _, err := p.Parse(nil)
require.NoError(t, err) 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
}