Skip to content

Commit

Permalink
Allow commands' partial match
Browse files Browse the repository at this point in the history
  • Loading branch information
denisvm committed Jan 29, 2019
1 parent 8b8aa74 commit fde396e
Show file tree
Hide file tree
Showing 5 changed files with 80 additions and 15 deletions.
22 changes: 19 additions & 3 deletions command.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
"fmt"
"sort"
"strings"
"text/tabwriter"
)

Expand Down Expand Up @@ -94,30 +95,45 @@ func (c Cmd) HelpText() string {
}

// findChildCmd returns the subcommand with matching name or alias.
func (c *Cmd) findChildCmd(name string) *Cmd {
func (c *Cmd) findChildCmd(name string, partialMatch bool) *Cmd {
// find perfect matches first
if cmd, ok := c.children[name]; ok {
return cmd
}

var prefixes []*Cmd

// find alias matching the name
for _, cmd := range c.children {
if partialMatch && strings.HasPrefix(cmd.Name, name) {
prefixes = append(prefixes, cmd)
}

for _, alias := range cmd.Aliases {
if alias == name {
return cmd
}

if partialMatch && strings.HasPrefix(alias, name) {
prefixes = append(prefixes, cmd)
}
}
}

// allow only unique partial match
if len(prefixes) == 1 {
return prefixes[0]
}

return nil
}

// FindCmd finds the matching Cmd for args.
// It returns the Cmd and the remaining args.
func (c Cmd) FindCmd(args []string) (*Cmd, []string) {
func (c Cmd) FindCmd(args []string, partialMatch bool) (*Cmd, []string) {
var cmd *Cmd
for i, arg := range args {
if cmd1 := c.findChildCmd(arg); cmd1 != nil {
if cmd1 := c.findChildCmd(arg, partialMatch); cmd1 != nil {
cmd = cmd1
c = *cmd
continue
Expand Down
42 changes: 36 additions & 6 deletions command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,19 +33,19 @@ func TestFindCmd(t *testing.T) {
cmd := newCmd("root", "")
cmd.AddCmd(newCmd("child1", ""))
cmd.AddCmd(newCmd("child2", ""))
res, err := cmd.FindCmd([]string{"child1"})
res, err := cmd.FindCmd([]string{"child1"}, false)
if err != nil {
t.Fatal("finding should work")
}
assert.Equal(t, res.Name, "child1")

res, err = cmd.FindCmd([]string{"child2"})
res, err = cmd.FindCmd([]string{"child2"}, false)
if err != nil {
t.Fatal("finding should work")
}
assert.Equal(t, res.Name, "child2")

res, err = cmd.FindCmd([]string{"child3"})
res, err = cmd.FindCmd([]string{"child3"}, false)
if err == nil {
t.Fatal("should not find this child!")
}
Expand All @@ -58,19 +58,49 @@ func TestFindAlias(t *testing.T) {
subcmd.Aliases = []string{"alias1", "alias2"}
cmd.AddCmd(subcmd)

res, err := cmd.FindCmd([]string{"alias1"})
res, err := cmd.FindCmd([]string{"alias1"}, false)
if err != nil {
t.Fatal("finding alias should work")
}
assert.Equal(t, res.Name, "child1")

res, err = cmd.FindCmd([]string{"alias2"})
res, err = cmd.FindCmd([]string{"alias2"}, false)
if err != nil {
t.Fatal("finding alias should work")
}
assert.Equal(t, res.Name, "child1")

res, err = cmd.FindCmd([]string{"alias3"})
res, err = cmd.FindCmd([]string{"alias3"}, false)
if err == nil {
t.Fatal("should not find this child!")
}
assert.Nil(t, res)
}

func TestFindCmdPrefix(t *testing.T) {
cmd := newCmd("root", "")
cmd.AddCmd(newCmd("cmdone", ""))
cmd.AddCmd(newCmd("cmdtwo", ""))

res, err := cmd.FindCmd([]string{"cmdo"}, true)
if err != nil {
t.Fatal("finding should work")
}
assert.Equal(t, res.Name, "cmdone")

res, err = cmd.FindCmd([]string{"cmdt"}, true)
if err != nil {
t.Fatal("finding should work")
}
assert.Equal(t, res.Name, "cmdtwo")

res, err = cmd.FindCmd([]string{"c"}, true)
if err == nil {
t.Fatal("should not find this child!")
}
assert.Nil(t, res)

res, err = cmd.FindCmd([]string{"cmd"}, true)
if err == nil {
t.Fatal("should not find this child!")
}
Expand Down
9 changes: 5 additions & 4 deletions completer.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ package ishell
import (
"strings"

"github.com/flynn-archive/go-shlex"
shlex "github.com/flynn-archive/go-shlex"
)

type iCompleter struct {
cmd *Cmd
disabled func() bool
cmd *Cmd
disabled func() bool
partialMatch bool
}

func (ic iCompleter) Do(line []rune, pos int) (newLine [][]rune, length int) {
Expand Down Expand Up @@ -45,7 +46,7 @@ func (ic iCompleter) Do(line []rune, pos int) (newLine [][]rune, length int) {
}

func (ic iCompleter) getWords(w []string) (s []string) {
cmd, args := ic.cmd.FindCmd(w)
cmd, args := ic.cmd.FindCmd(w, ic.partialMatch)
if cmd == nil {
cmd, args = ic.cmd, w
}
Expand Down
3 changes: 3 additions & 0 deletions example/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ import (
func main() {
shell := ishell.New()

// allow commands' partial match (prefix)
shell.PartialMatch(true)

// display info.
shell.Println("Sample Interactive Shell")

Expand Down
19 changes: 17 additions & 2 deletions ishell.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ type Shell struct {
active bool
activeMutex sync.RWMutex
ignoreCase bool
partialMatch bool
customCompleter bool
multiChoiceActive bool
haltChan chan struct{}
Expand Down Expand Up @@ -265,7 +266,7 @@ func (s *Shell) handleCommand(str []string) (bool, error) {
str[i] = strings.ToLower(str[i])
}
}
cmd, args := s.rootCmd.FindCmd(str)
cmd, args := s.rootCmd.FindCmd(str, s.partialMatch)
if cmd == nil {
return false, nil
}
Expand Down Expand Up @@ -358,7 +359,14 @@ func (s *Shell) readMultiLinesFunc(f func(string) bool) (string, error) {
}

func (s *Shell) initCompleters() {
s.setCompleter(iCompleter{cmd: s.rootCmd, disabled: func() bool { return s.multiChoiceActive }})
ic := iCompleter{
cmd: s.rootCmd,
disabled: func() bool {
return s.multiChoiceActive
},
partialMatch: s.partialMatch,
}
s.setCompleter(ic)
}

func (s *Shell) setCompleter(completer readline.AutoCompleter) {
Expand Down Expand Up @@ -642,6 +650,13 @@ func (s *Shell) IgnoreCase(ignore bool) {
s.ignoreCase = ignore
}

// PartialMatch specifies whether commands should match partially.
// Defaults to false i.e. commands must exactly match
// If true, unique prefixes should match commands.
func (s *Shell) PartialMatch(partialMatch bool) {
s.partialMatch = partialMatch
}

// ProgressBar returns the progress bar for the shell.
func (s *Shell) ProgressBar() ProgressBar {
return s.progressBar
Expand Down

0 comments on commit fde396e

Please sign in to comment.