Skip to content

Commit

Permalink
Only look for subcommands up to quoted with spaces
Browse files Browse the repository at this point in the history
Fixes mitchellh#68

This changes the subcommand search behavior to only look up to the point
where a space is found in a single arg. This doesn't break prior
behavior because prior behavior would've resulted in a panic.

Examples:

1. `./cli foo "bar baz"` would match subcommand "foo" and treat "bar baz"
   as a single argument to subcommand "foo".

2. `./cli "foo bar"` would have no subcommand.

The reason I took this approach is because we otherwise treat quoted
arguments with a space as a single argument (as do shells). This keeps
that behavior in line.
  • Loading branch information
mitchellh committed Nov 29, 2017
1 parent 6d23156 commit 3d2e81c
Show file tree
Hide file tree
Showing 2 changed files with 109 additions and 1 deletion.
22 changes: 21 additions & 1 deletion cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -646,9 +646,29 @@ func (c *CLI) processArgs() {
if c.subcommand == "" && arg != "" && arg[0] != '-' {
c.subcommand = arg
if c.commandNested {
// If the command has a space in it, then it is invalid.
// Set a blank command so that it fails.
if strings.ContainsRune(arg, ' ') {
c.subcommand = ""
return
}

// Determine the argument we look to to end subcommands.
// We look at all arguments until one has a space. This
// disallows commands like: ./cli foo "bar baz". An argument
// with a space is always an argument.
j := len(c.Args) - 1
for k, v := range c.Args[i:] {
if strings.ContainsRune(v, ' ') {
break
}

j = i + k + 1
}

// Nested CLI, the subcommand is actually the entire
// arg list up to a flag that is still a valid subcommand.
searchKey := strings.Join(c.Args[i:], " ")
searchKey := strings.Join(c.Args[i:j], " ")
k, _, ok := c.commandTree.LongestPrefix(searchKey)
if ok {
// k could be a prefix that doesn't contain the full
Expand Down
88 changes: 88 additions & 0 deletions cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,94 @@ func TestCLIRun_nestedMissingParent(t *testing.T) {
}
}

func TestCLIRun_nestedNoArgs(t *testing.T) {
command := new(MockCommand)
cli := &CLI{
Args: []string{"foo", "bar"},
Commands: map[string]CommandFactory{
"foo": func() (Command, error) {
return new(MockCommand), nil
},
"foo bar": func() (Command, error) {
return command, nil
},
},
}

exitCode, err := cli.Run()
if err != nil {
t.Fatalf("err: %s", err)
}

if exitCode != command.RunResult {
t.Fatalf("bad: %d", exitCode)
}

if !command.RunCalled {
t.Fatalf("run should be called")
}

if !reflect.DeepEqual(command.RunArgs, []string{}) {
t.Fatalf("bad args: %#v", command.RunArgs)
}
}

func TestCLIRun_nestedQuotedCommand(t *testing.T) {
command := new(MockCommand)
cli := &CLI{
Args: []string{"foo bar"},
Commands: map[string]CommandFactory{
"foo": func() (Command, error) {
return new(MockCommand), nil
},
"foo bar": func() (Command, error) {
return command, nil
},
},
}

exitCode, err := cli.Run()
if err != nil {
t.Fatalf("err: %s", err)
}

if exitCode != 127 {
t.Fatalf("bad: %d", exitCode)
}
}

func TestCLIRun_nestedQuotedArg(t *testing.T) {
command := new(MockCommand)
cli := &CLI{
Args: []string{"foo", "bar baz"},
Commands: map[string]CommandFactory{
"foo": func() (Command, error) {
return command, nil
},
"foo bar": func() (Command, error) {
return new(MockCommand), nil
},
},
}

exitCode, err := cli.Run()
if err != nil {
t.Fatalf("err: %s", err)
}

if exitCode != command.RunResult {
t.Fatalf("bad: %d", exitCode)
}

if !command.RunCalled {
t.Fatalf("run should be called")
}

if !reflect.DeepEqual(command.RunArgs, []string{"bar baz"}) {
t.Fatalf("bad args: %#v", command.RunArgs)
}
}

func TestCLIRun_printHelp(t *testing.T) {
testCases := [][]string{
{"-h"},
Expand Down

0 comments on commit 3d2e81c

Please sign in to comment.