Skip to content

Commit

Permalink
feat: add old "passthrough" behaviour back in as an option
Browse files Browse the repository at this point in the history
`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.
  • Loading branch information
alecthomas committed Dec 1, 2024
1 parent 88e13d7 commit 96647c3
Show file tree
Hide file tree
Showing 6 changed files with 117 additions and 60 deletions.
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:"<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:"-"` `` |

[^1]: `<mode>` can be `partial` or `all` (the default). `all` will pass through all arguments including flags, including

This comment has been minimized.

Copy link
@dbohdan

dbohdan Dec 1, 2024

There is a typo here. "Including flags" is written twice.

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:
Expand Down
23 changes: 12 additions & 11 deletions build.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
4 changes: 2 additions & 2 deletions context.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down
29 changes: 29 additions & 0 deletions kong_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
35 changes: 18 additions & 17 deletions model.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
80 changes: 51 additions & 29 deletions tag.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}

Expand Down

0 comments on commit 96647c3

Please sign in to comment.