diff --git a/README.md b/README.md index 47bd1cb..2d633f3 100644 --- a/README.md +++ b/README.md @@ -450,6 +450,7 @@ Tag | Description `prefix:"X"` | 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`. `-` | Ignore the field. Useful for adding non-CLI fields to a configuration struct. ## Plugins diff --git a/build.go b/build.go index 0e3bc5f..4d9081a 100644 --- a/build.go +++ b/build.go @@ -195,6 +195,7 @@ func buildField(k *Kong, node *Node, v reflect.Value, ft reflect.StructField, fv Tag: tag, Target: fv, Enum: tag.Enum, + Passthrough: tag.Passthrough, // Flags are optional by default, and args are required by default. Required: (!tag.Arg && tag.Required) || (tag.Arg && !tag.Optional), diff --git a/context.go b/context.go index 554e8b1..d01abfd 100644 --- a/context.go +++ b/context.go @@ -324,6 +324,21 @@ func (c *Context) Reset() error { }) } +func (c *Context) endParsing() { + args := []string{} + for { + token := c.scan.Pop() + if token.Type == EOLToken { + break + } + args = append(args, token.String()) + } + // Note: tokens must be pushed in reverse order. + for i := range args { + c.scan.PushTyped(args[len(args)-1-i], PositionalArgumentToken) + } +} + func (c *Context) trace(node *Node) (err error) { // nolint: gocyclo positional := 0 @@ -349,18 +364,7 @@ func (c *Context) trace(node *Node) (err error) { // nolint: gocyclo // Indicates end of parsing. All remaining arguments are treated as positional arguments only. case v == "--": c.scan.Pop() - args := []string{} - for { - token = c.scan.Pop() - if token.Type == EOLToken { - break - } - args = append(args, token.String()) - } - // Note: tokens must be pushed in reverse order. - for i := range args { - c.scan.PushTyped(args[len(args)-1-i], PositionalArgumentToken) - } + c.endParsing() // Long flag. case strings.HasPrefix(v, "--"): @@ -413,6 +417,11 @@ func (c *Context) trace(node *Node) (err error) { // nolint: gocyclo // Ensure we've consumed all positional arguments. if positional < len(node.Positional) { arg := node.Positional[positional] + + if arg.Passthrough { + c.endParsing() + } + err := arg.Parse(c.scan, c.getValue(arg)) if err != nil { return err diff --git a/mapper_test.go b/mapper_test.go index a8b84e9..4f052f9 100644 --- a/mapper_test.go +++ b/mapper_test.go @@ -207,6 +207,33 @@ func TestSliceConsumesRemainingPositionalArgs(t *testing.T) { require.Equal(t, []string{"ls", "-lart"}, cli.Remainder) } +func TestPassthroughStopsParsing(t *testing.T) { + type cli struct { + Interactive bool `short:"i"` + Image string `arg:""` + Argv []string `arg:"" optional:"" passthrough:""` + } + + var actual cli + p := mustNew(t, &actual) + + _, err := p.Parse([]string{"alpine", "sudo", "-i", "true"}) + require.NoError(t, err) + require.Equal(t, cli{ + Interactive: false, + Image: "alpine", + Argv: []string{"sudo", "-i", "true"}, + }, actual) + + _, err = p.Parse([]string{"alpine", "-i", "sudo", "-i", "true"}) + require.NoError(t, err) + require.Equal(t, cli{ + Interactive: true, + Image: "alpine", + Argv: []string{"sudo", "-i", "true"}, + }, actual) +} + type mappedValue struct { decoded string } diff --git a/model.go b/model.go index d3d1a8f..cb0534a 100644 --- a/model.go +++ b/model.go @@ -236,6 +236,7 @@ type Value struct { Set bool // Set to true when this value is set through some mechanism. Format string // Formatting directive, if applicable. Position int // Position (for positional arguments). + Passthrough bool // Set to true to stop flag parsing when encountered. } // EnumMap returns a map of the enums in this value. diff --git a/tag.go b/tag.go index deffd86..4d640c9 100644 --- a/tag.go +++ b/tag.go @@ -34,6 +34,7 @@ type Tag struct { Embed bool Aliases []string Negatable bool + Passthrough bool // Storage for all tag keys for arbitrary lookups. items map[string][]string @@ -184,6 +185,11 @@ func parseTag(fv reflect.Value, ft reflect.StructField) *Tag { t.PlaceHolder = strings.ToUpper(dashedString(fv.Type().Name())) } t.Enum = t.Get("enum") + passthrough := t.Has("passthrough") + if passthrough && !t.Arg { + fail("passthrough only makes sense for positional arguments") + } + t.Passthrough = passthrough return t }