Skip to content

Commit

Permalink
Cmd can be passthrough now, too.
Browse files Browse the repository at this point in the history
Fixes #253.
  • Loading branch information
mitar authored and alecthomas committed Jan 5, 2022
1 parent 76d5ed9 commit a7d3850
Show file tree
Hide file tree
Showing 6 changed files with 129 additions and 23 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 20 additions & 6 deletions build.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
14 changes: 14 additions & 0 deletions context.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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{}
Expand Down
77 changes: 77 additions & 0 deletions kong_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
29 changes: 15 additions & 14 deletions model.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
}
Expand Down
4 changes: 2 additions & 2 deletions tag.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit a7d3850

Please sign in to comment.