From 96647c30af56cdf1a273f3f95de0b73c6d7a0b38 Mon Sep 17 00:00:00 2001 From: Alec Thomas Date: Sun, 1 Dec 2024 19:58:24 +1100 Subject: [PATCH] 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. --- README.md | 6 +++- build.go | 23 +++++++-------- context.go | 4 +-- kong_test.go | 29 +++++++++++++++++++ model.go | 35 ++++++++++++----------- tag.go | 80 +++++++++++++++++++++++++++++++++------------------- 6 files changed, 117 insertions(+), 60 deletions(-) diff --git a/README.md b/README.md index 5dbed024..10b61289 100644 --- a/README.md +++ b/README.md @@ -590,9 +590,13 @@ Both can coexist with standard Tag parsing. | `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 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:""`[^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:"-"` `` | +[^1]: `` 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 Kong CLI's can be extended by embedding the `kong.Plugin` type and populating it with pointers to Kong annotated structs. For example: diff --git a/build.go b/build.go index 287beeb8..42d30f08 100644 --- a/build.go +++ b/build.go @@ -281,17 +281,18 @@ func buildField(k *Kong, node *Node, v reflect.Value, ft reflect.StructField, fv } value := &Value{ - Name: name, - Help: tag.Help, - OrigHelp: tag.Help, - HasDefault: tag.HasDefault, - Default: tag.Default, - DefaultValue: reflect.New(fv.Type()).Elem(), - Mapper: mapper, - Tag: tag, - Target: fv, - Enum: tag.Enum, - Passthrough: tag.Passthrough, + Name: name, + Help: tag.Help, + OrigHelp: tag.Help, + HasDefault: tag.HasDefault, + Default: tag.Default, + DefaultValue: reflect.New(fv.Type()).Elem(), + Mapper: mapper, + Tag: tag, + Target: fv, + Enum: tag.Enum, + Passthrough: tag.Passthrough, + PassthroughMode: tag.PassthroughMode, // 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 77643ae8..b339f6bd 100644 --- a/context.go +++ b/context.go @@ -425,7 +425,7 @@ func (c *Context) trace(node *Node) (err error) { //nolint: gocyclo case FlagToken: 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.PushTyped(token.String(), PositionalArgumentToken) } else { @@ -435,7 +435,7 @@ func (c *Context) trace(node *Node) (err error) { //nolint: gocyclo case ShortFlagToken: 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.PushTyped(token.String(), PositionalArgumentToken) } else { diff --git a/kong_test.go b/kong_test.go index 36e18e11..bd47185d 100644 --- a/kong_test.go +++ b/kong_test.go @@ -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) { tests := []struct { name string diff --git a/model.go b/model.go index 3190f3e8..25ffe96b 100644 --- a/model.go +++ b/model.go @@ -239,23 +239,24 @@ func (n *Node) ClosestGroup() *Group { // A Value is either a flag or a variable positional argument. type Value struct { - Flag *Flag // Nil if positional argument. - Name string - Help string - OrigHelp string // Original help string, without interpolated variables. - HasDefault bool - Default string - DefaultValue reflect.Value - Enum string - Mapper Mapper - Tag *Tag - Target reflect.Value - Required bool - 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. - Active bool // Denotes the value is part of an active branch in the CLI. + Flag *Flag // Nil if positional argument. + Name string + Help string + OrigHelp string // Original help string, without interpolated variables. + HasDefault bool + Default string + DefaultValue reflect.Value + Enum string + Mapper Mapper + Tag *Tag + Target reflect.Value + Required bool + 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 // Deprecated: Use PassthroughMode instead. Set to true to stop flag parsing when encountered. + 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. diff --git a/tag.go b/tag.go index 456f7ae4..00fb7e7f 100644 --- a/tag.go +++ b/tag.go @@ -9,37 +9,50 @@ import ( "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. type Tag struct { - Ignored bool // Field is ignored by Kong. ie. kong:"-" - Cmd bool - Arg bool - Required bool - Optional bool - Name string - Help string - Type string - TypeName string - HasDefault bool - Default string - Format string - PlaceHolder string - Envs []string - Short rune - Hidden bool - Sep rune - MapSep rune - Enum string - Group string - Xor []string - And []string - Vars Vars - Prefix string // Optional prefix on anonymous structs. All sub-flags will have this prefix. - EnvPrefix string - Embed bool - Aliases []string - Negatable string - Passthrough bool + Ignored bool // Field is ignored by Kong. ie. kong:"-" + Cmd bool + Arg bool + Required bool + Optional bool + Name string + Help string + Type string + TypeName string + HasDefault bool + Default string + Format string + PlaceHolder string + Envs []string + Short rune + Hidden bool + Sep rune + MapSep rune + Enum string + Group string + Xor []string + And []string + Vars Vars + Prefix string // Optional prefix on anonymous structs. All sub-flags will have this prefix. + EnvPrefix string + Embed bool + Aliases []string + Negatable string + Passthrough bool // Deprecated: use PassthroughMode instead. + PassthroughMode PassthroughMode // Storage for all tag keys for arbitrary lookups. 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") } 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 }