tag: add passthrough for positional arguments
This new tag tells the parser to stop processing flags after the positional argument is encountered. This is particularly useful for subcommands that forward their arguments to an external program, and makes it possible to implement commands like `kubectl exec` or `docker run`. Fixes #80.
This commit is contained in:
committed by
Alec Thomas
parent
49417fe966
commit
d4dd709445
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
+21
-12
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user