summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEyal Posener <[email protected]>2017-05-11 20:54:26 +0300
committerGitHub <[email protected]>2017-05-11 20:54:26 +0300
commitd3bbb859d52b45987e3cd2098e28423f32edd999 (patch)
tree4265893d0c665ba0e763482a70c9044fa983a1ed
parentdd2171d085ef5957a1c5c0794d6007822e47849b (diff)
parentba23c350c73d2dfdf071c14c22152bcaf7e7fd7b (diff)
Merge pull request #12 from posener/improves
Enhance program structure and data structures
-rw-r--r--args.go39
-rw-r--r--command.go77
-rw-r--r--complete.go31
-rw-r--r--complete_test.go12
-rw-r--r--gocomplete/complete.go8
-rw-r--r--gocomplete/tests.go15
-rw-r--r--match/file.go22
-rw-r--r--match/match.go13
-rw-r--r--match/match_test.go26
-rw-r--r--match/prefix.go10
-rw-r--r--predict.go (renamed from predicate.go)92
-rw-r--r--predict_test.go (renamed from predicate_test.go)27
12 files changed, 186 insertions, 186 deletions
diff --git a/args.go b/args.go
new file mode 100644
index 0000000..86dd41a
--- /dev/null
+++ b/args.go
@@ -0,0 +1,39 @@
+package complete
+
+// Args describes command line arguments
+type Args struct {
+ All []string
+ Completed []string
+ Last string
+ LastCompleted string
+}
+
+func newArgs(line []string) Args {
+ completed := removeLast(line)
+ return Args{
+ All: line[1:],
+ Completed: completed,
+ Last: last(line),
+ LastCompleted: last(completed),
+ }
+}
+
+func (a Args) from(i int) Args {
+ a.All = a.All[i:]
+ a.Completed = a.Completed[i:]
+ return a
+}
+
+func removeLast(a []string) []string {
+ if len(a) > 0 {
+ return a[:len(a)-1]
+ }
+ return a
+}
+
+func last(args []string) (last string) {
+ if len(args) > 0 {
+ last = args[len(args)-1]
+ }
+ return
+}
diff --git a/command.go b/command.go
index f52d175..80b2f99 100644
--- a/command.go
+++ b/command.go
@@ -3,7 +3,7 @@ package complete
import "github.com/posener/complete/match"
// Command represents a command line
-// It holds the data that enables auto completion of a given typed command line
+// It holds the data that enables auto completion of command line
// Command can also be a sub command.
type Command struct {
// Sub is map of sub commands of the current command
@@ -12,88 +12,79 @@ type Command struct {
Sub Commands
// Flags is a map of flags that the command accepts.
- // The key is the flag name, and the value is it's prediction options.
+ // The key is the flag name, and the value is it's predictions.
Flags Flags
// Args are extra arguments that the command accepts, those who are
// given without any flag before.
- Args Predicate
+ Args Predictor
}
// Commands is the type of Sub member, it maps a command name to a command struct
type Commands map[string]Command
-// Flags is the type Flags of the Flags member, it maps a flag name to the flag
-// prediction options.
-type Flags map[string]Predicate
+// Flags is the type Flags of the Flags member, it maps a flag name to the flag predictions.
+type Flags map[string]Predictor
-// options returns all available complete options for the given command
-// args are all except the last command line arguments relevant to the command
-func (c *Command) options(args []string) (options []match.Matcher, only bool) {
+// Predict returns all possible predictions for args according to the command struct
+func (c *Command) Predict(a Args) (predictions []string) {
+ predictions, _ = c.predict(a)
+ return
+}
+
+func (c *Command) predict(a Args) (options []string, only bool) {
- // remove the first argument, which is the command name
- args = args[1:]
- wordCurrent := last(args)
- wordCompleted := last(removeLast(args))
// if wordCompleted has something that needs to follow it,
// it is the most relevant completion
- if predicate, ok := c.Flags[wordCompleted]; ok && predicate != nil {
- Log("Predicting according to flag %s", wordCurrent)
- return predicate.predict(wordCurrent), true
+ if predictor, ok := c.Flags[a.LastCompleted]; ok && predictor != nil {
+ Log("Predicting according to flag %s", a.Last)
+ return predictor.Predict(a), true
}
- sub, options, only := c.searchSub(args)
+ sub, options, only := c.searchSub(a)
if only {
return
}
- // if no subcommand was entered in any of the args, add the
- // subcommands as complete options.
+ // if no sub command was found, return a list of the sub commands
if sub == "" {
- options = append(options, c.subCommands()...)
+ options = append(options, c.subCommands(a.Last)...)
}
// add global available complete options
for flag := range c.Flags {
- options = append(options, match.Prefix(flag))
+ if match.Prefix(flag, a.Last) {
+ options = append(options, flag)
+ }
}
// add additional expected argument of the command
- options = append(options, c.Args.predict(wordCurrent)...)
+ if c.Args != nil {
+ options = append(options, c.Args.Predict(a)...)
+ }
return
}
// searchSub searches recursively within sub commands if the sub command appear
// in the on of the arguments.
-func (c *Command) searchSub(args []string) (sub string, all []match.Matcher, only bool) {
-
- // search for sub command in all arguments except the last one
- // because that one might not be completed yet
- searchArgs := removeLast(args)
-
- for i, arg := range searchArgs {
+func (c *Command) searchSub(a Args) (sub string, all []string, only bool) {
+ for i, arg := range a.Completed {
if cmd, ok := c.Sub[arg]; ok {
sub = arg
- all, only = cmd.options(args[i:])
+ all, only = cmd.predict(a.from(i))
return
}
}
- return "", nil, false
+ return
}
-// suvCommands returns a list of matchers according to the sub command names
-func (c *Command) subCommands() []match.Matcher {
- subs := make([]match.Matcher, 0, len(c.Sub))
+// subCommands returns a list of matching sub commands
+func (c *Command) subCommands(last string) (prediction []string) {
for sub := range c.Sub {
- subs = append(subs, match.Prefix(sub))
- }
- return subs
-}
-
-func removeLast(a []string) []string {
- if len(a) > 0 {
- return a[:len(a)-1]
+ if match.Prefix(sub, last) {
+ prediction = append(prediction, sub)
+ }
}
- return a
+ return
}
diff --git a/complete.go b/complete.go
index c91bf5f..be0876e 100644
--- a/complete.go
+++ b/complete.go
@@ -41,37 +41,23 @@ func New(name string, command Command) *Complete {
// returns success if the completion ran or if the cli matched
// any of the given flags, false otherwise
func (c *Complete) Run() bool {
- args, ok := getLine()
+ line, ok := getLine()
if !ok {
// make sure flags parsed,
// in case they were not added in the main program
return c.CLI.Run()
}
- Log("Completing args: %s", args)
+ Log("Completing line: %s", line)
- options := complete(c.Command, args)
+ a := newArgs(line)
+
+ options := c.Command.Predict(a)
Log("Completion: %s", options)
output(options)
return true
}
-// complete get a command an command line arguments and returns
-// matching completion options
-func complete(c Command, args []string) (matching []string) {
- options, _ := c.options(args)
-
- // choose only matching options
- l := last(args)
- for _, option := range options {
- Log("option %T, %s -> %t", option, option, option.Match(l))
- if option.Match(l) {
- matching = append(matching, option.String())
- }
- }
- return
-}
-
func getLine() ([]string, bool) {
line := os.Getenv(envComplete)
if line == "" {
@@ -80,13 +66,6 @@ func getLine() ([]string, bool) {
return strings.Split(line, " "), true
}
-func last(args []string) (last string) {
- if len(args) > 0 {
- last = args[len(args)-1]
- }
- return
-}
-
func output(options []string) {
Log("")
// stdout of program defines the complete options
diff --git a/complete_test.go b/complete_test.go
index 282a2f6..ee5a133 100644
--- a/complete_test.go
+++ b/complete_test.go
@@ -13,20 +13,20 @@ func TestCompleter_Complete(t *testing.T) {
c := Command{
Sub: map[string]Command{
"sub1": {
- Flags: map[string]Predicate{
+ Flags: map[string]Predictor{
"-flag1": PredictAnything,
"-flag2": PredictNothing,
},
},
"sub2": {
- Flags: map[string]Predicate{
+ Flags: map[string]Predictor{
"-flag2": PredictNothing,
"-flag3": PredictSet("opt1", "opt2", "opt12"),
},
- Args: Predicate(PredictDirs("*")).Or(PredictFiles("*.md")),
+ Args: PredictOr(PredictDirs("*"), PredictFiles("*.md")),
},
},
- Flags: map[string]Predicate{
+ Flags: map[string]Predictor{
"-h": PredictNothing,
"-global1": PredictAnything,
"-o": PredictFiles("*.txt"),
@@ -174,9 +174,9 @@ func TestCompleter_Complete(t *testing.T) {
tt.args = "cmd " + tt.args
os.Setenv(envComplete, tt.args)
- args, _ := getLine()
+ line, _ := getLine()
- got := complete(c, args)
+ got := c.Predict(newArgs(line))
sort.Strings(tt.want)
sort.Strings(got)
diff --git a/gocomplete/complete.go b/gocomplete/complete.go
index 75d3672..1575e1b 100644
--- a/gocomplete/complete.go
+++ b/gocomplete/complete.go
@@ -6,9 +6,11 @@ import "github.com/posener/complete"
var (
predictEllipsis = complete.PredictSet("./...")
- goFilesOrPackages = complete.PredictFiles("*.go").
- Or(complete.PredictDirs("*")).
- Or(predictEllipsis)
+ goFilesOrPackages = complete.PredictOr(
+ complete.PredictFiles("*.go"),
+ complete.PredictDirs("*"),
+ predictEllipsis,
+ )
)
func main() {
diff --git a/gocomplete/tests.go b/gocomplete/tests.go
index 40210d4..d2c32e7 100644
--- a/gocomplete/tests.go
+++ b/gocomplete/tests.go
@@ -17,15 +17,16 @@ import (
// and then all the relevant function names.
// for test names use prefix of 'Test' or 'Example', and for benchmark
// test names use 'Benchmark'
-func predictTest(funcPrefix ...string) complete.Predicate {
- return func(last string) []match.Matcher {
+func predictTest(funcPrefix ...string) complete.Predictor {
+ return complete.PredictFunc(func(a complete.Args) (prediction []string) {
tests := testNames(funcPrefix)
- options := make([]match.Matcher, len(tests))
- for i := range tests {
- options[i] = match.Prefix(tests[i])
+ for _, t := range tests {
+ if match.Prefix(t, a.Last) {
+ prediction = append(prediction, t)
+ }
}
- return options
- }
+ return
+ })
}
// get all test names in current directory
diff --git a/match/file.go b/match/file.go
index 0b554ce..eee5bec 100644
--- a/match/file.go
+++ b/match/file.go
@@ -1,26 +1,16 @@
package match
-import (
- "strings"
-)
+import "strings"
-// File is a file name Matcher, if the last word can prefix the
-// File path, there is a possible match
-type File string
-
-func (a File) String() string {
- return string(a)
-}
-
-// Match returns true if prefix's abs path prefixes a's abs path
-func (a File) Match(prefix string) bool {
+// File returns true if prefix can match the file
+func File(file, prefix string) bool {
// special case for current directory completion
- if a == "./" && (prefix == "." || prefix == "") {
+ if file == "./" && (prefix == "." || prefix == "") {
return true
}
- cmp := strings.TrimPrefix(string(a), "./")
+ file = strings.TrimPrefix(file, "./")
prefix = strings.TrimPrefix(prefix, "./")
- return strings.HasPrefix(cmp, prefix)
+ return strings.HasPrefix(file, prefix)
}
diff --git a/match/match.go b/match/match.go
index ae95549..812fcac 100644
--- a/match/match.go
+++ b/match/match.go
@@ -1,11 +1,6 @@
package match
-import "fmt"
-
-// Matcher matches itself to a string
-// it is used for comparing a given argument to the last typed
-// word, and see if it is a possible auto complete option.
-type Matcher interface {
- fmt.Stringer
- Match(prefix string) bool
-}
+// Match matches two strings
+// it is used for comparing a term to the last typed
+// word, the prefix, and see if it is a possible auto complete option.
+type Match func(term, prefix string) bool
diff --git a/match/match_test.go b/match/match_test.go
index d7a851a..b5a0d87 100644
--- a/match/match_test.go
+++ b/match/match_test.go
@@ -1,6 +1,7 @@
package match
import (
+ "fmt"
"os"
"testing"
)
@@ -21,11 +22,13 @@ func TestMatch(t *testing.T) {
}
tests := []struct {
- m Matcher
+ m Match
+ long string
tests []matcherTest
}{
{
- m: Prefix("abcd"),
+ m: Prefix,
+ long: "abcd",
tests: []matcherTest{
{prefix: "", want: true},
{prefix: "ab", want: true},
@@ -33,14 +36,16 @@ func TestMatch(t *testing.T) {
},
},
{
- m: Prefix(""),
+ m: Prefix,
+ long: "",
tests: []matcherTest{
{prefix: "ac", want: false},
{prefix: "", want: true},
},
},
{
- m: File("file.txt"),
+ m: File,
+ long: "file.txt",
tests: []matcherTest{
{prefix: "", want: true},
{prefix: "f", want: true},
@@ -59,7 +64,8 @@ func TestMatch(t *testing.T) {
},
},
{
- m: File("./file.txt"),
+ m: File,
+ long: "./file.txt",
tests: []matcherTest{
{prefix: "", want: true},
{prefix: "f", want: true},
@@ -78,7 +84,8 @@ func TestMatch(t *testing.T) {
},
},
{
- m: File("/file.txt"),
+ m: File,
+ long: "/file.txt",
tests: []matcherTest{
{prefix: "", want: true},
{prefix: "f", want: false},
@@ -97,7 +104,8 @@ func TestMatch(t *testing.T) {
},
},
{
- m: File("./"),
+ m: File,
+ long: "./",
tests: []matcherTest{
{prefix: "", want: true},
{prefix: ".", want: true},
@@ -109,9 +117,9 @@ func TestMatch(t *testing.T) {
for _, tt := range tests {
for _, ttt := range tt.tests {
- name := "matcher='" + tt.m.String() + "'&prefix='" + ttt.prefix + "'"
+ name := fmt.Sprintf("matcher=%T&long='%s'&prefix='%s'", tt.m, tt.long, ttt.prefix)
t.Run(name, func(t *testing.T) {
- got := tt.m.Match(ttt.prefix)
+ got := tt.m(tt.long, ttt.prefix)
if got != ttt.want {
t.Errorf("Failed %s: got = %t, want: %t", name, got, ttt.want)
}
diff --git a/match/prefix.go b/match/prefix.go
index d54902d..9a01ba6 100644
--- a/match/prefix.go
+++ b/match/prefix.go
@@ -3,13 +3,7 @@ package match
import "strings"
// Prefix is a simple Matcher, if the word is it's prefix, there is a match
-type Prefix string
-
-func (a Prefix) String() string {
- return string(a)
-}
-
// Match returns true if a has the prefix as prefix
-func (a Prefix) Match(prefix string) bool {
- return strings.HasPrefix(string(a), prefix)
+func Prefix(long, prefix string) bool {
+ return strings.HasPrefix(long, prefix)
}
diff --git a/predicate.go b/predict.go
index bb7e8cb..9e1cce9 100644
--- a/predicate.go
+++ b/predict.go
@@ -7,52 +7,66 @@ import (
"github.com/posener/complete/match"
)
-// Predicate determines what terms can follow a command or a flag
-// It is used for auto completion, given last - the last word in the already
-// in the command line, what words can complete it.
-type Predicate func(last string) []match.Matcher
+// Predictor implements a predict method, in which given
+// command line arguments returns a list of options it predicts.
+type Predictor interface {
+ Predict(Args) []string
+}
-// Or unions two predicate functions, so that the result predicate
+// PredictOr unions two predicate functions, so that the result predicate
// returns the union of their predication
-func (p Predicate) Or(other Predicate) Predicate {
- if p == nil {
- return other
- }
- if other == nil {
- return p
- }
- return func(last string) []match.Matcher { return append(p.predict(last), other.predict(last)...) }
+func PredictOr(predictors ...Predictor) Predictor {
+ return PredictFunc(func(a Args) (prediction []string) {
+ for _, p := range predictors {
+ if p == nil {
+ continue
+ }
+ prediction = append(prediction, p.Predict(a)...)
+ }
+ return
+ })
}
-func (p Predicate) predict(last string) []match.Matcher {
+// PredictFunc determines what terms can follow a command or a flag
+// It is used for auto completion, given last - the last word in the already
+// in the command line, what words can complete it.
+type PredictFunc func(Args) []string
+
+// Predict invokes the predict function and implements the Predictor interface
+func (p PredictFunc) Predict(a Args) []string {
if p == nil {
return nil
}
- return p(last)
+ return p(a)
}
// PredictNothing does not expect anything after.
-var PredictNothing Predicate
+var PredictNothing Predictor
// PredictAnything expects something, but nothing particular, such as a number
// or arbitrary name.
-func PredictAnything(last string) []match.Matcher { return nil }
+var PredictAnything = PredictFunc(func(Args) []string { return nil })
// PredictSet expects specific set of terms, given in the options argument.
-func PredictSet(options ...string) Predicate {
- return func(last string) []match.Matcher {
- ret := make([]match.Matcher, len(options))
- for i := range options {
- ret[i] = match.Prefix(options[i])
+func PredictSet(options ...string) Predictor {
+ return predictSet(options)
+}
+
+type predictSet []string
+
+func (p predictSet) Predict(a Args) (prediction []string) {
+ for _, m := range p {
+ if match.Prefix(m, a.Last) {
+ prediction = append(prediction, m)
}
- return ret
}
+ return
}
// PredictDirs will search for directories in the given started to be typed
// path, if no path was started to be typed, it will complete to directories
// in the current working directory.
-func PredictDirs(pattern string) Predicate {
+func PredictDirs(pattern string) Predictor {
return files(pattern, true, false)
}
@@ -60,19 +74,19 @@ func PredictDirs(pattern string) Predicate {
// be typed path, if no path was started to be typed, it will complete to files that
// match the pattern in the current working directory.
// To match any file, use "*" as pattern. To match go files use "*.go", and so on.
-func PredictFiles(pattern string) Predicate {
+func PredictFiles(pattern string) Predictor {
return files(pattern, false, true)
}
-// PredictFilesOrDirs predict any file or directory that matches the pattern
-func PredictFilesOrDirs(pattern string) Predicate {
+// PredictFilesOrDirs any file or directory that matches the pattern
+func PredictFilesOrDirs(pattern string) Predictor {
return files(pattern, true, true)
}
-func files(pattern string, allowDirs, allowFiles bool) Predicate {
- return func(last string) []match.Matcher {
- dir := dirFromLast(last)
- Log("looking for files in %s (last=%s)", dir, last)
+func files(pattern string, allowDirs, allowFiles bool) PredictFunc {
+ return func(a Args) (prediction []string) {
+ dir := dirFromLast(a.Last)
+ Log("looking for files in %s (last=%s)", dir, a.Last)
files, err := filepath.Glob(filepath.Join(dir, pattern))
if err != nil {
Log("failed glob operation with pattern '%s': %s", pattern, err)
@@ -84,7 +98,13 @@ func files(pattern string, allowDirs, allowFiles bool) Predicate {
if !filepath.IsAbs(pattern) {
filesToRel(files)
}
- return filesToMatchers(files)
+ // add all matching files to prediction
+ for _, f := range files {
+ if match.File(f, a.Last) {
+ prediction = append(prediction, f)
+ }
+ }
+ return
}
}
@@ -130,14 +150,6 @@ func filesToRel(files []string) {
return
}
-func filesToMatchers(files []string) []match.Matcher {
- options := make([]match.Matcher, len(files))
- for i, f := range files {
- options[i] = match.File(f)
- }
- return options
-}
-
// dirFromLast gives the directory of the current written
// last argument if it represents a file name being written.
// in case that it is not, we fall back to the current directory.
diff --git a/predicate_test.go b/predict_test.go
index 1a694a1..6b77fbe 100644
--- a/predicate_test.go
+++ b/predict_test.go
@@ -12,7 +12,7 @@ func TestPredicate(t *testing.T) {
tests := []struct {
name string
- p Predicate
+ p Predictor
arg string
want []string
}{
@@ -38,28 +38,23 @@ func TestPredicate(t *testing.T) {
want: []string{},
},
{
- name: "nothing",
- p: PredictNothing,
- want: []string{},
- },
- {
name: "or: word with nil",
- p: PredictSet("a").Or(PredictNothing),
+ p: PredictOr(PredictSet("a"), nil),
want: []string{"a"},
},
{
name: "or: nil with word",
- p: PredictNothing.Or(PredictSet("a")),
+ p: PredictOr(nil, PredictSet("a")),
want: []string{"a"},
},
{
name: "or: nil with nil",
- p: PredictNothing.Or(PredictNothing),
+ p: PredictOr(PredictNothing, PredictNothing),
want: []string{},
},
{
name: "or: word with word with word",
- p: PredictSet("a").Or(PredictSet("b")).Or(PredictSet("c")),
+ p: PredictOr(PredictSet("a"), PredictSet("b"), PredictSet("c")),
want: []string{"a", "b", "c"},
},
{
@@ -118,18 +113,12 @@ func TestPredicate(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name+"?arg='"+tt.arg+"'", func(t *testing.T) {
- matchers := tt.p.predict(tt.arg)
+ matches := tt.p.Predict(newArgs(strings.Split(tt.arg, " ")))
- matchersString := []string{}
- for _, m := range matchers {
- if m.Match(tt.arg) {
- matchersString = append(matchersString, m.String())
- }
- }
- sort.Strings(matchersString)
+ sort.Strings(matches)
sort.Strings(tt.want)
- got := strings.Join(matchersString, ",")
+ got := strings.Join(matches, ",")
want := strings.Join(tt.want, ",")
if got != want {