From fde396e57802ebc1d7d52dabf2170cd603d9b039 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=AAnis=20Volpato=20Martins?= Date: Tue, 29 Jan 2019 11:54:49 -0200 Subject: [PATCH] Allow commands' partial match --- command.go | 22 +++++++++++++++++++--- command_test.go | 42 ++++++++++++++++++++++++++++++++++++------ completer.go | 9 +++++---- example/main.go | 3 +++ ishell.go | 19 +++++++++++++++++-- 5 files changed, 80 insertions(+), 15 deletions(-) diff --git a/command.go b/command.go index 4ede7e6..379c5e3 100644 --- a/command.go +++ b/command.go @@ -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 diff --git a/command_test.go b/command_test.go index 27c89a0..e619aa9 100644 --- a/command_test.go +++ b/command_test.go @@ -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!") } diff --git a/completer.go b/completer.go index 2071af0..cd11c8d 100644 --- a/completer.go +++ b/completer.go @@ -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 } diff --git a/example/main.go b/example/main.go index fcb1822..2761f73 100644 --- a/example/main.go +++ b/example/main.go @@ -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") diff --git a/ishell.go b/ishell.go index d3f4b42..fc43ef2 100644 --- a/ishell.go +++ b/ishell.go @@ -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