Skip to content

Commit fde396e

Browse files
committed
Allow commands' partial match
1 parent 8b8aa74 commit fde396e

File tree

5 files changed

+80
-15
lines changed

5 files changed

+80
-15
lines changed

command.go

+19-3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"bytes"
55
"fmt"
66
"sort"
7+
"strings"
78
"text/tabwriter"
89
)
910

@@ -94,30 +95,45 @@ func (c Cmd) HelpText() string {
9495
}
9596

9697
// findChildCmd returns the subcommand with matching name or alias.
97-
func (c *Cmd) findChildCmd(name string) *Cmd {
98+
func (c *Cmd) findChildCmd(name string, partialMatch bool) *Cmd {
9899
// find perfect matches first
99100
if cmd, ok := c.children[name]; ok {
100101
return cmd
101102
}
102103

104+
var prefixes []*Cmd
105+
103106
// find alias matching the name
104107
for _, cmd := range c.children {
108+
if partialMatch && strings.HasPrefix(cmd.Name, name) {
109+
prefixes = append(prefixes, cmd)
110+
}
111+
105112
for _, alias := range cmd.Aliases {
106113
if alias == name {
107114
return cmd
108115
}
116+
117+
if partialMatch && strings.HasPrefix(alias, name) {
118+
prefixes = append(prefixes, cmd)
119+
}
109120
}
110121
}
111122

123+
// allow only unique partial match
124+
if len(prefixes) == 1 {
125+
return prefixes[0]
126+
}
127+
112128
return nil
113129
}
114130

115131
// FindCmd finds the matching Cmd for args.
116132
// It returns the Cmd and the remaining args.
117-
func (c Cmd) FindCmd(args []string) (*Cmd, []string) {
133+
func (c Cmd) FindCmd(args []string, partialMatch bool) (*Cmd, []string) {
118134
var cmd *Cmd
119135
for i, arg := range args {
120-
if cmd1 := c.findChildCmd(arg); cmd1 != nil {
136+
if cmd1 := c.findChildCmd(arg, partialMatch); cmd1 != nil {
121137
cmd = cmd1
122138
c = *cmd
123139
continue

command_test.go

+36-6
Original file line numberDiff line numberDiff line change
@@ -33,19 +33,19 @@ func TestFindCmd(t *testing.T) {
3333
cmd := newCmd("root", "")
3434
cmd.AddCmd(newCmd("child1", ""))
3535
cmd.AddCmd(newCmd("child2", ""))
36-
res, err := cmd.FindCmd([]string{"child1"})
36+
res, err := cmd.FindCmd([]string{"child1"}, false)
3737
if err != nil {
3838
t.Fatal("finding should work")
3939
}
4040
assert.Equal(t, res.Name, "child1")
4141

42-
res, err = cmd.FindCmd([]string{"child2"})
42+
res, err = cmd.FindCmd([]string{"child2"}, false)
4343
if err != nil {
4444
t.Fatal("finding should work")
4545
}
4646
assert.Equal(t, res.Name, "child2")
4747

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

61-
res, err := cmd.FindCmd([]string{"alias1"})
61+
res, err := cmd.FindCmd([]string{"alias1"}, false)
6262
if err != nil {
6363
t.Fatal("finding alias should work")
6464
}
6565
assert.Equal(t, res.Name, "child1")
6666

67-
res, err = cmd.FindCmd([]string{"alias2"})
67+
res, err = cmd.FindCmd([]string{"alias2"}, false)
6868
if err != nil {
6969
t.Fatal("finding alias should work")
7070
}
7171
assert.Equal(t, res.Name, "child1")
7272

73-
res, err = cmd.FindCmd([]string{"alias3"})
73+
res, err = cmd.FindCmd([]string{"alias3"}, false)
74+
if err == nil {
75+
t.Fatal("should not find this child!")
76+
}
77+
assert.Nil(t, res)
78+
}
79+
80+
func TestFindCmdPrefix(t *testing.T) {
81+
cmd := newCmd("root", "")
82+
cmd.AddCmd(newCmd("cmdone", ""))
83+
cmd.AddCmd(newCmd("cmdtwo", ""))
84+
85+
res, err := cmd.FindCmd([]string{"cmdo"}, true)
86+
if err != nil {
87+
t.Fatal("finding should work")
88+
}
89+
assert.Equal(t, res.Name, "cmdone")
90+
91+
res, err = cmd.FindCmd([]string{"cmdt"}, true)
92+
if err != nil {
93+
t.Fatal("finding should work")
94+
}
95+
assert.Equal(t, res.Name, "cmdtwo")
96+
97+
res, err = cmd.FindCmd([]string{"c"}, true)
98+
if err == nil {
99+
t.Fatal("should not find this child!")
100+
}
101+
assert.Nil(t, res)
102+
103+
res, err = cmd.FindCmd([]string{"cmd"}, true)
74104
if err == nil {
75105
t.Fatal("should not find this child!")
76106
}

completer.go

+5-4
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@ package ishell
33
import (
44
"strings"
55

6-
"github.com/flynn-archive/go-shlex"
6+
shlex "github.com/flynn-archive/go-shlex"
77
)
88

99
type iCompleter struct {
10-
cmd *Cmd
11-
disabled func() bool
10+
cmd *Cmd
11+
disabled func() bool
12+
partialMatch bool
1213
}
1314

1415
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) {
4546
}
4647

4748
func (ic iCompleter) getWords(w []string) (s []string) {
48-
cmd, args := ic.cmd.FindCmd(w)
49+
cmd, args := ic.cmd.FindCmd(w, ic.partialMatch)
4950
if cmd == nil {
5051
cmd, args = ic.cmd, w
5152
}

example/main.go

+3
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ import (
1414
func main() {
1515
shell := ishell.New()
1616

17+
// allow commands' partial match (prefix)
18+
shell.PartialMatch(true)
19+
1720
// display info.
1821
shell.Println("Sample Interactive Shell")
1922

ishell.go

+17-2
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ type Shell struct {
4949
active bool
5050
activeMutex sync.RWMutex
5151
ignoreCase bool
52+
partialMatch bool
5253
customCompleter bool
5354
multiChoiceActive bool
5455
haltChan chan struct{}
@@ -265,7 +266,7 @@ func (s *Shell) handleCommand(str []string) (bool, error) {
265266
str[i] = strings.ToLower(str[i])
266267
}
267268
}
268-
cmd, args := s.rootCmd.FindCmd(str)
269+
cmd, args := s.rootCmd.FindCmd(str, s.partialMatch)
269270
if cmd == nil {
270271
return false, nil
271272
}
@@ -358,7 +359,14 @@ func (s *Shell) readMultiLinesFunc(f func(string) bool) (string, error) {
358359
}
359360

360361
func (s *Shell) initCompleters() {
361-
s.setCompleter(iCompleter{cmd: s.rootCmd, disabled: func() bool { return s.multiChoiceActive }})
362+
ic := iCompleter{
363+
cmd: s.rootCmd,
364+
disabled: func() bool {
365+
return s.multiChoiceActive
366+
},
367+
partialMatch: s.partialMatch,
368+
}
369+
s.setCompleter(ic)
362370
}
363371

364372
func (s *Shell) setCompleter(completer readline.AutoCompleter) {
@@ -642,6 +650,13 @@ func (s *Shell) IgnoreCase(ignore bool) {
642650
s.ignoreCase = ignore
643651
}
644652

653+
// PartialMatch specifies whether commands should match partially.
654+
// Defaults to false i.e. commands must exactly match
655+
// If true, unique prefixes should match commands.
656+
func (s *Shell) PartialMatch(partialMatch bool) {
657+
s.partialMatch = partialMatch
658+
}
659+
645660
// ProgressBar returns the progress bar for the shell.
646661
func (s *Shell) ProgressBar() ProgressBar {
647662
return s.progressBar

0 commit comments

Comments
 (0)