Merge pull request #12 from posener/improves

Enhance program structure and data structures
This commit is contained in:
Eyal Posener 2017-05-11 20:54:26 +03:00 committed by GitHub
commit d3bbb859d5
12 changed files with 188 additions and 188 deletions

39
args.go Normal file
View File

@ -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
}

View File

@ -3,7 +3,7 @@ package complete
import "github.com/posener/complete/match" import "github.com/posener/complete/match"
// Command represents a command line // 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. // Command can also be a sub command.
type Command struct { type Command struct {
// Sub is map of sub commands of the current command // Sub is map of sub commands of the current command
@ -12,88 +12,79 @@ type Command struct {
Sub Commands Sub Commands
// Flags is a map of flags that the command accepts. // 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 Flags Flags
// Args are extra arguments that the command accepts, those who are // Args are extra arguments that the command accepts, those who are
// given without any flag before. // 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 // Commands is the type of Sub member, it maps a command name to a command struct
type Commands map[string]Command type Commands map[string]Command
// Flags is the type Flags of the Flags member, it maps a flag name to the flag // Flags is the type Flags of the Flags member, it maps a flag name to the flag predictions.
// prediction options. type Flags map[string]Predictor
type Flags map[string]Predicate
// options returns all available complete options for the given command // Predict returns all possible predictions for args according to the command struct
// args are all except the last command line arguments relevant to the command func (c *Command) Predict(a Args) (predictions []string) {
func (c *Command) options(args []string) (options []match.Matcher, only bool) { predictions, _ = c.predict(a)
return
// 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
} }
sub, options, only := c.searchSub(args) func (c *Command) predict(a Args) (options []string, only bool) {
// if wordCompleted has something that needs to follow it,
// it is the most relevant completion
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(a)
if only { if only {
return return
} }
// if no subcommand was entered in any of the args, add the // if no sub command was found, return a list of the sub commands
// subcommands as complete options.
if sub == "" { if sub == "" {
options = append(options, c.subCommands()...) options = append(options, c.subCommands(a.Last)...)
} }
// add global available complete options // add global available complete options
for flag := range c.Flags { 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 // 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 return
} }
// searchSub searches recursively within sub commands if the sub command appear // searchSub searches recursively within sub commands if the sub command appear
// in the on of the arguments. // in the on of the arguments.
func (c *Command) searchSub(args []string) (sub string, all []match.Matcher, only bool) { func (c *Command) searchSub(a Args) (sub string, all []string, only bool) {
for i, arg := range a.Completed {
// 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 { if cmd, ok := c.Sub[arg]; ok {
sub = arg sub = arg
all, only = cmd.options(args[i:]) all, only = cmd.predict(a.from(i))
return return
} }
} }
return "", nil, false return
} }
// suvCommands returns a list of matchers according to the sub command names // subCommands returns a list of matching sub commands
func (c *Command) subCommands() []match.Matcher { func (c *Command) subCommands(last string) (prediction []string) {
subs := make([]match.Matcher, 0, len(c.Sub))
for sub := range c.Sub { for sub := range c.Sub {
subs = append(subs, match.Prefix(sub)) if match.Prefix(sub, last) {
prediction = append(prediction, sub)
} }
return subs
} }
return
func removeLast(a []string) []string {
if len(a) > 0 {
return a[:len(a)-1]
}
return a
} }

View File

@ -41,37 +41,23 @@ func New(name string, command Command) *Complete {
// returns success if the completion ran or if the cli matched // returns success if the completion ran or if the cli matched
// any of the given flags, false otherwise // any of the given flags, false otherwise
func (c *Complete) Run() bool { func (c *Complete) Run() bool {
args, ok := getLine() line, ok := getLine()
if !ok { if !ok {
// make sure flags parsed, // make sure flags parsed,
// in case they were not added in the main program // in case they were not added in the main program
return c.CLI.Run() 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) Log("Completion: %s", options)
output(options) output(options)
return true 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) { func getLine() ([]string, bool) {
line := os.Getenv(envComplete) line := os.Getenv(envComplete)
if line == "" { if line == "" {
@ -80,13 +66,6 @@ func getLine() ([]string, bool) {
return strings.Split(line, " "), true 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) { func output(options []string) {
Log("") Log("")
// stdout of program defines the complete options // stdout of program defines the complete options

View File

@ -13,20 +13,20 @@ func TestCompleter_Complete(t *testing.T) {
c := Command{ c := Command{
Sub: map[string]Command{ Sub: map[string]Command{
"sub1": { "sub1": {
Flags: map[string]Predicate{ Flags: map[string]Predictor{
"-flag1": PredictAnything, "-flag1": PredictAnything,
"-flag2": PredictNothing, "-flag2": PredictNothing,
}, },
}, },
"sub2": { "sub2": {
Flags: map[string]Predicate{ Flags: map[string]Predictor{
"-flag2": PredictNothing, "-flag2": PredictNothing,
"-flag3": PredictSet("opt1", "opt2", "opt12"), "-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, "-h": PredictNothing,
"-global1": PredictAnything, "-global1": PredictAnything,
"-o": PredictFiles("*.txt"), "-o": PredictFiles("*.txt"),
@ -174,9 +174,9 @@ func TestCompleter_Complete(t *testing.T) {
tt.args = "cmd " + tt.args tt.args = "cmd " + tt.args
os.Setenv(envComplete, 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(tt.want)
sort.Strings(got) sort.Strings(got)

View File

@ -6,9 +6,11 @@ import "github.com/posener/complete"
var ( var (
predictEllipsis = complete.PredictSet("./...") predictEllipsis = complete.PredictSet("./...")
goFilesOrPackages = complete.PredictFiles("*.go"). goFilesOrPackages = complete.PredictOr(
Or(complete.PredictDirs("*")). complete.PredictFiles("*.go"),
Or(predictEllipsis) complete.PredictDirs("*"),
predictEllipsis,
)
) )
func main() { func main() {

View File

@ -17,15 +17,16 @@ import (
// and then all the relevant function names. // and then all the relevant function names.
// for test names use prefix of 'Test' or 'Example', and for benchmark // for test names use prefix of 'Test' or 'Example', and for benchmark
// test names use 'Benchmark' // test names use 'Benchmark'
func predictTest(funcPrefix ...string) complete.Predicate { func predictTest(funcPrefix ...string) complete.Predictor {
return func(last string) []match.Matcher { return complete.PredictFunc(func(a complete.Args) (prediction []string) {
tests := testNames(funcPrefix) tests := testNames(funcPrefix)
options := make([]match.Matcher, len(tests)) for _, t := range tests {
for i := range tests { if match.Prefix(t, a.Last) {
options[i] = match.Prefix(tests[i]) prediction = append(prediction, t)
} }
return options
} }
return
})
} }
// get all test names in current directory // get all test names in current directory

View File

@ -1,26 +1,16 @@
package match package match
import ( import "strings"
"strings"
)
// File is a file name Matcher, if the last word can prefix the // File returns true if prefix can match the file
// File path, there is a possible match func File(file, prefix string) bool {
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 {
// special case for current directory completion // special case for current directory completion
if a == "./" && (prefix == "." || prefix == "") { if file == "./" && (prefix == "." || prefix == "") {
return true return true
} }
cmp := strings.TrimPrefix(string(a), "./") file = strings.TrimPrefix(file, "./")
prefix = strings.TrimPrefix(prefix, "./") prefix = strings.TrimPrefix(prefix, "./")
return strings.HasPrefix(cmp, prefix) return strings.HasPrefix(file, prefix)
} }

View File

@ -1,11 +1,6 @@
package match package match
import "fmt" // Match matches two strings
// it is used for comparing a term to the last typed
// Matcher matches itself to a string // word, the prefix, and see if it is a possible auto complete option.
// it is used for comparing a given argument to the last typed type Match func(term, prefix string) bool
// word, and see if it is a possible auto complete option.
type Matcher interface {
fmt.Stringer
Match(prefix string) bool
}

View File

@ -1,6 +1,7 @@
package match package match
import ( import (
"fmt"
"os" "os"
"testing" "testing"
) )
@ -21,11 +22,13 @@ func TestMatch(t *testing.T) {
} }
tests := []struct { tests := []struct {
m Matcher m Match
long string
tests []matcherTest tests []matcherTest
}{ }{
{ {
m: Prefix("abcd"), m: Prefix,
long: "abcd",
tests: []matcherTest{ tests: []matcherTest{
{prefix: "", want: true}, {prefix: "", want: true},
{prefix: "ab", want: true}, {prefix: "ab", want: true},
@ -33,14 +36,16 @@ func TestMatch(t *testing.T) {
}, },
}, },
{ {
m: Prefix(""), m: Prefix,
long: "",
tests: []matcherTest{ tests: []matcherTest{
{prefix: "ac", want: false}, {prefix: "ac", want: false},
{prefix: "", want: true}, {prefix: "", want: true},
}, },
}, },
{ {
m: File("file.txt"), m: File,
long: "file.txt",
tests: []matcherTest{ tests: []matcherTest{
{prefix: "", want: true}, {prefix: "", want: true},
{prefix: "f", 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{ tests: []matcherTest{
{prefix: "", want: true}, {prefix: "", want: true},
{prefix: "f", 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{ tests: []matcherTest{
{prefix: "", want: true}, {prefix: "", want: true},
{prefix: "f", want: false}, {prefix: "f", want: false},
@ -97,7 +104,8 @@ func TestMatch(t *testing.T) {
}, },
}, },
{ {
m: File("./"), m: File,
long: "./",
tests: []matcherTest{ tests: []matcherTest{
{prefix: "", want: true}, {prefix: "", want: true},
{prefix: ".", want: true}, {prefix: ".", want: true},
@ -109,9 +117,9 @@ func TestMatch(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
for _, ttt := range tt.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) { t.Run(name, func(t *testing.T) {
got := tt.m.Match(ttt.prefix) got := tt.m(tt.long, ttt.prefix)
if got != ttt.want { if got != ttt.want {
t.Errorf("Failed %s: got = %t, want: %t", name, got, ttt.want) t.Errorf("Failed %s: got = %t, want: %t", name, got, ttt.want)
} }

View File

@ -3,13 +3,7 @@ package match
import "strings" import "strings"
// Prefix is a simple Matcher, if the word is it's prefix, there is a match // 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 // Match returns true if a has the prefix as prefix
func (a Prefix) Match(prefix string) bool { func Prefix(long, prefix string) bool {
return strings.HasPrefix(string(a), prefix) return strings.HasPrefix(long, prefix)
} }

View File

@ -7,52 +7,66 @@ import (
"github.com/posener/complete/match" "github.com/posener/complete/match"
) )
// Predicate determines what terms can follow a command or a flag // Predictor implements a predict method, in which given
// command line arguments returns a list of options it predicts.
type Predictor interface {
Predict(Args) []string
}
// PredictOr unions two predicate functions, so that the result predicate
// returns the union of their predication
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
})
}
// 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 // It is used for auto completion, given last - the last word in the already
// in the command line, what words can complete it. // in the command line, what words can complete it.
type Predicate func(last string) []match.Matcher type PredictFunc func(Args) []string
// Or unions two predicate functions, so that the result predicate // Predict invokes the predict function and implements the Predictor interface
// returns the union of their predication func (p PredictFunc) Predict(a Args) []string {
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 (p Predicate) predict(last string) []match.Matcher {
if p == nil { if p == nil {
return nil return nil
} }
return p(last) return p(a)
} }
// PredictNothing does not expect anything after. // PredictNothing does not expect anything after.
var PredictNothing Predicate var PredictNothing Predictor
// PredictAnything expects something, but nothing particular, such as a number // PredictAnything expects something, but nothing particular, such as a number
// or arbitrary name. // 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. // PredictSet expects specific set of terms, given in the options argument.
func PredictSet(options ...string) Predicate { func PredictSet(options ...string) Predictor {
return func(last string) []match.Matcher { return predictSet(options)
ret := make([]match.Matcher, len(options))
for i := range options {
ret[i] = match.Prefix(options[i])
} }
return ret
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
}
// PredictDirs will search for directories in the given started to be typed // 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 // path, if no path was started to be typed, it will complete to directories
// in the current working directory. // in the current working directory.
func PredictDirs(pattern string) Predicate { func PredictDirs(pattern string) Predictor {
return files(pattern, true, false) 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 // 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. // match the pattern in the current working directory.
// To match any file, use "*" as pattern. To match go files use "*.go", and so on. // 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) return files(pattern, false, true)
} }
// PredictFilesOrDirs predict any file or directory that matches the pattern // PredictFilesOrDirs any file or directory that matches the pattern
func PredictFilesOrDirs(pattern string) Predicate { func PredictFilesOrDirs(pattern string) Predictor {
return files(pattern, true, true) return files(pattern, true, true)
} }
func files(pattern string, allowDirs, allowFiles bool) Predicate { func files(pattern string, allowDirs, allowFiles bool) PredictFunc {
return func(last string) []match.Matcher { return func(a Args) (prediction []string) {
dir := dirFromLast(last) dir := dirFromLast(a.Last)
Log("looking for files in %s (last=%s)", dir, last) Log("looking for files in %s (last=%s)", dir, a.Last)
files, err := filepath.Glob(filepath.Join(dir, pattern)) files, err := filepath.Glob(filepath.Join(dir, pattern))
if err != nil { if err != nil {
Log("failed glob operation with pattern '%s': %s", pattern, err) 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) { if !filepath.IsAbs(pattern) {
filesToRel(files) 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 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 // dirFromLast gives the directory of the current written
// last argument if it represents a file name being written. // last argument if it represents a file name being written.
// in case that it is not, we fall back to the current directory. // in case that it is not, we fall back to the current directory.

View File

@ -12,7 +12,7 @@ func TestPredicate(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
p Predicate p Predictor
arg string arg string
want []string want []string
}{ }{
@ -37,29 +37,24 @@ func TestPredicate(t *testing.T) {
p: PredictAnything, p: PredictAnything,
want: []string{}, want: []string{},
}, },
{
name: "nothing",
p: PredictNothing,
want: []string{},
},
{ {
name: "or: word with nil", name: "or: word with nil",
p: PredictSet("a").Or(PredictNothing), p: PredictOr(PredictSet("a"), nil),
want: []string{"a"}, want: []string{"a"},
}, },
{ {
name: "or: nil with word", name: "or: nil with word",
p: PredictNothing.Or(PredictSet("a")), p: PredictOr(nil, PredictSet("a")),
want: []string{"a"}, want: []string{"a"},
}, },
{ {
name: "or: nil with nil", name: "or: nil with nil",
p: PredictNothing.Or(PredictNothing), p: PredictOr(PredictNothing, PredictNothing),
want: []string{}, want: []string{},
}, },
{ {
name: "or: word with word with word", 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"}, want: []string{"a", "b", "c"},
}, },
{ {
@ -118,18 +113,12 @@ func TestPredicate(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name+"?arg='"+tt.arg+"'", func(t *testing.T) { 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{} sort.Strings(matches)
for _, m := range matchers {
if m.Match(tt.arg) {
matchersString = append(matchersString, m.String())
}
}
sort.Strings(matchersString)
sort.Strings(tt.want) sort.Strings(tt.want)
got := strings.Join(matchersString, ",") got := strings.Join(matches, ",")
want := strings.Join(tt.want, ",") want := strings.Join(tt.want, ",")
if got != want { if got != want {