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
}
```
This commit is contained in:
Bob Lail
2025-03-07 14:28:09 -08:00
committed by GitHub
parent 0c495e4936
commit 5b36573738
3 changed files with 63 additions and 17 deletions
+39 -17
View File
@@ -26,6 +26,9 @@ type Path struct {
// True if this Path element was created as the result of a resolver. // True if this Path element was created as the result of a resolver.
Resolved bool 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. // 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 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. // Context contains the current parse context.
type Context struct { type Context struct {
*Kong *Kong
@@ -87,14 +99,15 @@ type Context struct {
// This just constructs a new trace. To fully apply the trace you must call Reset(), Resolve(), // This just constructs a new trace. To fully apply the trace you must call Reset(), Resolve(),
// Validate() and Apply(). // Validate() and Apply().
func Trace(k *Kong, args []string) (*Context, error) { func Trace(k *Kong, args []string) (*Context, error) {
s := Scan(args...)
c := &Context{ c := &Context{
Kong: k, Kong: k,
Args: args, Args: args,
Path: []*Path{ Path: []*Path{
{App: k.Model, Flags: k.Model.Flags}, {App: k.Model, Flags: k.Model.Flags, remainder: s.PeekAll()},
}, },
values: map[*Value]reflect.Value{}, values: map[*Value]reflect.Value{},
scan: Scan(args...), scan: s,
bindings: bindings{}, bindings: bindings{},
} }
c.Error = c.trace(c.Model.Node) 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{ c.Path = append(c.Path, &Path{
Parent: node, Parent: node,
Positional: arg, Positional: arg,
remainder: c.scan.PeekAll(),
}) })
positional++ positional++
break break
@@ -508,9 +522,10 @@ func (c *Context) trace(node *Node) (err error) { //nolint: gocyclo
if branch.Type == CommandNode && branch.Name == token.Value { if branch.Type == CommandNode && branch.Name == token.Value {
c.scan.Pop() c.scan.Pop()
c.Path = append(c.Path, &Path{ c.Path = append(c.Path, &Path{
Parent: node, Parent: node,
Command: branch, Command: branch,
Flags: branch.Flags, Flags: branch.Flags,
remainder: c.scan.PeekAll(),
}) })
return c.trace(branch) return c.trace(branch)
} }
@@ -522,9 +537,10 @@ func (c *Context) trace(node *Node) (err error) { //nolint: gocyclo
arg := branch.Argument arg := branch.Argument
if err := arg.Parse(c.scan, c.getValue(arg)); err == nil { if err := arg.Parse(c.scan, c.getValue(arg)); err == nil {
c.Path = append(c.Path, &Path{ c.Path = append(c.Path, &Path{
Parent: node, Parent: node,
Argument: branch, Argument: branch,
Flags: branch.Flags, Flags: branch.Flags,
remainder: c.scan.PeekAll(),
}) })
return c.trace(branch) 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 // matches, take the branch of the default command
if node.DefaultCmd != nil && node.DefaultCmd.Tag.Default == "withargs" { if node.DefaultCmd != nil && node.DefaultCmd.Tag.Default == "withargs" {
c.Path = append(c.Path, &Path{ c.Path = append(c.Path, &Path{
Parent: node, Parent: node,
Command: node.DefaultCmd, Command: node.DefaultCmd,
Flags: node.DefaultCmd.Flags, Flags: node.DefaultCmd.Flags,
remainder: c.scan.PeekAll(),
}) })
return c.trace(node.DefaultCmd) return c.trace(node.DefaultCmd)
} }
@@ -565,9 +582,10 @@ func (c *Context) maybeSelectDefault(flags []*Flag, node *Node) error {
} }
if node.DefaultCmd != nil { if node.DefaultCmd != nil {
c.Path = append(c.Path, &Path{ c.Path = append(c.Path, &Path{
Parent: node.DefaultCmd, Parent: node.DefaultCmd,
Command: node.DefaultCmd, Command: node.DefaultCmd,
Flags: node.DefaultCmd.Flags, Flags: node.DefaultCmd.Flags,
remainder: c.scan.PeekAll(),
}) })
} }
return nil return nil
@@ -612,8 +630,9 @@ func (c *Context) Resolve() error {
return err return err
} }
inserted = append(inserted, &Path{ inserted = append(inserted, &Path{
Flag: flag, Flag: flag,
Resolved: true, Resolved: true,
remainder: c.scan.PeekAll(),
}) })
} }
} }
@@ -757,7 +776,10 @@ func (c *Context) parseFlag(flags []*Flag, match string) (err error) {
} }
flag.Value.Apply(value) 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 nil
} }
return &unknownFlagError{Cause: findPotentialCandidates(match, candidates, "unknown flag %s", match)} return &unknownFlagError{Cause: findPotentialCandidates(match, candidates, "unknown flag %s", match)}
+19
View File
@@ -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) { func TestBranchingArgument(t *testing.T) {
/* /*
app user create <id> <first> <last> app user create <id> <first> <last>
+5
View File
@@ -203,6 +203,11 @@ func (s *Scanner) Peek() Token {
return s.args[0] 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. // Push an untyped Token onto the front of the Scanner.
func (s *Scanner) Push(arg any) *Scanner { func (s *Scanner) Push(arg any) *Scanner {
s.PushToken(Token{Value: arg}) s.PushToken(Token{Value: arg})