From 3eb5e285ed4be64c12368b769de6dc9b84dd024a Mon Sep 17 00:00:00 2001 From: Gerald Kaszuba Date: Mon, 21 May 2018 23:25:19 +1000 Subject: [PATCH] Use "kong" as tag keys fixes #9 (#10) --- README.md | 12 ++-- _examples/shell/main.go | 24 ++++--- build.go | 68 ++++++++++---------- decoders.go | 11 ++-- global.go | 5 +- kong.go | 24 ++++--- kong_test.go | 122 +++++++++++++++++++++++++---------- tag.go | 136 ++++++++++++++++++++++++++++++++++++++++ 8 files changed, 304 insertions(+), 98 deletions(-) create mode 100644 tag.go diff --git a/README.md b/README.md index 143d108..8e1e931 100644 --- a/README.md +++ b/README.md @@ -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() { diff --git a/_examples/shell/main.go b/_examples/shell/main.go index 352cfec..9a6e7d0 100644 --- a/_examples/shell/main.go +++ b/_examples/shell/main.go @@ -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)) } diff --git a/build.go b/build.go index e7e3a96..93c6520 100644 --- a/build.go +++ b/build.go @@ -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 } diff --git a/decoders.go b/decoders.go index a5428ec..65d61a8 100644 --- a/decoders.go +++ b/decoders.go @@ -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) } diff --git a/global.go b/global.go index 9daaac6..72544ca 100644 --- a/global.go +++ b/global.go @@ -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 } diff --git a/kong.go b/kong.go index f653f06..a40aad2 100644 --- a/kong.go +++ b/kong.go @@ -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) } diff --git a/kong_test.go b/kong_test.go index b9f4f76..385e659 100644 --- a/kong_test.go +++ b/kong_test.go @@ -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) } diff --git a/tag.go b/tag.go new file mode 100644 index 0000000..acdb5c0 --- /dev/null +++ b/tag.go @@ -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 +}