summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--command.go26
-rw-r--r--complete.go4
-rw-r--r--complete_test.go10
-rw-r--r--gocomplete/complete.go16
-rw-r--r--match/file.go18
-rw-r--r--match/match_test.go19
-rw-r--r--predicate.go60
-rw-r--r--predicate_test.go38
-rw-r--r--tests/.dot.txt0
9 files changed, 128 insertions, 63 deletions
diff --git a/command.go b/command.go
index f64e225..f52d175 100644
--- a/command.go
+++ b/command.go
@@ -33,11 +33,13 @@ func (c *Command) options(args []string) (options []match.Matcher, only bool) {
// remove the first argument, which is the command name
args = args[1:]
- last := last(args)
- // if prev has something that needs to follow it,
+ 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[last]; ok && predicate != nil {
- return predicate.predict(last), true
+ if predicate, ok := c.Flags[wordCompleted]; ok && predicate != nil {
+ Log("Predicting according to flag %s", wordCurrent)
+ return predicate.predict(wordCurrent), true
}
sub, options, only := c.searchSub(args)
@@ -57,7 +59,7 @@ func (c *Command) options(args []string) (options []match.Matcher, only bool) {
}
// add additional expected argument of the command
- options = append(options, c.Args.predict(last)...)
+ options = append(options, c.Args.predict(wordCurrent)...)
return
}
@@ -65,7 +67,12 @@ func (c *Command) options(args []string) (options []match.Matcher, only bool) {
// 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) {
- for i, arg := range args {
+
+ // 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 {
if cmd, ok := c.Sub[arg]; ok {
sub = arg
all, only = cmd.options(args[i:])
@@ -83,3 +90,10 @@ func (c *Command) subCommands() []match.Matcher {
}
return subs
}
+
+func removeLast(a []string) []string {
+ if len(a) > 0 {
+ return a[:len(a)-1]
+ }
+ return a
+}
diff --git a/complete.go b/complete.go
index 925c9a2..c91bf5f 100644
--- a/complete.go
+++ b/complete.go
@@ -59,11 +59,12 @@ func (c *Complete) Run() bool {
// 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[:len(args)-1])
+ 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())
}
@@ -87,6 +88,7 @@ func last(args []string) (last string) {
}
func output(options []string) {
+ Log("")
// stdout of program defines the complete options
for _, option := range options {
fmt.Println(option)
diff --git a/complete_test.go b/complete_test.go
index 147a361..282a2f6 100644
--- a/complete_test.go
+++ b/complete_test.go
@@ -23,7 +23,7 @@ func TestCompleter_Complete(t *testing.T) {
"-flag2": PredictNothing,
"-flag3": PredictSet("opt1", "opt2", "opt12"),
},
- Args: Predicate(PredictDirs).Or(PredictFiles("*.md")),
+ Args: Predicate(PredictDirs("*")).Or(PredictFiles("*.md")),
},
},
Flags: map[string]Predicate{
@@ -41,7 +41,7 @@ func TestCompleter_Complete(t *testing.T) {
allGlobals = append(allGlobals, flag)
}
- testTXTFiles := []string{"./a.txt", "./b.txt", "./c.txt"}
+ testTXTFiles := []string{"./a.txt", "./b.txt", "./c.txt", "./.dot.txt"}
tests := []struct {
args string
@@ -81,11 +81,11 @@ func TestCompleter_Complete(t *testing.T) {
},
{
args: "sub2 ",
- want: []string{"./", "./dir", "./readme.md", "-flag2", "-flag3", "-h", "-global1", "-o"},
+ want: []string{"./", "./dir/", "./readme.md", "-flag2", "-flag3", "-h", "-global1", "-o"},
},
{
args: "sub2 ./",
- want: []string{"./", "./readme.md", "./dir"},
+ want: []string{"./", "./readme.md", "./dir/"},
},
{
args: "sub2 re",
@@ -93,7 +93,7 @@ func TestCompleter_Complete(t *testing.T) {
},
{
args: "sub2 -flag2 ",
- want: []string{"./", "./dir", "./readme.md", "-flag2", "-flag3", "-h", "-global1", "-o"},
+ want: []string{"./", "./dir/", "./readme.md", "-flag2", "-flag3", "-h", "-global1", "-o"},
},
{
args: "sub1 -fl",
diff --git a/gocomplete/complete.go b/gocomplete/complete.go
index ac5f5ed..75d3672 100644
--- a/gocomplete/complete.go
+++ b/gocomplete/complete.go
@@ -7,7 +7,7 @@ var (
predictEllipsis = complete.PredictSet("./...")
goFilesOrPackages = complete.PredictFiles("*.go").
- Or(complete.PredictDirs).
+ Or(complete.PredictDirs("*")).
Or(predictEllipsis)
)
@@ -33,7 +33,7 @@ func main() {
"-installsuffix": complete.PredictAnything,
"-ldflags": complete.PredictAnything,
"-linkshared": complete.PredictNothing,
- "-pkgdir": complete.PredictDirs,
+ "-pkgdir": complete.PredictDirs("*"),
"-tags": complete.PredictAnything,
"-toolexec": complete.PredictAnything,
},
@@ -58,7 +58,7 @@ func main() {
"-count": complete.PredictAnything,
"-cover": complete.PredictNothing,
"-covermode": complete.PredictSet("set", "count", "atomic"),
- "-coverpkg": complete.PredictDirs,
+ "-coverpkg": complete.PredictDirs("*"),
"-cpu": complete.PredictAnything,
"-run": predictTest("Test", "Example"),
"-short": complete.PredictNothing,
@@ -73,7 +73,7 @@ func main() {
"-memprofilerate": complete.PredictAnything,
"-mutexprofile": complete.PredictFiles("*.out"),
"-mutexprofilefraction": complete.PredictAnything,
- "-outputdir": complete.PredictDirs,
+ "-outputdir": complete.PredictDirs("*"),
"-trace": complete.PredictFiles("*.out"),
},
Args: goFilesOrPackages,
@@ -114,7 +114,7 @@ func main() {
"-n": complete.PredictNothing,
"-x": complete.PredictNothing,
},
- Args: complete.PredictDirs,
+ Args: complete.PredictDirs("*"),
}
list := complete.Command{
@@ -123,7 +123,7 @@ func main() {
"-f": complete.PredictAnything,
"-json": complete.PredictNothing,
},
- Args: complete.PredictDirs,
+ Args: complete.PredictDirs("*"),
}
tool := complete.Command{
@@ -140,7 +140,7 @@ func main() {
"-n": complete.PredictNothing,
"-x": complete.PredictNothing,
},
- Args: complete.PredictDirs,
+ Args: complete.PredictDirs("*"),
}
env := complete.Command{
@@ -151,7 +151,7 @@ func main() {
version := complete.Command{}
fix := complete.Command{
- Args: complete.PredictDirs,
+ Args: complete.PredictDirs("*"),
}
// commands that also accepts the build flags
diff --git a/match/file.go b/match/file.go
index c972ce0..0b554ce 100644
--- a/match/file.go
+++ b/match/file.go
@@ -1,7 +1,6 @@
package match
import (
- "path/filepath"
"strings"
)
@@ -15,16 +14,13 @@ func (a File) String() string {
// Match returns true if prefix's abs path prefixes a's abs path
func (a File) Match(prefix string) bool {
- full, err := filepath.Abs(string(a))
- if err != nil {
- return false
- }
- prefixFull, err := filepath.Abs(prefix)
- if err != nil {
- return false
+
+ // special case for current directory completion
+ if a == "./" && (prefix == "." || prefix == "") {
+ return true
}
- // if the file has the prefix as prefix,
- // but we don't want to show too many files, so, if it is in a deeper directory - omit it.
- return strings.HasPrefix(full, prefixFull) && (full == prefixFull || !strings.Contains(full[len(prefixFull)+1:], "/"))
+ cmp := strings.TrimPrefix(string(a), "./")
+ prefix = strings.TrimPrefix(prefix, "./")
+ return strings.HasPrefix(cmp, prefix)
}
diff --git a/match/match_test.go b/match/match_test.go
index ae1ffea..d7a851a 100644
--- a/match/match_test.go
+++ b/match/match_test.go
@@ -45,6 +45,7 @@ func TestMatch(t *testing.T) {
{prefix: "", want: true},
{prefix: "f", want: true},
{prefix: "./f", want: true},
+ {prefix: "./.", want: false},
{prefix: "file.", want: true},
{prefix: "./file.", want: true},
{prefix: "file.txt", want: true},
@@ -54,6 +55,7 @@ func TestMatch(t *testing.T) {
{prefix: "/file.txt", want: false},
{prefix: "/fil", want: false},
{prefix: "/file.txt2", want: false},
+ {prefix: "/.", want: false},
},
},
{
@@ -62,6 +64,7 @@ func TestMatch(t *testing.T) {
{prefix: "", want: true},
{prefix: "f", want: true},
{prefix: "./f", want: true},
+ {prefix: "./.", want: false},
{prefix: "file.", want: true},
{prefix: "./file.", want: true},
{prefix: "file.txt", want: true},
@@ -71,14 +74,16 @@ func TestMatch(t *testing.T) {
{prefix: "/file.txt", want: false},
{prefix: "/fil", want: false},
{prefix: "/file.txt2", want: false},
+ {prefix: "/.", want: false},
},
},
{
m: File("/file.txt"),
tests: []matcherTest{
- {prefix: "", want: false},
+ {prefix: "", want: true},
{prefix: "f", want: false},
{prefix: "./f", want: false},
+ {prefix: "./.", want: false},
{prefix: "file.", want: false},
{prefix: "./file.", want: false},
{prefix: "file.txt", want: false},
@@ -88,13 +93,23 @@ func TestMatch(t *testing.T) {
{prefix: "/file.txt", want: true},
{prefix: "/fil", want: true},
{prefix: "/file.txt2", want: false},
+ {prefix: "/.", want: false},
+ },
+ },
+ {
+ m: File("./"),
+ tests: []matcherTest{
+ {prefix: "", want: true},
+ {prefix: ".", want: true},
+ {prefix: "./", want: true},
+ {prefix: "./.", want: false},
},
},
}
for _, tt := range tests {
for _, ttt := range tt.tests {
- name := "matcher:" + tt.m.String() + "/prefix:" + ttt.prefix
+ name := "matcher='" + tt.m.String() + "'&prefix='" + ttt.prefix + "'"
t.Run(name, func(t *testing.T) {
got := tt.m.Match(ttt.prefix)
if got != ttt.want {
diff --git a/predicate.go b/predicate.go
index 4f8588f..bb7e8cb 100644
--- a/predicate.go
+++ b/predicate.go
@@ -3,7 +3,6 @@ package complete
import (
"os"
"path/filepath"
- "strings"
"github.com/posener/complete/match"
)
@@ -53,23 +52,8 @@ func PredictSet(options ...string) Predicate {
// 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(last string) (options []match.Matcher) {
- path := dirFromLast(last)
- dirs := []string{}
- filepath.Walk(path, func(path string, info os.FileInfo, err error) error {
- if err != nil {
- return nil
- }
- if info.IsDir() {
- dirs = append(dirs, path)
- }
- return nil
- })
- // if given path is not absolute, return relative paths
- if !filepath.IsAbs(path) {
- filesToRel(dirs)
- }
- return filesToMatchers(dirs)
+func PredictDirs(pattern string) Predicate {
+ return files(pattern, true, false)
}
// PredictFiles will search for files matching the given pattern in the started to
@@ -77,12 +61,26 @@ func PredictDirs(last string) (options []match.Matcher) {
// 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 {
+ return files(pattern, false, true)
+}
+
+// PredictFilesOrDirs predict any file or directory that matches the pattern
+func PredictFilesOrDirs(pattern string) Predicate {
+ 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)
files, err := filepath.Glob(filepath.Join(dir, pattern))
if err != nil {
Log("failed glob operation with pattern '%s': %s", pattern, err)
}
+ if allowDirs {
+ files = append(files, dir)
+ }
+ files = selectByType(files, allowDirs, allowFiles)
if !filepath.IsAbs(pattern) {
filesToRel(files)
}
@@ -90,6 +88,21 @@ func PredictFiles(pattern string) Predicate {
}
}
+func selectByType(names []string, allowDirs bool, allowFiles bool) []string {
+ filtered := make([]string, 0, len(names))
+ for _, name := range names {
+ stat, err := os.Stat(name)
+ if err != nil {
+ continue
+ }
+ if (stat.IsDir() && !allowDirs) || (!stat.IsDir() && !allowFiles) {
+ continue
+ }
+ filtered = append(filtered, name)
+ }
+ return filtered
+}
+
// filesToRel, change list of files to their names in the relative
// to current directory form.
func filesToRel(files []string) {
@@ -106,12 +119,12 @@ func filesToRel(files []string) {
if err != nil {
continue
}
- if rel == "." {
- rel = ""
- }
- if !strings.HasPrefix(rel, ".") {
+ if rel != "." {
rel = "./" + rel
}
+ if info, err := os.Stat(rel); err == nil && info.IsDir() {
+ rel += "/"
+ }
files[i] = rel
}
return
@@ -129,6 +142,9 @@ func filesToMatchers(files []string) []match.Matcher {
// last argument if it represents a file name being written.
// in case that it is not, we fall back to the current directory.
func dirFromLast(last string) string {
+ if info, err := os.Stat(last); err == nil && info.IsDir() {
+ return last
+ }
dir := filepath.Dir(last)
_, err := os.Stat(dir)
if err != nil {
diff --git a/predicate_test.go b/predicate_test.go
index 264e8d1..1a694a1 100644
--- a/predicate_test.go
+++ b/predicate_test.go
@@ -22,6 +22,12 @@ func TestPredicate(t *testing.T) {
want: []string{"a", "b", "c"},
},
{
+ name: "set with does",
+ p: PredictSet("./..", "./x"),
+ arg: "./.",
+ want: []string{"./.."},
+ },
+ {
name: "set/empty",
p: PredictSet(),
want: []string{},
@@ -59,7 +65,7 @@ func TestPredicate(t *testing.T) {
{
name: "files/txt",
p: PredictFiles("*.txt"),
- want: []string{"./a.txt", "./b.txt", "./c.txt"},
+ want: []string{"./a.txt", "./b.txt", "./c.txt", "./.dot.txt"},
},
{
name: "files/txt",
@@ -86,23 +92,39 @@ func TestPredicate(t *testing.T) {
},
{
name: "dirs",
- p: PredictDirs,
+ p: PredictDirs("*"),
arg: "./dir/",
- want: []string{"./dir"},
+ want: []string{"./dir/"},
+ },
+ {
+ name: "dirs and files",
+ p: PredictFilesOrDirs("*"),
+ arg: "./dir",
+ want: []string{"./dir/", "./dir/x"},
},
{
name: "dirs",
- p: PredictDirs,
- want: []string{"./", "./dir"},
+ p: PredictDirs("*"),
+ want: []string{"./", "./dir/"},
+ },
+ {
+ name: "subdir",
+ p: PredictFiles("*"),
+ arg: "./dir/",
+ want: []string{"./dir/x"},
},
}
for _, tt := range tests {
- t.Run(tt.name+"/"+tt.arg, func(t *testing.T) {
+ t.Run(tt.name+"?arg='"+tt.arg+"'", func(t *testing.T) {
+
matchers := tt.p.predict(tt.arg)
+
matchersString := []string{}
for _, m := range matchers {
- matchersString = append(matchersString, m.String())
+ if m.Match(tt.arg) {
+ matchersString = append(matchersString, m.String())
+ }
}
sort.Strings(matchersString)
sort.Strings(tt.want)
@@ -111,7 +133,7 @@ func TestPredicate(t *testing.T) {
want := strings.Join(tt.want, ",")
if got != want {
- t.Errorf("failed %s\ngot = %s\nwant: %s", tt.name, got, want)
+ t.Errorf("failed %s\ngot = %s\nwant: %s", t.Name(), got, want)
}
})
}
diff --git a/tests/.dot.txt b/tests/.dot.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/.dot.txt