Skip to content

Commit 96647c3

Browse files
committed
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.
1 parent 88e13d7 commit 96647c3

File tree

6 files changed

+117
-60
lines changed

6 files changed

+117
-60
lines changed

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -590,9 +590,13 @@ Both can coexist with standard Tag parsing.
590590
| `envprefix:"X"` | Envar prefix for all sub-flags. |
591591
| `set:"K=V"` | Set a variable for expansion by child elements. Multiples can occur. |
592592
| `embed:""` | If present, this field's children will be embedded in the parent. Useful for composition. |
593-
| `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. |
593+
| `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. |
594594
| `-` | Ignore the field. Useful for adding non-CLI fields to a configuration struct. e.g `` `kong:"-"` `` |
595595

596+
[^1]: `<mode>` can be `partial` or `all` (the default). `all` will pass through all arguments including flags, including
597+
flags. `partial` will validate flags until the first positional argument is encountered, then pass through all remaining
598+
positional arguments.
599+
596600
## Plugins
597601

598602
Kong CLI's can be extended by embedding the `kong.Plugin` type and populating it with pointers to Kong annotated structs. For example:

build.go

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -281,17 +281,18 @@ func buildField(k *Kong, node *Node, v reflect.Value, ft reflect.StructField, fv
281281
}
282282

283283
value := &Value{
284-
Name: name,
285-
Help: tag.Help,
286-
OrigHelp: tag.Help,
287-
HasDefault: tag.HasDefault,
288-
Default: tag.Default,
289-
DefaultValue: reflect.New(fv.Type()).Elem(),
290-
Mapper: mapper,
291-
Tag: tag,
292-
Target: fv,
293-
Enum: tag.Enum,
294-
Passthrough: tag.Passthrough,
284+
Name: name,
285+
Help: tag.Help,
286+
OrigHelp: tag.Help,
287+
HasDefault: tag.HasDefault,
288+
Default: tag.Default,
289+
DefaultValue: reflect.New(fv.Type()).Elem(),
290+
Mapper: mapper,
291+
Tag: tag,
292+
Target: fv,
293+
Enum: tag.Enum,
294+
Passthrough: tag.Passthrough,
295+
PassthroughMode: tag.PassthroughMode,
295296

296297
// Flags are optional by default, and args are required by default.
297298
Required: (!tag.Arg && tag.Required) || (tag.Arg && !tag.Optional),

context.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -425,7 +425,7 @@ func (c *Context) trace(node *Node) (err error) { //nolint: gocyclo
425425

426426
case FlagToken:
427427
if err := c.parseFlag(flags, token.String()); err != nil {
428-
if isUnknownFlagError(err) && positional < len(node.Positional) && node.Positional[positional].Passthrough {
428+
if isUnknownFlagError(err) && positional < len(node.Positional) && node.Positional[positional].PassthroughMode == PassThroughModeAll {
429429
c.scan.Pop()
430430
c.scan.PushTyped(token.String(), PositionalArgumentToken)
431431
} else {
@@ -435,7 +435,7 @@ func (c *Context) trace(node *Node) (err error) { //nolint: gocyclo
435435

436436
case ShortFlagToken:
437437
if err := c.parseFlag(flags, token.String()); err != nil {
438-
if isUnknownFlagError(err) && positional < len(node.Positional) && node.Positional[positional].Passthrough {
438+
if isUnknownFlagError(err) && positional < len(node.Positional) && node.Positional[positional].PassthroughMode == PassThroughModeAll {
439439
c.scan.Pop()
440440
c.scan.PushTyped(token.String(), PositionalArgumentToken)
441441
} else {

kong_test.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1803,6 +1803,35 @@ func TestPassthroughArgs(t *testing.T) {
18031803
}
18041804
}
18051805

1806+
func TestPassthroughPartial(t *testing.T) {
1807+
var cli struct {
1808+
Flag string
1809+
Args []string `arg:"" optional:"" passthrough:"partial"`
1810+
}
1811+
p := mustNew(t, &cli)
1812+
_, err := p.Parse([]string{"--flag", "foobar", "something"})
1813+
assert.NoError(t, err)
1814+
assert.Equal(t, "foobar", cli.Flag)
1815+
assert.Equal(t, []string{"something"}, cli.Args)
1816+
_, err = p.Parse([]string{"--invalid", "foobar", "something"})
1817+
assert.EqualError(t, err, "unknown flag --invalid")
1818+
}
1819+
1820+
func TestPassthroughAll(t *testing.T) {
1821+
var cli struct {
1822+
Flag string
1823+
Args []string `arg:"" optional:"" passthrough:"all"`
1824+
}
1825+
p := mustNew(t, &cli)
1826+
_, err := p.Parse([]string{"--flag", "foobar", "something"})
1827+
assert.NoError(t, err)
1828+
assert.Equal(t, "foobar", cli.Flag)
1829+
assert.Equal(t, []string{"something"}, cli.Args)
1830+
_, err = p.Parse([]string{"--invalid", "foobar", "something"})
1831+
assert.NoError(t, err)
1832+
assert.Equal(t, []string{"--invalid", "foobar", "something"}, cli.Args)
1833+
}
1834+
18061835
func TestPassthroughCmd(t *testing.T) {
18071836
tests := []struct {
18081837
name string

model.go

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -239,23 +239,24 @@ func (n *Node) ClosestGroup() *Group {
239239

240240
// A Value is either a flag or a variable positional argument.
241241
type Value struct {
242-
Flag *Flag // Nil if positional argument.
243-
Name string
244-
Help string
245-
OrigHelp string // Original help string, without interpolated variables.
246-
HasDefault bool
247-
Default string
248-
DefaultValue reflect.Value
249-
Enum string
250-
Mapper Mapper
251-
Tag *Tag
252-
Target reflect.Value
253-
Required bool
254-
Set bool // Set to true when this value is set through some mechanism.
255-
Format string // Formatting directive, if applicable.
256-
Position int // Position (for positional arguments).
257-
Passthrough bool // Set to true to stop flag parsing when encountered.
258-
Active bool // Denotes the value is part of an active branch in the CLI.
242+
Flag *Flag // Nil if positional argument.
243+
Name string
244+
Help string
245+
OrigHelp string // Original help string, without interpolated variables.
246+
HasDefault bool
247+
Default string
248+
DefaultValue reflect.Value
249+
Enum string
250+
Mapper Mapper
251+
Tag *Tag
252+
Target reflect.Value
253+
Required bool
254+
Set bool // Set to true when this value is set through some mechanism.
255+
Format string // Formatting directive, if applicable.
256+
Position int // Position (for positional arguments).
257+
Passthrough bool // Deprecated: Use PassthroughMode instead. Set to true to stop flag parsing when encountered.
258+
PassthroughMode PassthroughMode //
259+
Active bool // Denotes the value is part of an active branch in the CLI.
259260
}
260261

261262
// EnumMap returns a map of the enums in this value.

tag.go

Lines changed: 51 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -9,37 +9,50 @@ import (
99
"unicode/utf8"
1010
)
1111

12+
// PassthroughMode indicates how parameters are passed through when "passthrough" is set.
13+
type PassthroughMode int
14+
15+
const (
16+
// PassThroughModeNone indicates passthrough mode is disabled.
17+
PassThroughModeNone PassthroughMode = iota
18+
// PassThroughModeAll indicates that all parameters, including flags, are passed through. It is the default.
19+
PassThroughModeAll
20+
// PassThroughModePartial will validate flags until the first positional argument is encountered, then pass through all remaining positional arguments.
21+
PassThroughModePartial
22+
)
23+
1224
// Tag represents the parsed state of Kong tags in a struct field tag.
1325
type Tag struct {
14-
Ignored bool // Field is ignored by Kong. ie. kong:"-"
15-
Cmd bool
16-
Arg bool
17-
Required bool
18-
Optional bool
19-
Name string
20-
Help string
21-
Type string
22-
TypeName string
23-
HasDefault bool
24-
Default string
25-
Format string
26-
PlaceHolder string
27-
Envs []string
28-
Short rune
29-
Hidden bool
30-
Sep rune
31-
MapSep rune
32-
Enum string
33-
Group string
34-
Xor []string
35-
And []string
36-
Vars Vars
37-
Prefix string // Optional prefix on anonymous structs. All sub-flags will have this prefix.
38-
EnvPrefix string
39-
Embed bool
40-
Aliases []string
41-
Negatable string
42-
Passthrough bool
26+
Ignored bool // Field is ignored by Kong. ie. kong:"-"
27+
Cmd bool
28+
Arg bool
29+
Required bool
30+
Optional bool
31+
Name string
32+
Help string
33+
Type string
34+
TypeName string
35+
HasDefault bool
36+
Default string
37+
Format string
38+
PlaceHolder string
39+
Envs []string
40+
Short rune
41+
Hidden bool
42+
Sep rune
43+
MapSep rune
44+
Enum string
45+
Group string
46+
Xor []string
47+
And []string
48+
Vars Vars
49+
Prefix string // Optional prefix on anonymous structs. All sub-flags will have this prefix.
50+
EnvPrefix string
51+
Embed bool
52+
Aliases []string
53+
Negatable string
54+
Passthrough bool // Deprecated: use PassthroughMode instead.
55+
PassthroughMode PassthroughMode
4356

4457
// Storage for all tag keys for arbitrary lookups.
4558
items map[string][]string
@@ -289,6 +302,15 @@ func hydrateTag(t *Tag, typ reflect.Type) error { //nolint: gocyclo
289302
return fmt.Errorf("passthrough only makes sense for positional arguments or commands")
290303
}
291304
t.Passthrough = passthrough
305+
passthroughMode := t.Get("passthrough")
306+
switch passthroughMode {
307+
case "partial":
308+
t.PassthroughMode = PassThroughModePartial
309+
case "all", "":
310+
t.PassthroughMode = PassThroughModeAll
311+
default:
312+
return fmt.Errorf("invalid passthrough mode %q, must be one of 'partial' or 'all'", passthroughMode)
313+
}
292314
return nil
293315
}
294316

0 commit comments

Comments
 (0)