Skip to content

Allow commands' partial match #108

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

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
@@ -4,6 +4,7 @@ import (
"bytes"
"fmt"
"sort"
"strings"
"text/tabwriter"
)

@@ -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
42 changes: 36 additions & 6 deletions command_test.go
Original file line number Diff line number Diff line change
@@ -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!")
}
@@ -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!")
}
9 changes: 5 additions & 4 deletions completer.go
Original file line number Diff line number Diff line change
@@ -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) {
@@ -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
}
3 changes: 3 additions & 0 deletions example/main.go
Original file line number Diff line number Diff line change
@@ -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")

19 changes: 17 additions & 2 deletions ishell.go
Original file line number Diff line number Diff line change
@@ -49,6 +49,7 @@ type Shell struct {
active bool
activeMutex sync.RWMutex
ignoreCase bool
partialMatch bool
customCompleter bool
multiChoiceActive bool
haltChan chan struct{}
@@ -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
}
@@ -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) {
@@ -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