feat: add old "passthrough" behaviour back in as an option

`passthrough:""` or `passthrough:"all"` (the default) will pass through
all further arguments including unrecognised flags.

`passthrough:"partial"` will validate flags up until the `--` or the
first positional argument, then pass through all subsequent flags and
arguments.
This commit is contained in:
Alec Thomas
2024-12-01 19:58:24 +11:00
parent 88e13d750a
commit 96647c30af
6 changed files with 117 additions and 60 deletions
+5 -1
View File
@@ -590,9 +590,13 @@ Both can coexist with standard Tag parsing.
| `envprefix:"X"` | Envar prefix for all sub-flags. | | `envprefix:"X"` | Envar prefix for all sub-flags. |
| `set:"K=V"` | Set a variable for expansion by child elements. Multiples can occur. | | `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. | | `embed:""` | If present, this field's children will be embedded in the parent. Useful for composition. |
| `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. | | `passthrough:"<mode>"`[^1] | 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:"-"` `` | | `-` | Ignore the field. Useful for adding non-CLI fields to a configuration struct. e.g `` `kong:"-"` `` |
[^1]: `<mode>` can be `partial` or `all` (the default). `all` will pass through all arguments including flags, including
flags. `partial` will validate flags until the first positional argument is encountered, then pass through all remaining
positional arguments.
## Plugins ## Plugins
Kong CLI's can be extended by embedding the `kong.Plugin` type and populating it with pointers to Kong annotated structs. For example: Kong CLI's can be extended by embedding the `kong.Plugin` type and populating it with pointers to Kong annotated structs. For example:
+12 -11
View File
@@ -281,17 +281,18 @@ func buildField(k *Kong, node *Node, v reflect.Value, ft reflect.StructField, fv
} }
value := &Value{ value := &Value{
Name: name, Name: name,
Help: tag.Help, Help: tag.Help,
OrigHelp: tag.Help, OrigHelp: tag.Help,
HasDefault: tag.HasDefault, HasDefault: tag.HasDefault,
Default: tag.Default, Default: tag.Default,
DefaultValue: reflect.New(fv.Type()).Elem(), DefaultValue: reflect.New(fv.Type()).Elem(),
Mapper: mapper, Mapper: mapper,
Tag: tag, Tag: tag,
Target: fv, Target: fv,
Enum: tag.Enum, Enum: tag.Enum,
Passthrough: tag.Passthrough, Passthrough: tag.Passthrough,
PassthroughMode: tag.PassthroughMode,
// Flags are optional by default, and args are required by default. // Flags are optional by default, and args are required by default.
Required: (!tag.Arg && tag.Required) || (tag.Arg && !tag.Optional), Required: (!tag.Arg && tag.Required) || (tag.Arg && !tag.Optional),
+2 -2
View File
@@ -425,7 +425,7 @@ func (c *Context) trace(node *Node) (err error) { //nolint: gocyclo
case FlagToken: case FlagToken:
if err := c.parseFlag(flags, token.String()); err != nil { if err := c.parseFlag(flags, token.String()); err != nil {
if isUnknownFlagError(err) && positional < len(node.Positional) && node.Positional[positional].Passthrough { if isUnknownFlagError(err) && positional < len(node.Positional) && node.Positional[positional].PassthroughMode == PassThroughModeAll {
c.scan.Pop() c.scan.Pop()
c.scan.PushTyped(token.String(), PositionalArgumentToken) c.scan.PushTyped(token.String(), PositionalArgumentToken)
} else { } else {
@@ -435,7 +435,7 @@ func (c *Context) trace(node *Node) (err error) { //nolint: gocyclo
case ShortFlagToken: case ShortFlagToken:
if err := c.parseFlag(flags, token.String()); err != nil { if err := c.parseFlag(flags, token.String()); err != nil {
if isUnknownFlagError(err) && positional < len(node.Positional) && node.Positional[positional].Passthrough { if isUnknownFlagError(err) && positional < len(node.Positional) && node.Positional[positional].PassthroughMode == PassThroughModeAll {
c.scan.Pop() c.scan.Pop()
c.scan.PushTyped(token.String(), PositionalArgumentToken) c.scan.PushTyped(token.String(), PositionalArgumentToken)
} else { } else {
+29
View File
@@ -1803,6 +1803,35 @@ func TestPassthroughArgs(t *testing.T) {
} }
} }
func TestPassthroughPartial(t *testing.T) {
var cli struct {
Flag string
Args []string `arg:"" optional:"" passthrough:"partial"`
}
p := mustNew(t, &cli)
_, err := p.Parse([]string{"--flag", "foobar", "something"})
assert.NoError(t, err)
assert.Equal(t, "foobar", cli.Flag)
assert.Equal(t, []string{"something"}, cli.Args)
_, err = p.Parse([]string{"--invalid", "foobar", "something"})
assert.EqualError(t, err, "unknown flag --invalid")
}
func TestPassthroughAll(t *testing.T) {
var cli struct {
Flag string
Args []string `arg:"" optional:"" passthrough:"all"`
}
p := mustNew(t, &cli)
_, err := p.Parse([]string{"--flag", "foobar", "something"})
assert.NoError(t, err)
assert.Equal(t, "foobar", cli.Flag)
assert.Equal(t, []string{"something"}, cli.Args)
_, err = p.Parse([]string{"--invalid", "foobar", "something"})
assert.NoError(t, err)
assert.Equal(t, []string{"--invalid", "foobar", "something"}, cli.Args)
}
func TestPassthroughCmd(t *testing.T) { func TestPassthroughCmd(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
+18 -17
View File
@@ -239,23 +239,24 @@ func (n *Node) ClosestGroup() *Group {
// A Value is either a flag or a variable positional argument. // A Value is either a flag or a variable positional argument.
type Value struct { type Value struct {
Flag *Flag // Nil if positional argument. Flag *Flag // Nil if positional argument.
Name string Name string
Help string Help string
OrigHelp string // Original help string, without interpolated variables. OrigHelp string // Original help string, without interpolated variables.
HasDefault bool HasDefault bool
Default string Default string
DefaultValue reflect.Value DefaultValue reflect.Value
Enum string Enum string
Mapper Mapper Mapper Mapper
Tag *Tag Tag *Tag
Target reflect.Value Target reflect.Value
Required bool Required bool
Set bool // Set to true when this value is set through some mechanism. Set bool // Set to true when this value is set through some mechanism.
Format string // Formatting directive, if applicable. Format string // Formatting directive, if applicable.
Position int // Position (for positional arguments). Position int // Position (for positional arguments).
Passthrough bool // Set to true to stop flag parsing when encountered. Passthrough bool // Deprecated: Use PassthroughMode instead. Set to true to stop flag parsing when encountered.
Active bool // Denotes the value is part of an active branch in the CLI. PassthroughMode PassthroughMode //
Active bool // Denotes the value is part of an active branch in the CLI.
} }
// EnumMap returns a map of the enums in this value. // EnumMap returns a map of the enums in this value.
+51 -29
View File
@@ -9,37 +9,50 @@ import (
"unicode/utf8" "unicode/utf8"
) )
// PassthroughMode indicates how parameters are passed through when "passthrough" is set.
type PassthroughMode int
const (
// PassThroughModeNone indicates passthrough mode is disabled.
PassThroughModeNone PassthroughMode = iota
// PassThroughModeAll indicates that all parameters, including flags, are passed through. It is the default.
PassThroughModeAll
// PassThroughModePartial will validate flags until the first positional argument is encountered, then pass through all remaining positional arguments.
PassThroughModePartial
)
// Tag represents the parsed state of Kong tags in a struct field tag. // Tag represents the parsed state of Kong tags in a struct field tag.
type Tag struct { type Tag struct {
Ignored bool // Field is ignored by Kong. ie. kong:"-" Ignored bool // Field is ignored by Kong. ie. kong:"-"
Cmd bool Cmd bool
Arg bool Arg bool
Required bool Required bool
Optional bool Optional bool
Name string Name string
Help string Help string
Type string Type string
TypeName string TypeName string
HasDefault bool HasDefault bool
Default string Default string
Format string Format string
PlaceHolder string PlaceHolder string
Envs []string Envs []string
Short rune Short rune
Hidden bool Hidden bool
Sep rune Sep rune
MapSep rune MapSep rune
Enum string Enum string
Group string Group string
Xor []string Xor []string
And []string And []string
Vars Vars Vars Vars
Prefix string // Optional prefix on anonymous structs. All sub-flags will have this prefix. Prefix string // Optional prefix on anonymous structs. All sub-flags will have this prefix.
EnvPrefix string EnvPrefix string
Embed bool Embed bool
Aliases []string Aliases []string
Negatable string Negatable string
Passthrough bool Passthrough bool // Deprecated: use PassthroughMode instead.
PassthroughMode PassthroughMode
// Storage for all tag keys for arbitrary lookups. // Storage for all tag keys for arbitrary lookups.
items map[string][]string items map[string][]string
@@ -289,6 +302,15 @@ func hydrateTag(t *Tag, typ reflect.Type) error { //nolint: gocyclo
return fmt.Errorf("passthrough only makes sense for positional arguments or commands") return fmt.Errorf("passthrough only makes sense for positional arguments or commands")
} }
t.Passthrough = passthrough t.Passthrough = passthrough
passthroughMode := t.Get("passthrough")
switch passthroughMode {
case "partial":
t.PassthroughMode = PassThroughModePartial
case "all", "":
t.PassthroughMode = PassThroughModeAll
default:
return fmt.Errorf("invalid passthrough mode %q, must be one of 'partial' or 'all'", passthroughMode)
}
return nil return nil
} }