Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow commands' partial match #108

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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