From a7d3850e8012bb9550b1e8d1ef88391ba0507814 Mon Sep 17 00:00:00 2001 From: Mitar Date: Tue, 4 Jan 2022 21:58:56 +0100 Subject: [PATCH] Cmd can be passthrough now, too. Fixes #253. --- README.md | 2 +- build.go | 26 ++++++++++++++---- context.go | 14 ++++++++++ kong_test.go | 77 ++++++++++++++++++++++++++++++++++++++++++++++++++++ model.go | 29 ++++++++++---------- tag.go | 4 +-- 6 files changed, 129 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index de49d9b..e15ef2a 100644 --- a/README.md +++ b/README.md @@ -474,7 +474,7 @@ Tag | Description `envprefix:"X"` | Envar prefix for all sub-flags. `set:"K=V"` | Set a variable for expansion by child elements. Multiples can occur. `embed:""` | If present, this field's children will be embedded in the parent. Useful for composition. -`passthrough:""` | If present, this positional argument stops flag parsing when encountered, as if `--` was processed before. Useful for external command wrappers, like `exec`. +`passthrough:""` | If present on a positional argument, it stops flag parsing when encountered, as if `--` was processed before. Useful for external command wrappers, like `exec`. On a command it requires that the command contains only one argument of type `[]string` which is then filled with everything following the command, unparsed. `-` | Ignore the field. Useful for adding non-CLI fields to a configuration struct. e.g `` `kong:"-"` `` ## Plugins diff --git a/build.go b/build.go index 4d0e2fa..4be5f24 100644 --- a/build.go +++ b/build.go @@ -211,14 +211,28 @@ func buildChild(k *Kong, node *Node, typ NodeType, v reflect.Value, ft reflect.S if child.Help == "" { child.Help = child.Argument.Help } - } else if tag.HasDefault { - if node.DefaultCmd != nil { - return failField(v, ft, "can't have more than one default command under %s", node.Summary()) + } else { + if tag.HasDefault { + if node.DefaultCmd != nil { + return failField(v, ft, "can't have more than one default command under %s", node.Summary()) + } + if tag.Default != "withargs" && (len(child.Children) > 0 || len(child.Positional) > 0) { + return failField(v, ft, "default command %s must not have subcommands or arguments", child.Summary()) + } + node.DefaultCmd = child } - if tag.Default != "withargs" && (len(child.Children) > 0 || len(child.Positional) > 0) { - return failField(v, ft, "default command %s must not have subcommands or arguments", child.Summary()) + if tag.Passthrough { + if len(child.Children) > 0 || len(child.Flags) > 0 { + return failField(v, ft, "passthrough command %s must not have subcommands or flags", child.Summary()) + } + if len(child.Positional) != 1 { + return failField(v, ft, "passthrough command %s must contain exactly one positional argument", child.Summary()) + } + if !checkPassthroughArg(child.Positional[0].Target) { + return failField(v, ft, "passthrough command %s must contain exactly one positional argument of []string type", child.Summary()) + } + child.Passthrough = true } - node.DefaultCmd = child } node.Children = append(node.Children, child) diff --git a/context.go b/context.go index 6ad4dd0..de4301a 100644 --- a/context.go +++ b/context.go @@ -361,6 +361,10 @@ func (c *Context) trace(node *Node) (err error) { // nolint: gocyclo flags = append(flags, group...) } + if node.Passthrough { + c.endParsing() + } + for !c.scan.Peek().IsEOL() { token := c.scan.Peek() switch token.Type { @@ -901,6 +905,16 @@ func checkEnum(value *Value, target reflect.Value) error { } } +func checkPassthroughArg(target reflect.Value) bool { + typ := target.Type() + switch typ.Kind() { + case reflect.Slice: + return typ.Elem().Kind() == reflect.String + default: + return false + } +} + func checkXorDuplicates(paths []*Path) error { for _, path := range paths { seen := map[string]*Flag{} diff --git a/kong_test.go b/kong_test.go index 0594565..482775d 100644 --- a/kong_test.go +++ b/kong_test.go @@ -1452,3 +1452,80 @@ func TestEnumValidation(t *testing.T) { }) } } + +func TestPassthroughCmd(t *testing.T) { + tests := []struct { + name string + args []string + flag string + cmdArgs []string + }{ + { + "Simple", + []string{"--flag", "foobar", "command", "something"}, + "foobar", + []string{"something"}, + }, + { + "DashDash", + []string{"--flag", "foobar", "command", "--", "something"}, + "foobar", + []string{"--", "something"}, + }, + { + "Flag", + []string{"command", "--flag", "foobar"}, + "", + []string{"--flag", "foobar"}, + }, + { + "FlagAndFlag", + []string{"--flag", "foobar", "command", "--flag", "foobar"}, + "foobar", + []string{"--flag", "foobar"}, + }, + { + "NoArgs", + []string{"--flag", "foobar", "command"}, + "foobar", + []string(nil), + }, + } + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + var cli struct { + Flag string + Command struct { + Args []string `arg:"" optional:""` + } `cmd:"" passthrough:""` + } + p := mustNew(t, &cli) + _, err := p.Parse(test.args) + require.NoError(t, err) + require.Equal(t, test.flag, cli.Flag) + require.Equal(t, test.cmdArgs, cli.Command.Args) + }) + } +} + +func TestPassthroughCmdOnlyArgs(t *testing.T) { + var cli struct { + Command struct { + Flag string + Args []string `arg:"" optional:""` + } `cmd:"" passthrough:""` + } + _, err := kong.New(&cli) + require.EqualError(t, err, ".Command: passthrough command command [ ...] must not have subcommands or flags") +} + +func TestPassthroughCmdOnlyStringArgs(t *testing.T) { + var cli struct { + Command struct { + Args []int `arg:"" optional:""` + } `cmd:"" passthrough:""` + } + _, err := kong.New(&cli) + require.EqualError(t, err, ".Command: passthrough command command [ ...] must contain exactly one positional argument of []string type") +} diff --git a/model.go b/model.go index a86b510..59c272a 100644 --- a/model.go +++ b/model.go @@ -41,20 +41,21 @@ const ( // Node is a branch in the CLI. ie. a command or positional argument. type Node struct { - Type NodeType - Parent *Node - Name string - Help string // Short help displayed in summaries. - Detail string // Detailed help displayed when describing command/arg alone. - Group *Group - Hidden bool - Flags []*Flag - Positional []*Positional - Children []*Node - DefaultCmd *Node - Target reflect.Value // Pointer to the value in the grammar that this Node is associated with. - Tag *Tag - Aliases []string + Type NodeType + Parent *Node + Name string + Help string // Short help displayed in summaries. + Detail string // Detailed help displayed when describing command/arg alone. + Group *Group + Hidden bool + Flags []*Flag + Positional []*Positional + Children []*Node + DefaultCmd *Node + Target reflect.Value // Pointer to the value in the grammar that this Node is associated with. + Tag *Tag + Aliases []string + Passthrough bool // Set to true to stop flag parsing when encountered. Argument *Value // Populated when Type is ArgumentNode. } diff --git a/tag.go b/tag.go index e4fae67..8e159dd 100644 --- a/tag.go +++ b/tag.go @@ -235,8 +235,8 @@ func hydrateTag(t *Tag, typ reflect.Type) error { // nolint: gocyclo return fmt.Errorf("enum value is only valid if it is either required or has a valid default value") } passthrough := t.Has("passthrough") - if passthrough && !t.Arg { - return fmt.Errorf("passthrough only makes sense for positional arguments") + if passthrough && !t.Arg && !t.Cmd { + return fmt.Errorf("passthrough only makes sense for positional arguments or commands") } t.Passthrough = passthrough return nil