From 5b36573738d800fbd511e9ed3c5711db6c7b6661 Mon Sep 17 00:00:00 2001 From: Bob Lail Date: Fri, 7 Mar 2025 14:28:09 -0800 Subject: [PATCH] feat: Allow kong.Path to describe remaining unparsed args (#472) As Kong traces a sequence of command line arguments, it parses them and appends them to the parsed `Path` sequence. For each element in `Path`, these is a corresponding sequence of unparsed arguments. This change enables `Path` to yield these. I have a package that uses Kong's hooks to instrument Kong applications (to monitor usage, reliability, etc of internal tools). I would like to instrument the commandline arguments as well. This change would enable it to work roughly as follows: ```golang func (Foo) BeforeApply(app *kong.Kong, ctx *kong.Context, t *Tracker) error { command := []string{ctx.Model.Name} var args []string for _, path := range ctx.Path { if path.Command != nil { command = append(command, path.Command.Name) args = path.Remainder() } } app.Exit = t.exit(app.Exit) t.WithCommand(strings.Join(command, " ")).WithArgs(args) return nil } ``` --- context.go | 56 ++++++++++++++++++++++++++++++++++++---------------- kong_test.go | 19 ++++++++++++++++++ scanner.go | 5 +++++ 3 files changed, 63 insertions(+), 17 deletions(-) diff --git a/context.go b/context.go index dcaf099..16a7353 100644 --- a/context.go +++ b/context.go @@ -26,6 +26,9 @@ type Path struct { // True if this Path element was created as the result of a resolver. Resolved bool + + // Remaining tokens after this node + remainder []Token } // Node returns the Node associated with this Path, or nil if Path is a non-Node. @@ -64,6 +67,15 @@ func (p *Path) Visitable() Visitable { return nil } +// Remainder returns the remaining unparsed args after this Path element. +func (p *Path) Remainder() []string { + args := []string{} + for _, token := range p.remainder { + args = append(args, token.String()) + } + return args +} + // Context contains the current parse context. type Context struct { *Kong @@ -87,14 +99,15 @@ type Context struct { // This just constructs a new trace. To fully apply the trace you must call Reset(), Resolve(), // Validate() and Apply(). func Trace(k *Kong, args []string) (*Context, error) { + s := Scan(args...) c := &Context{ Kong: k, Args: args, Path: []*Path{ - {App: k.Model, Flags: k.Model.Flags}, + {App: k.Model, Flags: k.Model.Flags, remainder: s.PeekAll()}, }, values: map[*Value]reflect.Value{}, - scan: Scan(args...), + scan: s, bindings: bindings{}, } c.Error = c.trace(c.Model.Node) @@ -477,6 +490,7 @@ func (c *Context) trace(node *Node) (err error) { //nolint: gocyclo c.Path = append(c.Path, &Path{ Parent: node, Positional: arg, + remainder: c.scan.PeekAll(), }) positional++ break @@ -508,9 +522,10 @@ func (c *Context) trace(node *Node) (err error) { //nolint: gocyclo if branch.Type == CommandNode && branch.Name == token.Value { c.scan.Pop() c.Path = append(c.Path, &Path{ - Parent: node, - Command: branch, - Flags: branch.Flags, + Parent: node, + Command: branch, + Flags: branch.Flags, + remainder: c.scan.PeekAll(), }) return c.trace(branch) } @@ -522,9 +537,10 @@ func (c *Context) trace(node *Node) (err error) { //nolint: gocyclo arg := branch.Argument if err := arg.Parse(c.scan, c.getValue(arg)); err == nil { c.Path = append(c.Path, &Path{ - Parent: node, - Argument: branch, - Flags: branch.Flags, + Parent: node, + Argument: branch, + Flags: branch.Flags, + remainder: c.scan.PeekAll(), }) return c.trace(branch) } @@ -535,9 +551,10 @@ func (c *Context) trace(node *Node) (err error) { //nolint: gocyclo // matches, take the branch of the default command if node.DefaultCmd != nil && node.DefaultCmd.Tag.Default == "withargs" { c.Path = append(c.Path, &Path{ - Parent: node, - Command: node.DefaultCmd, - Flags: node.DefaultCmd.Flags, + Parent: node, + Command: node.DefaultCmd, + Flags: node.DefaultCmd.Flags, + remainder: c.scan.PeekAll(), }) return c.trace(node.DefaultCmd) } @@ -565,9 +582,10 @@ func (c *Context) maybeSelectDefault(flags []*Flag, node *Node) error { } if node.DefaultCmd != nil { c.Path = append(c.Path, &Path{ - Parent: node.DefaultCmd, - Command: node.DefaultCmd, - Flags: node.DefaultCmd.Flags, + Parent: node.DefaultCmd, + Command: node.DefaultCmd, + Flags: node.DefaultCmd.Flags, + remainder: c.scan.PeekAll(), }) } return nil @@ -612,8 +630,9 @@ func (c *Context) Resolve() error { return err } inserted = append(inserted, &Path{ - Flag: flag, - Resolved: true, + Flag: flag, + Resolved: true, + remainder: c.scan.PeekAll(), }) } } @@ -757,7 +776,10 @@ func (c *Context) parseFlag(flags []*Flag, match string) (err error) { } flag.Value.Apply(value) } - c.Path = append(c.Path, &Path{Flag: flag}) + c.Path = append(c.Path, &Path{ + Flag: flag, + remainder: c.scan.PeekAll(), + }) return nil } return &unknownFlagError{Cause: findPotentialCandidates(match, candidates, "unknown flag %s", match)} diff --git a/kong_test.go b/kong_test.go index 15d1f51..4edda4f 100644 --- a/kong_test.go +++ b/kong_test.go @@ -48,6 +48,25 @@ func TestPositionalArguments(t *testing.T) { }) } +func TestRemainderReturnsUnparsedArgs(t *testing.T) { + var cli struct { + User struct { + Create struct { + ID int `kong:"arg"` + First string `kong:"arg"` + Last string `kong:"arg"` + } `kong:"cmd"` + } `kong:"cmd"` + } + p := mustNew(t, &cli) + args := []string{"user", "create", "10", "Alec", "Thomas"} + ctx, err := p.Parse(args) + assert.NoError(t, err) + for i, x := range ctx.Path { + assert.Equal(t, strings.Join(args[i:], " "), strings.Join(x.Remainder(), " ")) + } +} + func TestBranchingArgument(t *testing.T) { /* app user create diff --git a/scanner.go b/scanner.go index 262d16f..68f708e 100644 --- a/scanner.go +++ b/scanner.go @@ -203,6 +203,11 @@ func (s *Scanner) Peek() Token { return s.args[0] } +// PeekAll remaining tokens +func (s *Scanner) PeekAll() []Token { + return s.args +} + // Push an untyped Token onto the front of the Scanner. func (s *Scanner) Push(arg any) *Scanner { s.PushToken(Token{Value: arg})