Repository: alecthomas/kong (go)

Issue:
Unable to fully capture the rest of arguments and pass it through to another command
So I am trying to implement `exec` pattern, where I have a subtool I want to execute and pass all other arguments to it. Trying `passthrough:""` does not fully work with `--help` flag. I have something like:

```
type Commands struct {
	Exec ExecCommand `cmd:"" help:"Run the sub-tool.`
}

type ExecCommand struct {
        Arg []string `arg:"" optional:"" passthrough:"" help:"Arguments passed on to the sub-tool."`
}
```

Inside `ExecCommand`'s `Run` I do: `subtool.Main(append([]string{"subtool"}, c.Arg...))` to pass all args to the `subtool`'s CLI parsing.

And this does not work. So if I do:

```
tool exec foobar
```

it works well, and sub-tool gets `foobar` as an argument.

But:

```
tool exec --help
```

returns help of `tool` and not of the sub-tool.

A workaround is:

```
tool exec -- --help
```

which does work. But how can I make it work without `--`? I think this might be related to https://github.com/alecthomas/kong/discussions/216 and in general that I would prefer that global flags do not get inherited by the subcommands. So in general `tool --help subcommand` should work and `tool subcommand --help` should not, unless subcommand explicitly defines `--help` flag.

I tried to move `passthrough:""` to `Exec` itself, but I got panic `passthrough only makes sense for positional arguments`.

Pull request:
Cmd can be passthrough now, too
Fixes #253.

Patch:
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/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


Test patch:
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, "<anonymous struct>.Command: passthrough command command [<args> ...] 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, "<anonymous struct>.Command: passthrough command command [<args> ...] must contain exactly one positional argument of []string type")
+}