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:
+39
-17
@@ -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)}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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})
|
||||||
|
|||||||
Reference in New Issue
Block a user