From 949a6ea948b8dc2c3153174946ed422461fc583d Mon Sep 17 00:00:00 2001 From: Janusz Marcinkiewicz Date: Tue, 31 Mar 2020 19:28:11 +0200 Subject: [PATCH] Prevent panic on flagSet access from custom BashComplete --- app.go | 4 ++-- command.go | 6 +++--- command_test.go | 47 +++++++++++++++++++++++++++++++++++++++++++++++ parse.go | 9 +++++++-- 4 files changed, 59 insertions(+), 7 deletions(-) diff --git a/app.go b/app.go index 6e729841c1..ddb7685d62 100644 --- a/app.go +++ b/app.go @@ -207,7 +207,7 @@ func (a *App) Run(arguments []string) (err error) { return err } - err = parseIter(set, a, arguments[1:]) + err = parseIter(set, a, arguments[1:], shellComplete) nerr := normalizeFlags(a.Flags, set) context := NewContext(a, set, nil) if nerr != nil { @@ -330,7 +330,7 @@ func (a *App) RunAsSubcommand(ctx *Context) (err error) { return err } - err = parseIter(set, a, ctx.Args().Tail()) + err = parseIter(set, a, ctx.Args().Tail(), ctx.shellComplete) nerr := normalizeFlags(a.Flags, set) context := NewContext(a, set, ctx) diff --git a/command.go b/command.go index e7cb97a6af..24e9e5c572 100644 --- a/command.go +++ b/command.go @@ -114,7 +114,7 @@ func (c Command) Run(ctx *Context) (err error) { c.UseShortOptionHandling = true } - set, err := c.parseFlags(ctx.Args().Tail()) + set, err := c.parseFlags(ctx.Args().Tail(), ctx.shellComplete) context := NewContext(ctx.App, set, ctx) context.Command = c @@ -179,7 +179,7 @@ func (c Command) Run(ctx *Context) (err error) { return err } -func (c *Command) parseFlags(args Args) (*flag.FlagSet, error) { +func (c *Command) parseFlags(args Args, shellComplete bool) (*flag.FlagSet, error) { if c.SkipFlagParsing { set, err := c.newFlagSet() if err != nil { @@ -198,7 +198,7 @@ func (c *Command) parseFlags(args Args) (*flag.FlagSet, error) { return nil, err } - err = parseIter(set, c, args) + err = parseIter(set, c, args, shellComplete) if err != nil { return nil, err } diff --git a/command_test.go b/command_test.go index bcfbee747e..a59d368c46 100644 --- a/command_test.go +++ b/command_test.go @@ -1,6 +1,7 @@ package cli import ( + "bytes" "errors" "flag" "fmt" @@ -371,3 +372,49 @@ func TestCommandSkipFlagParsing(t *testing.T) { expect(t, args, c.expectedArgs) } } + +func TestCommand_Run_CustomShellCompleteAcceptsMalformedFlags(t *testing.T) { + cases := []struct { + testArgs []string + expectedOut string + }{ + {testArgs: []string{"--undefined"}, expectedOut: "found 0 args | flag 'number' set: false"}, + {testArgs: []string{"--number"}, expectedOut: "found 0 args | flag 'number' set: false"}, + {testArgs: []string{"--number", "fourty-two"}, expectedOut: "found 0 args | flag 'number' set: false"}, + {testArgs: []string{"--number", "42"}, expectedOut: "found 0 args | flag 'number' set: true"}, + {testArgs: []string{"--number", "42", "newArg"}, expectedOut: "found 1 args | flag 'number' set: true"}, + {testArgs: []string{"--undefined", "--number", "42", "newArg"}, expectedOut: "found 1 args | flag 'number' set: true"}, + } + + for _, c := range cases { + var outputBuffer bytes.Buffer + app := &App{ + Writer: &outputBuffer, + EnableBashCompletion: true, + Commands: []Command{ + { + Name: "bar", + Usage: "this is for testing", + Flags: []Flag{ + &IntFlag{ + Name: "number", + Usage: "A number to parse", + }, + }, + BashComplete: func(c *Context) { + fmt.Fprintf(c.App.Writer, "found %d args | flag 'number' set: %t", c.NArg(), c.IsSet("number")) + }, + }, + }, + } + + osArgs := []string{"foo", "bar"} + osArgs = append(osArgs, c.testArgs...) + osArgs = append(osArgs, "--generate-bash-completion") + + err := app.Run(osArgs) + stdout := outputBuffer.String() + expect(t, err, nil) + expect(t, stdout, c.expectedOut) + } +} diff --git a/parse.go b/parse.go index 660f538b03..7df17296a4 100644 --- a/parse.go +++ b/parse.go @@ -11,13 +11,18 @@ type iterativeParser interface { } // To enable short-option handling (e.g., "-it" vs "-i -t") we have to -// iteratively catch parsing errors. This way we achieve LR parsing without +// iteratively catch parsing errors. This way we achieve LR parsing without // transforming any arguments. Otherwise, there is no way we can discriminate // combined short options from common arguments that should be left untouched. -func parseIter(set *flag.FlagSet, ip iterativeParser, args []string) error { +// Pass `shellComplete` to continue parsing options on failure during shell +// completion when, the user-supplied options may be incomplete. +func parseIter(set *flag.FlagSet, ip iterativeParser, args []string, shellComplete bool) error { for { err := set.Parse(args) if !ip.useShortOptionHandling() || err == nil { + if shellComplete { + return nil + } return err }