From 8724aaf18312e54750540a9578e00d61b1c545d8 Mon Sep 17 00:00:00 2001 From: Eyal Posener Date: Thu, 14 Nov 2019 06:51:44 +0200 Subject: [PATCH] V2 --- .travis.yml | 1 + args.go | 114 --- args_test.go | 213 ----- cmd/cmd.go | 128 --- command.go | 139 +-- common_test.go | 26 - compflag/compflag.go | 245 +++++ compflag/compflag_test.go | 105 +++ complete.go | 390 ++++++-- complete_test.go | 596 +++++------- doc.go | 164 ++-- example/command/main.go | 45 + example/compflag/main.go | 31 + example/self/main.go | 53 -- example/stdlib/main.go | 35 + flags.go | 44 + flags_test.go | 57 ++ gocomplete/complete.go | 883 +++++++++--------- gocomplete/parse.go | 5 +- gocomplete/pkgs.go | 73 +- gocomplete/tests.go | 2 +- gocomplete/tests_test.go | 7 +- internal/arg/arg.go | 124 +++ internal/arg/arg_test.go | 122 +++ {cmd => internal}/install/bash.go | 0 {cmd => internal}/install/fish.go | 0 {cmd => internal}/install/install.go | 28 + {cmd => internal}/install/utils.go | 0 {cmd => internal}/install/zsh.go | 0 internal/tokener/tokener.go | 67 ++ log.go | 22 - match/match.go | 39 - match/match_test.go | 129 --- predict.go | 41 - predict/files.go | 175 ++++ predict/files_test.go | 233 +++++ predict/predict.go | 34 + predict/predict_test.go | 61 ++ {tests => predict/testdata}/.dot.txt | 0 {tests => predict/testdata}/a.txt | 0 {tests => predict/testdata}/b.txt | 0 {tests => predict/testdata}/c.txt | 0 {tests => predict/testdata}/dir/bar | 0 {tests => predict/testdata}/dir/foo | 0 .../testdata}/outer/inner/readme.md | 0 {tests => predict/testdata}/readme.md | 0 predict_files.go | 174 ---- predict_set.go | 12 - predict_test.go | 271 ------ testing.go | 29 + 50 files changed, 2608 insertions(+), 2309 deletions(-) delete mode 100644 args.go delete mode 100644 args_test.go delete mode 100644 cmd/cmd.go delete mode 100644 common_test.go create mode 100644 compflag/compflag.go create mode 100644 compflag/compflag_test.go create mode 100644 example/command/main.go create mode 100644 example/compflag/main.go delete mode 100644 example/self/main.go create mode 100644 example/stdlib/main.go create mode 100644 flags.go create mode 100644 flags_test.go create mode 100644 internal/arg/arg.go create mode 100644 internal/arg/arg_test.go rename {cmd => internal}/install/bash.go (100%) rename {cmd => internal}/install/fish.go (100%) rename {cmd => internal}/install/install.go (84%) rename {cmd => internal}/install/utils.go (100%) rename {cmd => internal}/install/zsh.go (100%) create mode 100644 internal/tokener/tokener.go delete mode 100644 log.go delete mode 100644 match/match.go delete mode 100644 match/match_test.go delete mode 100644 predict.go create mode 100644 predict/files.go create mode 100644 predict/files_test.go create mode 100644 predict/predict.go create mode 100644 predict/predict_test.go rename {tests => predict/testdata}/.dot.txt (100%) rename {tests => predict/testdata}/a.txt (100%) rename {tests => predict/testdata}/b.txt (100%) rename {tests => predict/testdata}/c.txt (100%) rename {tests => predict/testdata}/dir/bar (100%) rename {tests => predict/testdata}/dir/foo (100%) rename {tests => predict/testdata}/outer/inner/readme.md (100%) rename {tests => predict/testdata}/readme.md (100%) delete mode 100644 predict_files.go delete mode 100644 predict_set.go delete mode 100644 predict_test.go create mode 100644 testing.go diff --git a/.travis.yml b/.travis.yml index 6ba8d86..3e42c5c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,7 @@ language: go go: - tip + - 1.13.x - 1.12.x - 1.11.x - 1.10.x diff --git a/args.go b/args.go deleted file mode 100644 index 3340285..0000000 --- a/args.go +++ /dev/null @@ -1,114 +0,0 @@ -package complete - -import ( - "os" - "path/filepath" - "strings" - "unicode" -) - -// Args describes command line arguments -type Args struct { - // All lists of all arguments in command line (not including the command itself) - All []string - // Completed lists of all completed arguments in command line, - // If the last one is still being typed - no space after it, - // it won't appear in this list of arguments. - Completed []string - // Last argument in command line, the one being typed, if the last - // character in the command line is a space, this argument will be empty, - // otherwise this would be the last word. - Last string - // LastCompleted is the last argument that was fully typed. - // If the last character in the command line is space, this would be the - // last word, otherwise, it would be the word before that. - LastCompleted string -} - -// Directory 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. -// -// Deprecated. -func (a Args) Directory() string { - if info, err := os.Stat(a.Last); err == nil && info.IsDir() { - return fixPathForm(a.Last, a.Last) - } - dir := filepath.Dir(a.Last) - if info, err := os.Stat(dir); err != nil || !info.IsDir() { - return "./" - } - return fixPathForm(a.Last, dir) -} - -func newArgs(line string) Args { - var ( - all []string - completed []string - ) - parts := splitFields(line) - if len(parts) > 0 { - all = parts[1:] - completed = removeLast(parts[1:]) - } - return Args{ - All: all, - Completed: completed, - Last: last(parts), - LastCompleted: last(completed), - } -} - -// splitFields returns a list of fields from the given command line. -// If the last character is space, it appends an empty field in the end -// indicating that the field before it was completed. -// If the last field is of the form "a=b", it splits it to two fields: "a", "b", -// So it can be completed. -func splitFields(line string) []string { - parts := strings.Fields(line) - - // Add empty field if the last field was completed. - if len(line) > 0 && unicode.IsSpace(rune(line[len(line)-1])) { - parts = append(parts, "") - } - - // Treat the last field if it is of the form "a=b" - parts = splitLastEqual(parts) - return parts -} - -func splitLastEqual(line []string) []string { - if len(line) == 0 { - return line - } - parts := strings.Split(line[len(line)-1], "=") - return append(line[:len(line)-1], parts...) -} - -// from returns a copy of Args of all arguments after the i'th argument. -func (a Args) from(i int) Args { - if i >= len(a.All) { - i = len(a.All) - 1 - } - a.All = a.All[i+1:] - - if i >= len(a.Completed) { - i = len(a.Completed) - 1 - } - a.Completed = a.Completed[i+1:] - return a -} - -func removeLast(a []string) []string { - if len(a) > 0 { - return a[:len(a)-1] - } - return a -} - -func last(args []string) string { - if len(args) == 0 { - return "" - } - return args[len(args)-1] -} diff --git a/args_test.go b/args_test.go deleted file mode 100644 index 3b42db0..0000000 --- a/args_test.go +++ /dev/null @@ -1,213 +0,0 @@ -package complete - -import ( - "fmt" - "strings" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestArgs(t *testing.T) { - t.Parallel() - tests := []struct { - line string - completed string - last string - lastCompleted string - }{ - { - line: "a b c", - completed: "b", - last: "c", - lastCompleted: "b", - }, - { - line: "a b ", - completed: "b", - last: "", - lastCompleted: "b", - }, - { - line: "", - completed: "", - last: "", - lastCompleted: "", - }, - { - line: "a", - completed: "", - last: "a", - lastCompleted: "", - }, - { - line: "a ", - completed: "", - last: "", - lastCompleted: "", - }, - } - - for _, tt := range tests { - t.Run(tt.line, func(t *testing.T) { - - a := newArgs(tt.line) - - if got, want := strings.Join(a.Completed, " "), tt.completed; got != want { - t.Errorf("%s failed: Completed = %q, want %q", t.Name(), got, want) - } - if got, want := a.Last, tt.last; got != want { - t.Errorf("Last = %q, want %q", got, want) - } - if got, want := a.LastCompleted, tt.lastCompleted; got != want { - t.Errorf("%s failed: LastCompleted = %q, want %q", t.Name(), got, want) - } - }) - } -} - -func TestArgs_From(t *testing.T) { - t.Parallel() - tests := []struct { - line string - from int - newLine string - newCompleted string - }{ - { - line: "a b c", - from: 0, - newLine: "b c", - newCompleted: "b", - }, - { - line: "a b c", - from: 1, - newLine: "c", - newCompleted: "", - }, - { - line: "a b c", - from: 2, - newLine: "", - newCompleted: "", - }, - { - line: "a b c", - from: 3, - newLine: "", - newCompleted: "", - }, - { - line: "a b c ", - from: 0, - newLine: "b c ", - newCompleted: "b c", - }, - { - line: "a b c ", - from: 1, - newLine: "c ", - newCompleted: "c", - }, - { - line: "a b c ", - from: 2, - newLine: "", - newCompleted: "", - }, - { - line: "", - from: 0, - newLine: "", - newCompleted: "", - }, - { - line: "", - from: 1, - newLine: "", - newCompleted: "", - }, - } - - for _, tt := range tests { - t.Run(fmt.Sprintf("%s/%d", tt.line, tt.from), func(t *testing.T) { - - a := newArgs("cmd " + tt.line) - n := a.from(tt.from) - - assert.Equal(t, tt.newLine, strings.Join(n.All, " ")) - assert.Equal(t, tt.newCompleted, strings.Join(n.Completed, " ")) - }) - } -} - -func TestArgs_Directory(t *testing.T) { - t.Parallel() - initTests() - - tests := []struct { - line string - directory string - }{ - { - line: "a b c", - directory: "./", - }, - { - line: "a b c /tm", - directory: "/", - }, - { - line: "a b c /tmp", - directory: "/tmp/", - }, - { - line: "a b c /tmp ", - directory: "./", - }, - { - line: "a b c ./", - directory: "./", - }, - { - line: "a b c ./dir", - directory: "./dir/", - }, - { - line: "a b c dir", - directory: "dir/", - }, - { - line: "a b c ./di", - directory: "./", - }, - { - line: "a b c ./dir ", - directory: "./", - }, - { - line: "a b c ./di", - directory: "./", - }, - { - line: "a b c ./a.txt", - directory: "./", - }, - { - line: "a b c ./a.txt/x", - directory: "./", - }, - } - - for _, tt := range tests { - t.Run(tt.line, func(t *testing.T) { - - a := newArgs(tt.line) - - if got, want := a.Directory(), tt.directory; got != want { - t.Errorf("%s failed: directory = %q, want %q", t.Name(), got, want) - } - }) - } -} diff --git a/cmd/cmd.go b/cmd/cmd.go deleted file mode 100644 index b99fe52..0000000 --- a/cmd/cmd.go +++ /dev/null @@ -1,128 +0,0 @@ -// Package cmd used for command line options for the complete tool -package cmd - -import ( - "errors" - "flag" - "fmt" - "os" - "strings" - - "github.com/posener/complete/cmd/install" -) - -// CLI for command line -type CLI struct { - Name string - InstallName string - UninstallName string - - install bool - uninstall bool - yes bool -} - -const ( - defaultInstallName = "install" - defaultUninstallName = "uninstall" -) - -// Run is used when running complete in command line mode. -// this is used when the complete is not completing words, but to -// install it or uninstall it. -func (f *CLI) Run() bool { - err := f.validate() - if err != nil { - os.Stderr.WriteString(err.Error() + "\n") - os.Exit(1) - } - - switch { - case f.install: - f.prompt() - err = install.Install(f.Name) - case f.uninstall: - f.prompt() - err = install.Uninstall(f.Name) - default: - // non of the action flags matched, - // returning false should make the real program execute - return false - } - - if err != nil { - fmt.Printf("%s failed! %s\n", f.action(), err) - os.Exit(3) - } - fmt.Println("Done!") - return true -} - -// prompt use for approval -// exit if approval was not given -func (f *CLI) prompt() { - defer fmt.Println(f.action() + "ing...") - if f.yes { - return - } - fmt.Printf("%s completion for %s? ", f.action(), f.Name) - var answer string - fmt.Scanln(&answer) - - switch strings.ToLower(answer) { - case "y", "yes": - return - default: - fmt.Println("Cancelling...") - os.Exit(1) - } -} - -// AddFlags adds the CLI flags to the flag set. -// If flags is nil, the default command line flags will be taken. -// Pass non-empty strings as installName and uninstallName to override the default -// flag names. -func (f *CLI) AddFlags(flags *flag.FlagSet) { - if flags == nil { - flags = flag.CommandLine - } - - if f.InstallName == "" { - f.InstallName = defaultInstallName - } - if f.UninstallName == "" { - f.UninstallName = defaultUninstallName - } - - if flags.Lookup(f.InstallName) == nil { - flags.BoolVar(&f.install, f.InstallName, false, - fmt.Sprintf("Install completion for %s command", f.Name)) - } - if flags.Lookup(f.UninstallName) == nil { - flags.BoolVar(&f.uninstall, f.UninstallName, false, - fmt.Sprintf("Uninstall completion for %s command", f.Name)) - } - if flags.Lookup("y") == nil { - flags.BoolVar(&f.yes, "y", false, "Don't prompt user for typing 'yes' when installing completion") - } -} - -// validate the CLI -func (f *CLI) validate() error { - if f.install && f.uninstall { - return errors.New("Install and uninstall are mutually exclusive") - } - return nil -} - -// action name according to the CLI values. -func (f *CLI) action() string { - switch { - case f.install: - return "Install" - case f.uninstall: - return "Uninstall" - default: - return "unknown" - } -} diff --git a/command.go b/command.go index 82d37d5..74ab299 100644 --- a/command.go +++ b/command.go @@ -1,111 +1,62 @@ package complete -// Command represents a command line -// It holds the data that enables auto completion of command line -// Command can also be a sub command. +// Command is an object that can be used to create complete options for a go executable that does +// not have a good binding to the `Completer` interface, or to use a Go program as complete binary +// for another executable (see ./gocomplete as an example.) type Command struct { - // Sub is map of sub commands of the current command - // The key refer to the sub command name, and the value is it's - // Command descriptive 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 predictions. - Flags Flags - - // GlobalFlags is a map of flags that the command accepts. - // Global flags that can appear also after a sub command. - GlobalFlags Flags - - // Args are extra arguments that the command accepts, those who are - // given without any flag before. + // Sub is map of sub commands of the current command. The key refer to the sub command name, and + // the value is it's command descriptive struct. + Sub map[string]*Command + // Flags is a map of flags that the command accepts. The key is the flag name, and the value is + // it's predictions. In a chain of sub commands, no duplicate flags should be defined. + Flags map[string]Predictor + // Args are extra arguments that the command accepts, those who are given without any flag + // before. In any chain of sub commands, only one of them should predict positional arguments. Args Predictor } -// Predict returns all possible predictions for args according to the command struct -func (c *Command) Predict(a Args) []string { - options, _ := c.predict(a) - return options +// Complete runs the completion of the described command. +func (c *Command) Complete(name string) { + Complete(name, c) } -// Commands is the type of Sub member, it maps a command name to a command struct -type Commands map[string]Command - -// Predict completion of sub command names names according to command line arguments -func (c Commands) Predict(a Args) (prediction []string) { - for sub := range c { - prediction = append(prediction, sub) +func (c *Command) SubCmdList() []string { + subs := make([]string, 0, len(c.Sub)) + for sub := range c.Sub { + subs = append(subs, sub) } - return + return subs } -// Flags is the type Flags of the Flags member, it maps a flag name to the flag predictions. -type Flags map[string]Predictor +func (c *Command) SubCmdGet(cmd string) Completer { + if c.Sub[cmd] == nil { + return nil + } + return c.Sub[cmd] +} +func (c *Command) FlagList() []string { + flags := make([]string, 0, len(c.Flags)) + for flag := range c.Flags { + flags = append(flags, flag) + } + return flags +} -// Predict completion of flags names according to command line arguments -func (f Flags) Predict(a Args) (prediction []string) { - for flag := range f { - // If the flag starts with a hyphen, we avoid emitting the prediction - // unless the last typed arg contains a hyphen as well. - flagHyphenStart := len(flag) != 0 && flag[0] == '-' - lastHyphenStart := len(a.Last) != 0 && a.Last[0] == '-' - if flagHyphenStart && !lastHyphenStart { - continue +func (c *Command) FlagGet(flag string) Predictor { + return PredictFunc(func(prefix string) (options []string) { + f := c.Flags[flag] + if f == nil { + return nil } - prediction = append(prediction, flag) - } - return + return f.Predict(prefix) + }) } -// predict options -// only is set to true if no more options are allowed to be returned -// those are in cases of special flag that has specific completion arguments, -// and other flags or sub commands can't come after it. -func (c *Command) predict(a Args) (options []string, only bool) { - - // search sub commands for predictions first - subCommandFound := false - for i, arg := range a.Completed { - if cmd, ok := c.Sub[arg]; ok { - subCommandFound = true - - // recursive call for sub command - options, only = cmd.predict(a.from(i)) - if only { - return - } - - // We matched so stop searching. Continuing to search can accidentally - // match a subcommand with current set of commands, see issue #46. - break +func (c *Command) ArgsGet() Predictor { + return PredictFunc(func(prefix string) (options []string) { + if c.Args == nil { + return nil } - } - - // if last completed word is a global flag that we need to complete - if predictor, ok := c.GlobalFlags[a.LastCompleted]; ok && predictor != nil { - Log("Predicting according to global flag %s", a.LastCompleted) - return predictor.Predict(a), true - } - - options = append(options, c.GlobalFlags.Predict(a)...) - - // if a sub command was entered, we won't add the parent command - // completions and we return here. - if subCommandFound { - return - } - - // if last completed word is a command flag that we need to complete - if predictor, ok := c.Flags[a.LastCompleted]; ok && predictor != nil { - Log("Predicting according to flag %s", a.LastCompleted) - return predictor.Predict(a), true - } - - options = append(options, c.Sub.Predict(a)...) - options = append(options, c.Flags.Predict(a)...) - if c.Args != nil { - options = append(options, c.Args.Predict(a)...) - } - - return + return c.Args.Predict(prefix) + }) } diff --git a/common_test.go b/common_test.go deleted file mode 100644 index 38fe5f1..0000000 --- a/common_test.go +++ /dev/null @@ -1,26 +0,0 @@ -package complete - -import ( - "os" - "sync" - "testing" -) - -var once = sync.Once{} - -func initTests() { - once.Do(func() { - // Set debug environment variable so logs will be printed - if testing.Verbose() { - os.Setenv(envDebug, "1") - // refresh the logger with environment variable set - Log = getLogger() - } - - // Change to tests directory for testing completion of files and directories - err := os.Chdir("./tests") - if err != nil { - panic(err) - } - }) -} diff --git a/compflag/compflag.go b/compflag/compflag.go new file mode 100644 index 0000000..cfb4440 --- /dev/null +++ b/compflag/compflag.go @@ -0,0 +1,245 @@ +// Package compflag provides a handful of standard library-compatible flags with bash complition capabilities. +// +// Usage +// +// import "github.com/posener/complete/compflag" +// +// var ( +// // Define flags... +// foo = compflag.String("foo", "", "") +// ) +// +// func main() { +// compflag.Parse("my-program") +// // Main function. +// } +// +// Alternatively, the library can just be used with the standard library flag package: +// +// import ( +// "flag" +// "github.com/posener/complete/compflag" +// ) +// +// var ( +// // Define flags... +// foo = compflag.String("foo", "", "") +// bar = flag.String("bar", "", "") +// ) +// +// func main() { +// complete.CommandLine("my-program") +// flag.ParseArgs() +// // Main function. +// } +package compflag + +import ( + "flag" + "fmt" + "os" + "strconv" + "strings" + + "github.com/posener/complete" +) + +// Flag options. +type Option func(*options) + +// OptValues allows to set a desired set of valid values for the flag. +func OptValues(values ...string) Option { + return func(o *options) { o.values = values } +} + +// OptCheck enforces the valid values on the predicted flag. +func OptCheck() Option { + return func(o *options) { o.check = true } +} + +type options struct { + values []string + check bool +} + +func config(fs ...Option) options { + var op options + for _, f := range fs { + f(&op) + } + return op +} + +// FlagSet is bash completion enabled flag.FlagSet. +type FlagSet flag.FlagSet + +// Parse parses command line arguments. +func (fs *FlagSet) Parse(args []string) error { + return (*flag.FlagSet)(fs).Parse(args) +} + +// Complete performs bash completion if needed. +func (fs *FlagSet) Complete(name string) { + complete.Complete(name, complete.FlagSet((*flag.FlagSet)(CommandLine))) +} + +func (fs *FlagSet) String(name string, value string, usage string, options ...Option) *string { + p := new(string) + (*flag.FlagSet)(fs).Var(newStringValue(value, p, config(options...)), name, usage) + return p +} + +func (fs *FlagSet) Bool(name string, value bool, usage string, options ...Option) *bool { + p := new(bool) + (*flag.FlagSet)(fs).Var(newBoolValue(value, p, config(options...)), name, usage) + return p +} + +func (fs *FlagSet) Int(name string, value int, usage string, options ...Option) *int { + p := new(int) + (*flag.FlagSet)(fs).Var(newIntValue(value, p, config(options...)), name, usage) + return p +} + +func (o options) checkValue(v string) error { + if !o.check || len(o.values) == 0 { + return nil + } + for _, vv := range o.values { + if v == vv { + return nil + } + } + return fmt.Errorf("not in allowed values: %s", strings.Join(o.values, ",")) +} + +var CommandLine = (*FlagSet)(flag.CommandLine) + +// Parse parses command line arguments. It also performs bash completion when needed. +func Parse(name string) { + CommandLine.Complete(name) + CommandLine.Parse(os.Args[1:]) +} + +func String(name string, value string, usage string, options ...Option) *string { + return CommandLine.String(name, value, usage, options...) +} + +func Bool(name string, value bool, usage string, options ...Option) *bool { + return CommandLine.Bool(name, value, usage, options...) +} + +func Int(name string, value int, usage string, options ...Option) *int { + return CommandLine.Int(name, value, usage, options...) +} + +type boolValue struct { + v *bool + options +} + +func newBoolValue(val bool, p *bool, o options) *boolValue { + *p = val + return &boolValue{v: p, options: o} +} + +func (b *boolValue) Set(val string) error { + v, err := strconv.ParseBool(val) + *b.v = v + if err != nil { + return fmt.Errorf("bad value for bool flag") + } + return b.checkValue(val) +} + +func (b *boolValue) Get() interface{} { return bool(*b.v) } + +func (b *boolValue) String() string { + if b == nil || b.v == nil { + return strconv.FormatBool(false) + } + return strconv.FormatBool(bool(*b.v)) +} + +func (b *boolValue) IsBoolFlag() bool { return true } + +func (b *boolValue) Predict(_ string) []string { + if b.values != nil { + return b.values + } + // If false, typing the bool flag is expected to turn it on, so there is nothing to complete + // after the flag. + if !*b.v { + return nil + } + // Otherwise, suggest only to turn it off. + return []string{"false"} +} + +type stringValue struct { + v *string + options +} + +func newStringValue(val string, p *string, o options) *stringValue { + *p = val + return &stringValue{v: p, options: o} +} + +func (s *stringValue) Set(val string) error { + *s.v = val + return s.options.checkValue(val) +} + +func (s *stringValue) Get() interface{} { + return string(*s.v) +} + +func (s *stringValue) String() string { + if s == nil || s.v == nil { + return "" + } + return string(*s.v) +} + +func (s *stringValue) Predict(_ string) []string { + if s.values != nil { + return s.values + } + return []string{""} +} + +type intValue struct { + v *int + options +} + +func newIntValue(val int, p *int, o options) *intValue { + *p = val + return &intValue{v: p, options: o} +} + +func (i *intValue) Set(val string) error { + v, err := strconv.ParseInt(val, 0, strconv.IntSize) + *i.v = int(v) + if err != nil { + return fmt.Errorf("bad value for int flag") + } + return i.checkValue(val) +} + +func (i *intValue) Get() interface{} { return int(*i.v) } + +func (i *intValue) String() string { + if i == nil || i.v == nil { + return strconv.Itoa(0) + } + return strconv.Itoa(int(*i.v)) +} + +func (s *intValue) Predict(_ string) []string { + if s.values != nil { + return s.values + } + return []string{""} +} diff --git a/compflag/compflag_test.go b/compflag/compflag_test.go new file mode 100644 index 0000000..1e8dea5 --- /dev/null +++ b/compflag/compflag_test.go @@ -0,0 +1,105 @@ +package compflag + +import ( + "flag" + "testing" + + "github.com/posener/complete" + "github.com/stretchr/testify/assert" +) + +func TestBool(t *testing.T) { + t.Parallel() + + t.Run("complete default off", func(t *testing.T) { + var cmd FlagSet + _ = cmd.Bool("a", false, "") + complete.Test(t, complete.FlagSet((*flag.FlagSet)(&cmd)), "-a ", []string{"-a", "-h"}) + }) + + t.Run("complete default on", func(t *testing.T) { + var cmd FlagSet + _ = cmd.Bool("a", true, "") + complete.Test(t, complete.FlagSet((*flag.FlagSet)(&cmd)), "-a ", []string{"false"}) + complete.Test(t, complete.FlagSet((*flag.FlagSet)(&cmd)), "-a=", []string{"false"}) + }) +} + +func TestString(t *testing.T) { + t.Parallel() + + t.Run("options invalid not checked", func(t *testing.T) { + var cmd FlagSet + value := cmd.String("a", "", "", OptValues("1", "2")) + err := cmd.Parse([]string{"-a", "3"}) + assert.NoError(t, err) + assert.Equal(t, "3", *value) + }) + + t.Run("options valid checked", func(t *testing.T) { + var cmd FlagSet + value := cmd.String("a", "", "", OptValues("1", "2"), OptCheck()) + err := cmd.Parse([]string{"-a", "2"}) + assert.NoError(t, err) + assert.Equal(t, "2", *value) + }) + + t.Run("options invalid checked", func(t *testing.T) { + var cmd FlagSet + _ = cmd.String("a", "", "", OptValues("1", "2"), OptCheck()) + err := cmd.Parse([]string{"-a", "3"}) + assert.Error(t, err) + }) + + t.Run("complete", func(t *testing.T) { + var cmd FlagSet + _ = cmd.String("a", "", "", OptValues("1", "2")) + complete.Test(t, complete.FlagSet((*flag.FlagSet)(&cmd)), "-a ", []string{"1", "2"}) + complete.Test(t, complete.FlagSet((*flag.FlagSet)(&cmd)), "-a=", []string{"1", "2"}) + complete.Test(t, complete.FlagSet((*flag.FlagSet)(&cmd)), "-a 1", []string{"1"}) + complete.Test(t, complete.FlagSet((*flag.FlagSet)(&cmd)), "-a=1", []string{"1"}) + }) +} + +func TestInt(t *testing.T) { + t.Parallel() + + t.Run("options invalid not checked", func(t *testing.T) { + var cmd FlagSet + value := cmd.Int("a", 0, "", OptValues("1", "2")) + err := cmd.Parse([]string{"-a", "3"}) + assert.NoError(t, err) + assert.Equal(t, 3, *value) + }) + + t.Run("options valid checked", func(t *testing.T) { + var cmd FlagSet + value := cmd.Int("a", 0, "", OptValues("1", "2"), OptCheck()) + err := cmd.Parse([]string{"-a", "2"}) + assert.NoError(t, err) + assert.Equal(t, 2, *value) + }) + + t.Run("options invalid checked", func(t *testing.T) { + var cmd FlagSet + _ = cmd.Int("a", 0, "", OptValues("1", "2"), OptCheck()) + err := cmd.Parse([]string{"-a", "3"}) + assert.Error(t, err) + }) + + t.Run("options invalid int value", func(t *testing.T) { + var cmd FlagSet + _ = cmd.Int("a", 0, "", OptValues("1", "2", "x"), OptCheck()) + err := cmd.Parse([]string{"-a", "x"}) + assert.Error(t, err) + }) + + t.Run("complete", func(t *testing.T) { + var cmd FlagSet + _ = cmd.Int("a", 0, "", OptValues("1", "2")) + complete.Test(t, complete.FlagSet((*flag.FlagSet)(&cmd)), "-a ", []string{"1", "2"}) + complete.Test(t, complete.FlagSet((*flag.FlagSet)(&cmd)), "-a=", []string{"1", "2"}) + complete.Test(t, complete.FlagSet((*flag.FlagSet)(&cmd)), "-a 1", []string{"1"}) + complete.Test(t, complete.FlagSet((*flag.FlagSet)(&cmd)), "-a=1", []string{"1"}) + }) +} diff --git a/complete.go b/complete.go index 423cbec..7e4a3d5 100644 --- a/complete.go +++ b/complete.go @@ -1,104 +1,332 @@ package complete import ( - "flag" "fmt" "io" "os" "strconv" "strings" - "github.com/posener/complete/cmd" + "github.com/posener/complete/internal/arg" + "github.com/posener/complete/internal/install" + "github.com/posener/complete/internal/tokener" ) -const ( - envLine = "COMP_LINE" - envPoint = "COMP_POINT" - envDebug = "COMP_DEBUG" +// Completer is an interface that a command line should implement in order to get bash completion. +type Completer interface { + // SubCmdList should return the list of all sub commands of the current command. + SubCmdList() []string + // SubCmdGet should return a sub command of the current command for the given sub command name. + SubCmdGet(cmd string) Completer + // FlagList should return a list of all the flag names of the current command. The flag names + // should not have the dash prefix. + FlagList() []string + // FlagGet should return completion options for a given flag. It is invoked with the flag name + // without the dash prefix. The flag is not promised to be in the command flags. In that case, + // this method should return a nil predictor. + FlagGet(flag string) Predictor + // ArgsGet should return predictor for positional arguments of the command line. + ArgsGet() Predictor +} + +// Predictor can predict completion options. +type Predictor interface { + // Predict returns prediction options for a given prefix. The prefix is what currently is typed + // as a hint for what to return, but the returned values can have any prefix. The returned + // values will be filtered by the prefix when needed regardless. The prefix may be empty which + // means that no value was typed. + Predict(prefix string) []string +} + +// PredictFunc is a function that implements the Predictor interface. +type PredictFunc func(prefix string) []string + +func (p PredictFunc) Predict(prefix string) []string { + if p == nil { + return nil + } + return p(prefix) +} + +var ( + getEnv = os.Getenv + exit = os.Exit + out io.Writer = os.Stdout + in io.Reader = os.Stdin ) -// Complete structs define completion for a command with CLI options -type Complete struct { - Command Command - cmd.CLI - Out io.Writer -} - -// New creates a new complete command. -// name is the name of command we want to auto complete. -// IMPORTANT: it must be the same name - if the auto complete -// completes the 'go' command, name must be equal to "go". -// command is the struct of the command completion. -func New(name string, command Command) *Complete { - return &Complete{ - Command: command, - CLI: cmd.CLI{Name: name}, - Out: os.Stdout, +// Complete the command line arguments for the given command in the case that the program +// was invoked with COMP_LINE and COMP_POINT environment variables. In that case it will also +// `os.Exit()`. The program name should be provided for installation purposes. +func Complete(name string, cmd Completer) { + var ( + line = getEnv("COMP_LINE") + point = getEnv("COMP_POINT") + doInstall = getEnv("COMP_INSTALL") == "1" + doUninstall = getEnv("COMP_UNINSTALL") == "1" + yes = getEnv("COMP_YES") == "1" + ) + if doInstall || doUninstall { + install.Run(name, doUninstall, yes, out, in) + exit(0) + return } -} - -// Run runs the completion and add installation flags beforehand. -// The flags are added to the main flag CommandLine variable. -func (c *Complete) Run() bool { - c.AddFlags(nil) - flag.Parse() - return c.Complete() -} - -// Complete a command from completion line in environment variable, -// and print out the complete options. -// returns success if the completion ran or if the cli matched -// any of the given flags, false otherwise -// For installation: it assumes that flags were added and parsed before -// it was called. -func (c *Complete) Complete() bool { - line, point, ok := getEnv() - if !ok { - // make sure flags parsed, - // in case they were not added in the main program - return c.CLI.Run() - } - - if point >= 0 && point < len(line) { - line = line[:point] - } - - Log("Completing phrase: %s", line) - a := newArgs(line) - Log("Completing last field: %s", a.Last) - options := c.Command.Predict(a) - Log("Options: %s", options) - - // filter only options that match the last argument - matches := []string{} - for _, option := range options { - if strings.HasPrefix(option, a.Last) { - matches = append(matches, option) - } - } - Log("Matches: %s", matches) - c.output(matches) - return true -} - -func getEnv() (line string, point int, ok bool) { - line = os.Getenv(envLine) if line == "" { return } - point, err := strconv.Atoi(os.Getenv(envPoint)) + i, err := strconv.Atoi(point) if err != nil { - // If failed parsing point for some reason, set it to point - // on the end of the line. - Log("Failed parsing point %s: %v", os.Getenv(envPoint), err) - point = len(line) + panic("COMP_POINT env should be integer, got: " + point) } - return line, point, true + + // Parse the command line up to the completion point. + args := arg.Parse(line[:i]) + + // The first word is the current command name. + args = args[1:] + + // Run the completion algorithm. + options, err := completer{Completer: cmd, args: args}.complete() + if err != nil { + fmt.Fprintln(out, "\n"+err.Error()) + } else { + for _, option := range options { + fmt.Fprintln(out, option) + } + } + exit(0) } -func (c *Complete) output(options []string) { - // stdout of program defines the complete options - for _, option := range options { - fmt.Fprintln(c.Out, option) +type completer struct { + Completer + args []arg.Arg + stack []Completer +} + +// compete command with given before and after text. +// if the command has sub commands: try to complete only sub commands or help flags. Otherwise +// complete flags and positional arguments. +func (c completer) complete() ([]string, error) { +reset: + arg := arg.Arg{} + if len(c.args) > 0 { + arg = c.args[0] + } + switch { + case len(c.SubCmdList()) == 0: + // No sub commands, parse flags and positional arguments. + return c.suggestLeafCommandOptions(), nil + + // case !arg.Completed && arg.IsFlag(): + // Suggest help flags for command + // return []string{helpFlag(arg.Text)}, nil + + case !arg.Completed: + // Currently typing a sub command. + return c.suggestSubCommands(arg.Text), nil + + case c.SubCmdGet(arg.Text) != nil: + // Sub command completed, look into that sub command completion. + // Set the complete command to the requested sub command, and the before text to all the text + // after the command name and rerun the complete algorithm with the new sub command. + c.stack = append([]Completer{c.Completer}, c.stack...) + c.Completer = c.SubCmdGet(arg.Text) + c.args = c.args[1:] + goto reset + + default: + + // Sub command is unknown... + return nil, fmt.Errorf("unknown subcommand: %s", arg.Text) } } + +func (c completer) suggestSubCommands(prefix string) []string { + if len(prefix) > 0 && prefix[0] == '-' { + return []string{helpFlag(prefix)} + } + subs := c.SubCmdList() + return suggest("", prefix, func(prefix string) []string { + var options []string + for _, sub := range subs { + if strings.HasPrefix(sub, prefix) { + options = append(options, sub) + } + } + return options + }) +} + +func (c completer) suggestLeafCommandOptions() (options []string) { + arg, before := arg.Arg{}, arg.Arg{} + if len(c.args) > 0 { + arg = c.args[len(c.args)-1] + } + if len(c.args) > 1 { + before = c.args[len(c.args)-2] + } + + if !arg.Completed { + // Complete value being typed. + if arg.HasValue { + // Complete value of current flag. + if arg.HasFlag { + return c.suggestFlagValue(arg.Flag, arg.Value) + } + // Complete value of flag in a previous argument. + if before.HasFlag && !before.HasValue { + return c.suggestFlagValue(before.Flag, arg.Value) + } + } + + // A value with no flag. Suggest positional argument. + if !arg.HasValue { + options = c.suggestFlag(arg.Dashes, arg.Flag) + } + if !arg.HasFlag { + options = append(options, c.suggestArgsValue(arg.Value)...) + } + // Suggest flag according to prefix. + return options + } + + // Has a value that was already completed. Suggest all flags and positional arguments. + if arg.HasValue { + options = c.suggestFlag(arg.Dashes, "") + if !arg.HasFlag { + options = append(options, c.suggestArgsValue("")...) + } + return options + } + // A flag without a value. Suggest a value or suggest any flag. + options = c.suggestFlagValue(arg.Flag, "") + if len(options) > 0 { + return options + } + return c.suggestFlag("", "") +} + +func (c completer) suggestFlag(dashes, prefix string) []string { + if dashes == "" { + dashes = "-" + } + return suggest(dashes, prefix, func(prefix string) []string { + var options []string + c.iterateStack(func(cmd Completer) { + // Suggest all flags with the given prefix. + for _, name := range cmd.FlagList() { + if strings.HasPrefix(name, prefix) { + options = append(options, dashes+name) + } + } + }) + return options + }) +} + +func (c completer) suggestFlagValue(flagName, prefix string) []string { + var options []string + c.iterateStack(func(cmd Completer) { + if len(options) == 0 { + if p := cmd.FlagGet(flagName); p != nil { + options = p.Predict(prefix) + } + } + }) + return filterByPrefix(prefix, options...) +} + +func (c completer) suggestArgsValue(prefix string) []string { + var options []string + c.iterateStack(func(cmd Completer) { + if len(options) == 0 { + if p := cmd.ArgsGet(); p != nil { + options = p.Predict(prefix) + } + } + }) + return filterByPrefix(prefix, options...) +} + +func (c completer) iterateStack(f func(Completer)) { + for _, cmd := range append([]Completer{c.Completer}, c.stack...) { + f(cmd) + } +} + +func suggest(dashes, prefix string, collect func(prefix string) []string) []string { + options := collect(prefix) + // If nothing was suggested, suggest all flags. + if len(options) == 0 { + prefix = "" + options = collect(prefix) + } + + // Add help flag if needed. + help := helpFlag(dashes + prefix) + if len(options) == 0 || strings.HasPrefix(help, dashes+prefix) { + options = append(options, help) + } + + return options +} + +func filterByPrefix(prefix string, options ...string) []string { + var filtered []string + for _, option := range options { + if fixed, ok := hasPrefix(option, prefix); ok { + filtered = append(filtered, fixed) + } + } + if len(filtered) > 0 { + return filtered + } + return options +} + +// hasPrefix checks if s has the give prefix. It disregards quotes and escaped spaces, and return +// s in the form of the given prefix. +func hasPrefix(s, prefix string) (string, bool) { + var ( + token tokener.Tokener + si, pi int + ) + for ; pi < len(prefix); pi++ { + token.Visit(prefix[pi]) + lastQuote := !token.Escaped() && (prefix[pi] == '"' || prefix[pi] == '\'') + if lastQuote { + continue + } + if si == len(s) { + break + } + if s[si] == ' ' && !token.Quoted() && token.Escaped() { + s = s[:si] + "\\" + s[si:] + } + if s[si] != prefix[pi] { + return "", false + } + si++ + } + + if pi < len(prefix) { + return "", false + } + + for ; si < len(s); si++ { + token.Visit(s[si]) + } + + return token.Closed(), true +} + +// helpFlag returns either "-h", "-help" or "--help". +func helpFlag(prefix string) string { + if prefix == "" || prefix == "-" || prefix == "-h" { + return "-h" + } + if strings.HasPrefix(prefix, "--") { + return "--help" + } + return "-help" +} diff --git a/complete_test.go b/complete_test.go index 7125223..4752230 100644 --- a/complete_test.go +++ b/complete_test.go @@ -1,414 +1,246 @@ package complete import ( - "bytes" - "fmt" + "io/ioutil" "os" - "sort" - "strconv" - "strings" "testing" + + "github.com/posener/complete/internal/arg" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -func TestCompleter_Complete(t *testing.T) { - initTests() +var testCmd = &Command{ + Flags: map[string]Predictor{"cmd-flag": nil}, + Sub: map[string]*Command{ + "flags": &Command{ + Flags: map[string]Predictor{ + "values": set{"a", "a a", "b"}, + "something": set{""}, + "nothing": nil, + }, + }, + "sub1": &Command{ + Flags: map[string]Predictor{"flag1": nil}, + Sub: map[string]*Command{ + "sub11": &Command{ + Flags: map[string]Predictor{"flag11": nil}, + }, + "sub12": &Command{}, + }, + Args: set{"arg1", "arg2"}, + }, + "sub2": &Command{}, + "args": &Command{ + Args: set{"a", "a a", "b"}, + }, + }, +} - c := Command{ - Sub: Commands{ - "sub1": { - Flags: Flags{ - "-flag1": PredictAnything, - "-flag2": PredictNothing, - }, - Sub: Commands{ - "sub11": {}, - }, - }, - "sub2": { - Flags: Flags{ - "-flag2": PredictNothing, - "-flag3": PredictSet("opt1", "opt2", "opt12"), - }, - Args: PredictFiles("*.md"), - }, - "sub3": { - Sub: Commands{ - "sub3": {}, - }, - }, - }, - Flags: Flags{ - "-o": PredictFiles("*.txt"), - }, - GlobalFlags: Flags{ - "-h": PredictNothing, - "-global1": PredictAnything, - }, - } - cmp := New("cmd", c) +func TestCompleter(t *testing.T) { + t.Parallel() tests := []struct { - line string - point int // -1 indicates len(line) - want []string + args string + want []string }{ - { - line: "cmd ", - point: -1, - want: []string{"sub1", "sub2", "sub3"}, - }, - { - line: "cmd -", - point: -1, - want: []string{"-h", "-global1", "-o"}, - }, - { - line: "cmd -h ", - point: -1, - want: []string{"sub1", "sub2", "sub3"}, - }, - { - line: "cmd -global1 ", // global1 is known follow flag - point: -1, - want: []string{}, - }, - { - line: "cmd sub", - point: -1, - want: []string{"sub1", "sub2", "sub3"}, - }, - { - line: "cmd sub1", - point: -1, - want: []string{"sub1"}, - }, - { - line: "cmd sub2", - point: -1, - want: []string{"sub2"}, - }, - { - line: "cmd sub1 ", - point: -1, - want: []string{"sub11"}, - }, - { - line: "cmd sub3 ", - point: -1, - want: []string{"sub3"}, - }, - { - line: "cmd sub1 -", - point: -1, - want: []string{"-flag1", "-flag2", "-h", "-global1"}, - }, - { - line: "cmd sub2 ", - point: -1, - want: []string{"./", "dir/", "outer/", "readme.md"}, - }, - { - line: "cmd sub2 ./", - point: -1, - want: []string{"./", "./readme.md", "./dir/", "./outer/"}, - }, - { - line: "cmd sub2 re", - point: -1, - want: []string{"readme.md"}, - }, - { - line: "cmd sub2 ./re", - point: -1, - want: []string{"./readme.md"}, - }, - { - line: "cmd sub2 -flag2 ", - point: -1, - want: []string{"./", "dir/", "outer/", "readme.md"}, - }, - { - line: "cmd sub1 -fl", - point: -1, - want: []string{"-flag1", "-flag2"}, - }, - { - line: "cmd sub1 -flag1", - point: -1, - want: []string{"-flag1"}, - }, - { - line: "cmd sub1 -flag1 ", - point: -1, - want: []string{}, // flag1 is unknown follow flag - }, - { - line: "cmd sub1 -flag2 -", - point: -1, - want: []string{"-flag1", "-flag2", "-h", "-global1"}, - }, - { - line: "cmd -no-such-flag", - point: -1, - want: []string{}, - }, - { - line: "cmd -no-such-flag ", - point: -1, - want: []string{"sub1", "sub2", "sub3"}, - }, - { - line: "cmd -no-such-flag -", - point: -1, - want: []string{"-h", "-global1", "-o"}, - }, - { - line: "cmd no-such-command", - point: -1, - want: []string{}, - }, - { - line: "cmd no-such-command ", - point: -1, - want: []string{"sub1", "sub2", "sub3"}, - }, - { - line: "cmd -o ", - point: -1, - want: []string{"a.txt", "b.txt", "c.txt", ".dot.txt", "./", "dir/", "outer/"}, - }, - { - line: "cmd -o ./no-su", - point: -1, - want: []string{}, - }, - { - line: "cmd -o ./", - point: -1, - want: []string{"./a.txt", "./b.txt", "./c.txt", "./.dot.txt", "./", "./dir/", "./outer/"}, - }, - { - line: "cmd -o=./", - point: -1, - want: []string{"./a.txt", "./b.txt", "./c.txt", "./.dot.txt", "./", "./dir/", "./outer/"}, - }, - { - line: "cmd -o .", - point: -1, - want: []string{"./a.txt", "./b.txt", "./c.txt", "./.dot.txt", "./", "./dir/", "./outer/"}, - }, - { - line: "cmd -o ./b", - point: -1, - want: []string{"./b.txt"}, - }, - { - line: "cmd -o=./b", - point: -1, - want: []string{"./b.txt"}, - }, - { - line: "cmd -o ./read", - point: -1, - want: []string{}, - }, - { - line: "cmd -o=./read", - point: -1, - want: []string{}, - }, - { - line: "cmd -o ./readme.md", - point: -1, - want: []string{}, - }, - { - line: "cmd -o ./readme.md ", - point: -1, - want: []string{"sub1", "sub2", "sub3"}, - }, - { - line: "cmd -o=./readme.md ", - point: -1, - want: []string{"sub1", "sub2", "sub3"}, - }, - { - line: "cmd -o sub2 -flag3 ", - point: -1, - want: []string{"opt1", "opt2", "opt12"}, - }, - { - line: "cmd -o sub2 -flag3 opt1", - point: -1, - want: []string{"opt1", "opt12"}, - }, - { - line: "cmd -o sub2 -flag3 opt", - point: -1, - want: []string{"opt1", "opt2", "opt12"}, - }, - { - line: "cmd -o ./b foo", - // ^ - point: 10, - want: []string{"./b.txt"}, - }, - { - line: "cmd -o=./b foo", - // ^ - point: 10, - want: []string{"./b.txt"}, - }, - { - line: "cmd -o sub2 -flag3 optfoo", - // ^ - point: 22, - want: []string{"opt1", "opt2", "opt12"}, - }, - { - line: "cmd -o ", - // ^ - point: 4, - want: []string{"sub1", "sub2", "sub3"}, - }, + // Check empty flag name matching. + + {args: "flags ", want: []string{"-values", "-nothing", "-something", "-cmd-flag", "-h"}}, + {args: "flags -", want: []string{"-values", "-nothing", "-something", "-cmd-flag", "-h"}}, + {args: "flags --", want: []string{"--values", "--nothing", "--something", "--cmd-flag", "--help"}}, + // If started a flag with no matching prefix, expect to see all possible flags. + {args: "flags -x", want: []string{"-values", "-nothing", "-something", "-cmd-flag", "-h"}}, + // Check prefix matching for chain of sub commands. + {args: "sub1 sub11 -fl", want: []string{"-flag11", "-flag1"}}, + {args: "sub1 sub11 --fl", want: []string{"--flag11", "--flag1"}}, + + // Test sub command completion. + + {args: "", want: []string{"flags", "sub1", "sub2", "args", "-h"}}, + {args: " ", want: []string{"flags", "sub1", "sub2", "args", "-h"}}, + {args: "f", want: []string{"flags"}}, + {args: "sub", want: []string{"sub1", "sub2"}}, + {args: "sub1", want: []string{"sub1"}}, + {args: "sub1 ", want: []string{"sub11", "sub12", "-h"}}, + // Suggest all sub commands if prefix is not known. + {args: "x", want: []string{"flags", "sub1", "sub2", "args", "-h"}}, + + // Suggest flag value. + + // A flag that has an empty completion should return empty completion. It "completes + // something"... But it doesn't know what, so we should not complete anything else. + {args: "flags -something ", want: []string{""}}, + {args: "flags -something foo", want: []string{""}}, + // A flag that have nil completion should complete all other options. + {args: "flags -nothing ", want: []string{"-values", "-nothing", "-something", "-cmd-flag", "-h"}}, + // Trying to provide a value to the nothing flag should revert the phrase back to nothing. + {args: "flags -nothing=", want: []string{}}, + // The flag value was not started, suggest all relevant values. + {args: "flags -values ", want: []string{"a", "a\\ a", "b"}}, + {args: "flags -values a", want: []string{"a", "a\\ a"}}, + {args: "flags -values a\\", want: []string{"a\\ a"}}, + {args: "flags -values a\\ ", want: []string{"a\\ a"}}, + {args: "flags -values a\\ a", want: []string{"a\\ a"}}, + {args: "flags -values a\\ a ", want: []string{"-values", "-nothing", "-something", "-cmd-flag", "-h"}}, + {args: "flags -values \"a", want: []string{"\"a\"", "\"a a\""}}, + {args: "flags -values \"a ", want: []string{"\"a a\""}}, + {args: "flags -values \"a a", want: []string{"\"a a\""}}, + {args: "flags -values \"a a\"", want: []string{"\"a a\""}}, + {args: "flags -values \"a a\" ", want: []string{"-values", "-nothing", "-something", "-cmd-flag", "-h"}}, + + {args: "flags -values=", want: []string{"a", "a\\ a", "b"}}, + {args: "flags -values=a", want: []string{"a", "a\\ a"}}, + {args: "flags -values=a\\", want: []string{"a\\ a"}}, + {args: "flags -values=a\\ ", want: []string{"a\\ a"}}, + {args: "flags -values=a\\ a", want: []string{"a\\ a"}}, + {args: "flags -values=a\\ a ", want: []string{"-values", "-nothing", "-something", "-cmd-flag", "-h"}}, + {args: "flags -values=\"a", want: []string{"\"a\"", "\"a a\""}}, + {args: "flags -values=\"a ", want: []string{"\"a a\""}}, + {args: "flags -values=\"a a", want: []string{"\"a a\""}}, + {args: "flags -values=\"a a\"", want: []string{"\"a a\""}}, + {args: "flags -values=\"a a\" ", want: []string{"-values", "-nothing", "-something", "-cmd-flag", "-h"}}, + + // Complete positional arguments + + {args: "args ", want: []string{"-cmd-flag", "-h", "a", "a\\ a", "b"}}, + {args: "args a", want: []string{"a", "a\\ a"}}, + {args: "args a\\", want: []string{"a\\ a"}}, + {args: "args a\\ ", want: []string{"a\\ a"}}, + {args: "args a\\ a", want: []string{"a\\ a"}}, + {args: "args a\\ a ", want: []string{"-cmd-flag", "-h", "a", "a\\ a", "b"}}, + {args: "args \"a", want: []string{"\"a\"", "\"a a\""}}, + {args: "args \"a ", want: []string{"\"a a\""}}, + {args: "args \"a a", want: []string{"\"a a\""}}, + {args: "args \"a a\"", want: []string{"\"a a\""}}, + {args: "args \"a a\" ", want: []string{"-cmd-flag", "-h", "a", "a\\ a", "b"}}, + + // Complete positional arguments from a parent command + {args: "sub1 sub12 arg", want: []string{"arg1", "arg2"}}, + + // Test help + + {args: "-", want: []string{"-h"}}, + {args: " -", want: []string{"-h"}}, + {args: "--", want: []string{"--help"}}, + {args: "-he", want: []string{"-help"}}, + {args: "-x", want: []string{"-help"}}, } for _, tt := range tests { - t.Run(fmt.Sprintf("%s@%d", tt.line, tt.point), func(t *testing.T) { - got := runComplete(cmp, tt.line, tt.point) + t.Run(tt.args, func(t *testing.T) { + Test(t, testCmd, tt.args, tt.want) + }) + } +} - sort.Strings(tt.want) - sort.Strings(got) +func TestCompleter_error(t *testing.T) { + t.Parallel() - if !equalSlices(got, tt.want) { - t.Errorf("failed '%s'\ngot: %s\nwant: %s", t.Name(), got, tt.want) + tests := []struct { + args string + err string + }{ + // Sub command already fully typed but unknown. + {args: "x ", err: "unknown subcommand: x"}, + } + + for _, tt := range tests { + t.Run(tt.args, func(t *testing.T) { + _, err := completer{Completer: testCmd, args: arg.Parse(tt.args)}.complete() + require.Error(t, err) + assert.Equal(t, tt.err, err.Error()) + }) + } +} + +func TestComplete(t *testing.T) { + defer func() { + getEnv = os.Getenv + exit = os.Exit + out = os.Stdout + }() + + tests := []struct { + line, point string + shouldExit bool + shouldPanic bool + install string + uninstall string + }{ + {shouldExit: true, line: "cmd", point: "1"}, + {shouldExit: false, line: "", point: ""}, + {shouldPanic: true, line: "cmd", point: ""}, + {shouldPanic: true, line: "cmd", point: "a"}, + {shouldPanic: true, line: "cmd", point: "4"}, + + {shouldExit: true, install: "1"}, + {shouldExit: false, install: "a"}, + {shouldExit: true, uninstall: "1"}, + {shouldExit: false, uninstall: "a"}, + } + + for _, tt := range tests { + t.Run(tt.line+"@"+tt.point, func(t *testing.T) { + getEnv = func(env string) string { + switch env { + case "COMP_LINE": + return tt.line + case "COMP_POINT": + return tt.point + case "COMP_INSTALL": + return tt.install + case "COMP_UNINSTALL": + return tt.uninstall + case "COMP_YES": + return "0" + default: + panic(env) + } + } + isExit := false + exit = func(int) { + isExit = true + } + out = ioutil.Discard + if tt.shouldPanic { + assert.Panics(t, func() { testCmd.Complete("") }) + } else { + testCmd.Complete("") + assert.Equal(t, tt.shouldExit, isExit) } }) } } -func TestCompleter_Complete_SharedPrefix(t *testing.T) { - initTests() +type set []string - c := Command{ - Sub: Commands{ - "status": { - Flags: Flags{ - "-f3": PredictNothing, - }, - }, - "job": { - Sub: Commands{ - "status": { - Flags: Flags{ - "-f4": PredictNothing, - }, - }, - }, - }, - }, - Flags: Flags{ - "-o": PredictFiles("*.txt"), - }, - GlobalFlags: Flags{ - "-h": PredictNothing, - "-global1": PredictAnything, - }, - } +func (s set) Predict(_ string) []string { + return s +} - cmp := New("cmd", c) +func TestHasPrefix(t *testing.T) { + t.Parallel() tests := []struct { - line string - point int // -1 indicates len(line) - want []string + s string + prefix string + want string + wantOK bool }{ - { - line: "cmd ", - point: -1, - want: []string{"status", "job"}, - }, - { - line: "cmd -", - point: -1, - want: []string{"-h", "-global1", "-o"}, - }, - { - line: "cmd j", - point: -1, - want: []string{"job"}, - }, - { - line: "cmd job ", - point: -1, - want: []string{"status"}, - }, - { - line: "cmd job -", - point: -1, - want: []string{"-h", "-global1"}, - }, - { - line: "cmd job status ", - point: -1, - want: []string{}, - }, - { - line: "cmd job status -", - point: -1, - want: []string{"-f4", "-h", "-global1"}, - }, + {s: "ab", prefix: `b`, want: ``, wantOK: false}, + {s: "", prefix: `b`, want: ``, wantOK: false}, + {s: "ab", prefix: `a`, want: `ab`, wantOK: true}, + {s: "ab", prefix: `"'b`, want: ``, wantOK: false}, + {s: "ab", prefix: `"'a`, want: `"'ab'"`, wantOK: true}, + {s: "ab", prefix: `'"a`, want: `'"ab"'`, wantOK: true}, } for _, tt := range tests { - t.Run(tt.line, func(t *testing.T) { - got := runComplete(cmp, tt.line, tt.point) - - sort.Strings(tt.want) - sort.Strings(got) - - if !equalSlices(got, tt.want) { - t.Errorf("failed '%s'\ngot = %s\nwant: %s", t.Name(), got, tt.want) - } + t.Run(tt.s+"/"+tt.prefix, func(t *testing.T) { + got, gotOK := hasPrefix(tt.s, tt.prefix) + assert.Equal(t, tt.want, got) + assert.Equal(t, tt.wantOK, gotOK) }) } } - -// runComplete runs the complete login for test purposes -// it gets the complete struct and command line arguments and returns -// the complete options -func runComplete(c *Complete, line string, point int) (completions []string) { - if point == -1 { - point = len(line) - } - os.Setenv(envLine, line) - os.Setenv(envPoint, strconv.Itoa(point)) - b := bytes.NewBuffer(nil) - c.Out = b - c.Complete() - completions = parseOutput(b.String()) - return -} - -func parseOutput(output string) []string { - lines := strings.Split(output, "\n") - options := []string{} - for _, l := range lines { - if l != "" { - options = append(options, l) - } - } - return options -} - -func equalSlices(a, b []string) bool { - if len(a) != len(b) { - return false - } - for i := range a { - if a[i] != b[i] { - return false - } - } - return true -} diff --git a/doc.go b/doc.go index 0ae09a1..470dcf5 100644 --- a/doc.go +++ b/doc.go @@ -1,35 +1,46 @@ /* -Package complete provides a tool for bash writing bash completion in go, and bash completion for the go command line. +Package complete is everything for bash completion and Go. -Writing bash completion scripts is a hard work. This package provides an easy way -to create bash completion scripts for any command, and also an easy way to install/uninstall -the completion of the command. +Writing bash completion scripts is a hard work, usually done in the bash scripting language. +This package provides: + +* A library for bash completion for Go programs. + +* A tool for writing bash completion script in the Go language. For any Go or non Go program. + +* Bash completion for the `go` command line (See ./gocomplete). + +* Library for bash-completion enabled flags (See ./compflag). + +* Enables an easy way to install/uninstall the completion of the command. + +The library and tools are extensible such that any program can add its one logic, completion types +or methologies. Go Command Bash Completion -In ./cmd/gocomplete there is an example for bash completion for the `go` command line. +./gocomplete is the script for bash completion for the `go` command line. This is an example +that uses the `complete` package on the `go` command - the `complete` package can also be used to +implement any completions, see #usage. -This is an example that uses the `complete` package on the `go` command - the `complete` package -can also be used to implement any completions, see #usage. - -Install +Install: 1. Type in your shell: go get -u github.com/posener/complete/gocomplete - gocomplete -install + COMP_INSTALL=1 gocomplete 2. Restart your shell -Uninstall by `gocomplete -uninstall` +Uninstall by `COMP_UNINSTALL=1 gocomplete` -Features +Features: -- Complete `go` command, including sub commands and all flags. +- Complete `go` command, including sub commands and flags. - Complete packages names or `.go` files when necessary. - Complete test names after `-run` flag. -Complete package +Complete Package Supported shells: @@ -39,72 +50,83 @@ Supported shells: Usage -Assuming you have program called `run` and you want to have bash completion -for it, meaning, if you type `run` then space, then press the `Tab` key, -the shell will suggest relevant complete options. +Add bash completion capabilities to any Go program. See ./example/command. -In that case, we will create a program called `runcomplete`, a go program, -with a `func main()` and so, that will make the completion of the `run` -program. Once the `runcomplete` will be in a binary form, we could -`runcomplete -install` and that will add to our shell all the bash completion -options for `run`. + import ( + "flag" + "github.com/posener/complete" + "github.com/posener/complete/predict" + ) -So here it is: - - import "github.com/posener/complete" + var ( + // Add variables to the program. + name = flag.String("name", "", "") + something = flag.String("something", "", "") + nothing = flag.String("nothing", "", "") + ) func main() { - - // create a Command object, that represents the command we want - // to complete. - run := complete.Command{ - - // Sub defines a list of sub commands of the program, - // this is recursive, since every command is of type command also. - Sub: complete.Commands{ - - // add a build sub command - "build": complete.Command { - - // define flags of the build sub command - Flags: complete.Flags{ - // build sub command has a flag '-cpus', which - // expects number of cpus after it. in that case - // anything could complete this flag. - "-cpus": complete.PredictAnything, - }, - }, - }, - - // define flags of the 'run' main command - Flags: complete.Flags{ - // a flag -o, which expects a file ending with .out after - // it, the tab completion will auto complete for files matching - // the given pattern. - "-o": complete.PredictFiles("*.out"), - }, - - // define global flags of the 'run' main command - // those will show up also when a sub command was entered in the - // command line - GlobalFlags: complete.Flags{ - - // a flag '-h' which does not expects anything after it - "-h": complete.PredictNothing, + // Create the complete command. + // Here we define completion values for each flag. + cmd := &complete.Command{ + Flags: map[string]complete.Predictor{ + "name": predict.Set{"foo", "bar", "foo bar"}, + "something": predict.Something, + "nothing": predict.Nothing, }, } - - // run the command completion, as part of the main() function. - // this triggers the autocompletion when needed. - // name must be exactly as the binary that we want to complete. - complete.New("run", run).Run() + // Run the completion - provide it with the binary name. + cmd.Complete("my-program") + // Parse the flags. + flag.Parse() + // Program logic... } -Self completing program +This package also enables to complete flags defined by the standard library `flag` package. +To use this feature, simply call `complete.CommandLine` before `flag.Parse`. (See ./example/stdlib). -In case that the program that we want to complete is written in go we -can make it self completing. -Here is an example: ./example/self/main.go . + import ( + "flag" + + "github.com/posener/complete" + ) + var ( + // Define flags here... + foo = flag.Bool("foo", false, "") + ) + func main() { + // Call command line completion before parsing the flags - provide it with the binary name. + + complete.CommandLine("my-program") + flag.Parse() + } + +If flag value completion is desired, it can be done by providing the standard library `flag.Var` +function a `flag.Value` that also implements the `complete.Predictor` interface. For standard +flag with values, it is possible to use the `github.com/posener/complete/compflag` package. +(See ./example/compflag). + + import ( + "flag" + + "github.com/posener/complete" + + "github.com/posener/complete/compflag" + ) + var ( + // Define flags here... + - foo = flag.Bool("foo", false, "") + + foo = compflag.Bool("foo", false, "") + ) + + func main() { + // Call command line completion before parsing the flags. + + complete.CommandLine("my-program") + flag.Parse() + } + +Instead of calling both `complete.CommandLine` and `flag.Parse`, one can call just `compflag.Parse` +which does them both. + +Testing + +For command line bash completion testing use the `complete.Test` function. */ package complete diff --git a/example/command/main.go b/example/command/main.go new file mode 100644 index 0000000..0b073d9 --- /dev/null +++ b/example/command/main.go @@ -0,0 +1,45 @@ +// command shows how to have bash completion to an arbitrary Go program using the `complete.Command` +// struct. +package main + +import ( + "flag" + "fmt" + "os" + + "github.com/posener/complete" + "github.com/posener/complete/predict" +) + +var ( + // Add variables to the program. + name = flag.String("name", "", "Give your name") + something = flag.String("something", "", "Expect somthing, but we don't know what, so no other completion options will be provided.") + nothing = flag.String("nothing", "", "Expect nothing after flag, so other completion can be provided.") +) + +func main() { + // Create the complete command. + // Here we define completion values for each flag. + cmd := &complete.Command{ + Flags: map[string]complete.Predictor{ + "name": predict.Set{"foo", "bar", "foo bar"}, + "something": predict.Something, + "nothing": predict.Nothing, + }, + } + + // Run the completion. + cmd.Complete("command") + + // Parse the flags. + flag.Parse() + + // Program logic. + if *name == "" { + fmt.Println("Your name is missing") + os.Exit(1) + } + + fmt.Println("Hi,", name) +} diff --git a/example/compflag/main.go b/example/compflag/main.go new file mode 100644 index 0000000..84d82d6 --- /dev/null +++ b/example/compflag/main.go @@ -0,0 +1,31 @@ +// compflag shows how to use the github.com/posener/complete/compflag package to have auto bash +// completion for a defined set of flags. +package main + +import ( + "fmt" + "os" + + "github.com/posener/complete/compflag" +) + +var ( + // Add variables to the program. Since we are using the compflag library, we can pass options to + // enable bash completion to the flag values. + name = compflag.String("name", "", "Give your name", compflag.OptValues("foo", "bar", "foo bar")) + something = compflag.String("something", "", "Expect somthing, but we don't know what, so no other completion options will be provided.", compflag.OptValues("")) + nothing = compflag.String("nothing", "", "Expect nothing after flag, so other completion can be provided.") +) + +func main() { + // Parse flags and perform bash completion if needed. + compflag.Parse("stdlib") + + // Program logic. + if *name == "" { + fmt.Println("Your name is missing") + os.Exit(1) + } + + fmt.Println("Hi,", name) +} diff --git a/example/self/main.go b/example/self/main.go deleted file mode 100644 index 9479e64..0000000 --- a/example/self/main.go +++ /dev/null @@ -1,53 +0,0 @@ -// Package self -// a program that complete itself -package main - -import ( - "flag" - "fmt" - "os" - - "github.com/posener/complete" -) - -func main() { - - // add a variable to the program - var name string - flag.StringVar(&name, "name", "", "Give your name") - - // create the complete command - cmp := complete.New( - "self", - complete.Command{Flags: complete.Flags{"-name": complete.PredictAnything}}, - ) - - // AddFlags adds the completion flags to the program flags, - // in case of using non-default flag set, it is possible to pass - // it as an argument. - // it is possible to set custom flags name - // so when one will type 'self -h', he will see '-complete' to install the - // completion and -uncomplete to uninstall it. - cmp.CLI.InstallName = "complete" - cmp.CLI.UninstallName = "uncomplete" - cmp.AddFlags(nil) - - // parse the flags - both the program's flags and the completion flags - flag.Parse() - - // run the completion, in case that the completion was invoked - // and ran as a completion script or handled a flag that passed - // as argument, the Run method will return true, - // in that case, our program have nothing to do and should return. - if cmp.Complete() { - return - } - - // if the completion did not do anything, we can run our program logic here. - if name == "" { - fmt.Println("Your name is missing") - os.Exit(1) - } - - fmt.Println("Hi,", name) -} diff --git a/example/stdlib/main.go b/example/stdlib/main.go new file mode 100644 index 0000000..03c6391 --- /dev/null +++ b/example/stdlib/main.go @@ -0,0 +1,35 @@ +// stdlib shows how to have flags bash completion to an arbitrary Go program that uses the standard +// library flag package. +package main + +import ( + "flag" + "fmt" + "os" + + "github.com/posener/complete" +) + +var ( + // Add variables to the program. + name = flag.String("name", "", "Give your name") + something = flag.String("something", "", "Expect somthing, but we don't know what, so no other completion options will be provided.") + nothing = flag.String("nothing", "", "Expect nothing after flag, so other completion can be provided.") +) + +func main() { + // Run the completion. Notice that since we are using standard library flags, only the flag + // names will be completed and not their values. + complete.CommandLine("stdlib") + + // Parse the flags. + flag.Parse() + + // Program logic. + if *name == "" { + fmt.Println("Your name is missing") + os.Exit(1) + } + + fmt.Println("Hi,", name) +} diff --git a/flags.go b/flags.go new file mode 100644 index 0000000..7658061 --- /dev/null +++ b/flags.go @@ -0,0 +1,44 @@ +package complete + +import ( + "flag" +) + +// Complete default command line flag set defined by the standard library. +func CommandLine(name string) { + Complete(name, FlagSet(flag.CommandLine)) +} + +// FlagSet returns a completer for a given standard library `flag.FlagSet`. It completes flag names, +// and additionally completes value if the `flag.Value` implements the `Predicate` interface. +func FlagSet(flags *flag.FlagSet) Completer { + return (*flagSet)(flags) +} + +type flagSet flag.FlagSet + +func (fs *flagSet) SubCmdList() []string { return nil } + +func (fs *flagSet) SubCmdGet(cmd string) Completer { return nil } + +func (fs *flagSet) FlagList() []string { + var flags []string + (*flag.FlagSet)(fs).VisitAll(func(f *flag.Flag) { + flags = append(flags, f.Name) + }) + return flags +} + +func (fs *flagSet) FlagGet(name string) Predictor { + f := (*flag.FlagSet)(fs).Lookup(name) + if f == nil { + return nil + } + p, ok := f.Value.(Predictor) + if !ok { + return PredictFunc(func(string) []string { return []string{""} }) + } + return p +} + +func (fs *flagSet) ArgsGet() Predictor { return nil } diff --git a/flags_test.go b/flags_test.go new file mode 100644 index 0000000..374a6cc --- /dev/null +++ b/flags_test.go @@ -0,0 +1,57 @@ +package complete + +import ( + "flag" + "fmt" + "strconv" + "testing" +) + +func TestFlags(t *testing.T) { + t.Parallel() + + var ( + tr boolValue = true + fl boolValue = false + ) + + fs := flag.NewFlagSet("test", flag.ExitOnError) + fs.Var(&tr, "foo", "") + fs.Var(&fl, "bar", "") + fs.String("foo-bar", "", "") + cmp := FlagSet(fs) + + Test(t, cmp, "", []string{"-foo", "-bar", "-foo-bar", "-h"}) + Test(t, cmp, "-foo", []string{"-foo", "-foo-bar"}) + Test(t, cmp, "-foo ", []string{"false"}) + Test(t, cmp, "-foo=", []string{"false"}) + Test(t, cmp, "-bar ", []string{"-foo", "-bar", "-foo-bar", "-h"}) + Test(t, cmp, "-bar=", []string{}) +} + +type boolValue bool + +func (b *boolValue) Set(s string) error { + v, err := strconv.ParseBool(s) + if err != nil { + return fmt.Errorf("bad value %q for bool flag", s) + } + *b = boolValue(v) + return nil +} + +func (b *boolValue) Get() interface{} { return bool(*b) } + +func (b *boolValue) String() string { return strconv.FormatBool(bool(*b)) } + +func (b *boolValue) IsBoolFlag() bool { return true } + +func (b *boolValue) Predict(_ string) []string { + // If false, typing the bool flag is expected to turn it on, so there is nothing to complete + // after the flag. + if *b == false { + return nil + } + // Otherwise, suggest only to turn it off. + return []string{"false"} +} diff --git a/gocomplete/complete.go b/gocomplete/complete.go index 2c31010..3dbb4c9 100644 --- a/gocomplete/complete.go +++ b/gocomplete/complete.go @@ -1,551 +1,554 @@ // Package main is complete tool for the go command line package main -import "github.com/posener/complete" +import ( + "github.com/posener/complete" + "github.com/posener/complete/predict" +) var ( - ellipsis = complete.PredictSet("./...") + ellipsis = predict.Set{"./..."} anyPackage = complete.PredictFunc(predictPackages) - goFiles = complete.PredictFiles("*.go") - anyFile = complete.PredictFiles("*") - anyGo = complete.PredictOr(goFiles, anyPackage, ellipsis) + goFiles = predict.Files("*.go") + anyFile = predict.Files("*") + anyGo = predict.Or(goFiles, anyPackage, ellipsis) ) func main() { - build := complete.Command{ - Flags: complete.Flags{ - "-o": anyFile, - "-i": complete.PredictNothing, + build := &complete.Command{ + Flags: map[string]complete.Predictor{ + "o": anyFile, + "i": predict.Nothing, - "-a": complete.PredictNothing, - "-n": complete.PredictNothing, - "-p": complete.PredictAnything, - "-race": complete.PredictNothing, - "-msan": complete.PredictNothing, - "-v": complete.PredictNothing, - "-work": complete.PredictNothing, - "-x": complete.PredictNothing, - "-asmflags": complete.PredictAnything, - "-buildmode": complete.PredictAnything, - "-compiler": complete.PredictAnything, - "-gccgoflags": complete.PredictSet("gccgo", "gc"), - "-gcflags": complete.PredictAnything, - "-installsuffix": complete.PredictAnything, - "-ldflags": complete.PredictAnything, - "-linkshared": complete.PredictNothing, - "-pkgdir": anyPackage, - "-tags": complete.PredictAnything, - "-toolexec": complete.PredictAnything, + "a": predict.Nothing, + "n": predict.Nothing, + "p": predict.Something, + "race": predict.Nothing, + "msan": predict.Nothing, + "v": predict.Nothing, + "work": predict.Nothing, + "x": predict.Nothing, + "asmflags": predict.Something, + "buildmode": predict.Something, + "compiler": predict.Something, + "gccgoflags": predict.Set{"gccgo", "gc"}, + "gcflags": predict.Something, + "installsuffix": predict.Something, + "ldflags": predict.Something, + "linkshared": predict.Nothing, + "pkgdir": anyPackage, + "tags": predict.Something, + "toolexec": predict.Something, }, Args: anyGo, } - run := complete.Command{ - Flags: complete.Flags{ - "-exec": complete.PredictAnything, + run := &complete.Command{ + Flags: map[string]complete.Predictor{ + "exec": predict.Something, }, Args: goFiles, } - test := complete.Command{ - Flags: complete.Flags{ - "-args": complete.PredictAnything, - "-c": complete.PredictNothing, - "-exec": complete.PredictAnything, + test := &complete.Command{ + Flags: map[string]complete.Predictor{ + "args": predict.Something, + "c": predict.Nothing, + "exec": predict.Something, - "-bench": predictBenchmark, - "-benchtime": complete.PredictAnything, - "-count": complete.PredictAnything, - "-cover": complete.PredictNothing, - "-covermode": complete.PredictSet("set", "count", "atomic"), - "-coverpkg": complete.PredictDirs("*"), - "-cpu": complete.PredictAnything, - "-run": predictTest, - "-short": complete.PredictNothing, - "-timeout": complete.PredictAnything, + "bench": predictBenchmark, + "benchtime": predict.Something, + "count": predict.Something, + "cover": predict.Nothing, + "covermode": predict.Set{"set", "count", "atomic"}, + "coverpkg": predict.Dirs("*"), + "cpu": predict.Something, + "run": predictTest, + "short": predict.Nothing, + "timeout": predict.Something, - "-benchmem": complete.PredictNothing, - "-blockprofile": complete.PredictFiles("*.out"), - "-blockprofilerate": complete.PredictAnything, - "-coverprofile": complete.PredictFiles("*.out"), - "-cpuprofile": complete.PredictFiles("*.out"), - "-memprofile": complete.PredictFiles("*.out"), - "-memprofilerate": complete.PredictAnything, - "-mutexprofile": complete.PredictFiles("*.out"), - "-mutexprofilefraction": complete.PredictAnything, - "-outputdir": complete.PredictDirs("*"), - "-trace": complete.PredictFiles("*.out"), + "benchmem": predict.Nothing, + "blockprofile": predict.Files("*.out"), + "blockprofilerate": predict.Something, + "coverprofile": predict.Files("*.out"), + "cpuprofile": predict.Files("*.out"), + "memprofile": predict.Files("*.out"), + "memprofilerate": predict.Something, + "mutexprofile": predict.Files("*.out"), + "mutexprofilefraction": predict.Something, + "outputdir": predict.Dirs("*"), + "trace": predict.Files("*.out"), }, Args: anyGo, } - fmt := complete.Command{ - Flags: complete.Flags{ - "-n": complete.PredictNothing, - "-x": complete.PredictNothing, + fmt := &complete.Command{ + Flags: map[string]complete.Predictor{ + "n": predict.Nothing, + "x": predict.Nothing, }, Args: anyGo, } - get := complete.Command{ - Flags: complete.Flags{ - "-d": complete.PredictNothing, - "-f": complete.PredictNothing, - "-fix": complete.PredictNothing, - "-insecure": complete.PredictNothing, - "-t": complete.PredictNothing, - "-u": complete.PredictNothing, + get := &complete.Command{ + Flags: map[string]complete.Predictor{ + "d": predict.Nothing, + "f": predict.Nothing, + "fix": predict.Nothing, + "insecure": predict.Nothing, + "t": predict.Nothing, + "u": predict.Nothing, }, Args: anyGo, } - generate := complete.Command{ - Flags: complete.Flags{ - "-n": complete.PredictNothing, - "-x": complete.PredictNothing, - "-v": complete.PredictNothing, - "-run": complete.PredictAnything, + generate := &complete.Command{ + Flags: map[string]complete.Predictor{ + "n": predict.Nothing, + "x": predict.Nothing, + "v": predict.Nothing, + "run": predict.Something, }, Args: anyGo, } - vet := complete.Command{ - Flags: complete.Flags{ - "-n": complete.PredictNothing, - "-x": complete.PredictNothing, + vet := &complete.Command{ + Flags: map[string]complete.Predictor{ + "n": predict.Nothing, + "x": predict.Nothing, }, Args: anyGo, } - list := complete.Command{ - Flags: complete.Flags{ - "-e": complete.PredictNothing, - "-f": complete.PredictAnything, - "-json": complete.PredictNothing, + list := &complete.Command{ + Flags: map[string]complete.Predictor{ + "e": predict.Nothing, + "f": predict.Something, + "json": predict.Nothing, }, - Args: complete.PredictOr(anyPackage, ellipsis), + Args: predict.Or(anyPackage, ellipsis), } - doc := complete.Command{ - Flags: complete.Flags{ - "-c": complete.PredictNothing, - "-cmd": complete.PredictNothing, - "-u": complete.PredictNothing, + doc := &complete.Command{ + Flags: map[string]complete.Predictor{ + "c": predict.Nothing, + "cmd": predict.Nothing, + "u": predict.Nothing, }, Args: anyPackage, } - tool := complete.Command{ - Flags: complete.Flags{ - "-n": complete.PredictNothing, + tool := &complete.Command{ + Flags: map[string]complete.Predictor{ + "n": predict.Nothing, }, - Sub: complete.Commands{ + Sub: map[string]*complete.Command{ "addr2line": { Args: anyFile, }, "asm": { - Flags: complete.Flags{ - "-D": complete.PredictAnything, - "-I": complete.PredictDirs("*"), - "-S": complete.PredictNothing, - "-V": complete.PredictNothing, - "-debug": complete.PredictNothing, - "-dynlink": complete.PredictNothing, - "-e": complete.PredictNothing, - "-o": anyFile, - "-shared": complete.PredictNothing, - "-trimpath": complete.PredictNothing, + Flags: map[string]complete.Predictor{ + "D": predict.Something, + "I": predict.Dirs("*"), + "S": predict.Nothing, + "V": predict.Nothing, + "debug": predict.Nothing, + "dynlink": predict.Nothing, + "e": predict.Nothing, + "o": anyFile, + "shared": predict.Nothing, + "trimpath": predict.Nothing, }, - Args: complete.PredictFiles("*.s"), + Args: predict.Files("*.s"), }, "cgo": { - Flags: complete.Flags{ - "-debug-define": complete.PredictNothing, - "debug-gcc": complete.PredictNothing, + Flags: map[string]complete.Predictor{ + "debug-define": predict.Nothing, + "debug-gcc": predict.Nothing, "dynimport": anyFile, - "dynlinker": complete.PredictNothing, + "dynlinker": predict.Nothing, "dynout": anyFile, "dynpackage": anyPackage, - "exportheader": complete.PredictDirs("*"), - "gccgo": complete.PredictNothing, - "gccgopkgpath": complete.PredictDirs("*"), - "gccgoprefix": complete.PredictAnything, - "godefs": complete.PredictNothing, - "import_runtime_cgo": complete.PredictNothing, - "import_syscall": complete.PredictNothing, - "importpath": complete.PredictDirs("*"), - "objdir": complete.PredictDirs("*"), - "srcdir": complete.PredictDirs("*"), + "exportheader": predict.Dirs("*"), + "gccgo": predict.Nothing, + "gccgopkgpath": predict.Dirs("*"), + "gccgoprefix": predict.Something, + "godefs": predict.Nothing, + "import_runtime_cgo": predict.Nothing, + "import_syscall": predict.Nothing, + "importpath": predict.Dirs("*"), + "objdir": predict.Dirs("*"), + "srcdir": predict.Dirs("*"), }, Args: goFiles, }, "compile": { - Flags: complete.Flags{ - "-%": complete.PredictNothing, - "-+": complete.PredictNothing, - "-B": complete.PredictNothing, - "-D": complete.PredictDirs("*"), - "-E": complete.PredictNothing, - "-I": complete.PredictDirs("*"), - "-K": complete.PredictNothing, - "-N": complete.PredictNothing, - "-S": complete.PredictNothing, - "-V": complete.PredictNothing, - "-W": complete.PredictNothing, - "-asmhdr": anyFile, - "-bench": anyFile, - "-buildid": complete.PredictNothing, - "-complete": complete.PredictNothing, - "-cpuprofile": anyFile, - "-d": complete.PredictNothing, - "-dynlink": complete.PredictNothing, - "-e": complete.PredictNothing, - "-f": complete.PredictNothing, - "-h": complete.PredictNothing, - "-i": complete.PredictNothing, - "-importmap": complete.PredictAnything, - "-installsuffix": complete.PredictAnything, - "-j": complete.PredictNothing, - "-l": complete.PredictNothing, - "-largemodel": complete.PredictNothing, - "-linkobj": anyFile, - "-live": complete.PredictNothing, - "-m": complete.PredictNothing, - "-memprofile": complete.PredictNothing, - "-memprofilerate": complete.PredictAnything, - "-msan": complete.PredictNothing, - "-nolocalimports": complete.PredictNothing, - "-o": anyFile, - "-p": complete.PredictDirs("*"), - "-pack": complete.PredictNothing, - "-r": complete.PredictNothing, - "-race": complete.PredictNothing, - "-s": complete.PredictNothing, - "-shared": complete.PredictNothing, - "-traceprofile": anyFile, - "-trimpath": complete.PredictAnything, - "-u": complete.PredictNothing, - "-v": complete.PredictNothing, - "-w": complete.PredictNothing, - "-wb": complete.PredictNothing, + Flags: map[string]complete.Predictor{ + "%": predict.Nothing, + "+": predict.Nothing, + "B": predict.Nothing, + "D": predict.Dirs("*"), + "E": predict.Nothing, + "I": predict.Dirs("*"), + "K": predict.Nothing, + "N": predict.Nothing, + "S": predict.Nothing, + "V": predict.Nothing, + "W": predict.Nothing, + "asmhdr": anyFile, + "bench": anyFile, + "buildid": predict.Nothing, + "complete": predict.Nothing, + "cpuprofile": anyFile, + "d": predict.Nothing, + "dynlink": predict.Nothing, + "e": predict.Nothing, + "f": predict.Nothing, + "h": predict.Nothing, + "i": predict.Nothing, + "importmap": predict.Something, + "installsuffix": predict.Something, + "j": predict.Nothing, + "l": predict.Nothing, + "largemodel": predict.Nothing, + "linkobj": anyFile, + "live": predict.Nothing, + "m": predict.Nothing, + "memprofile": predict.Nothing, + "memprofilerate": predict.Something, + "msan": predict.Nothing, + "nolocalimports": predict.Nothing, + "o": anyFile, + "p": predict.Dirs("*"), + "pack": predict.Nothing, + "r": predict.Nothing, + "race": predict.Nothing, + "s": predict.Nothing, + "shared": predict.Nothing, + "traceprofile": anyFile, + "trimpath": predict.Something, + "u": predict.Nothing, + "v": predict.Nothing, + "w": predict.Nothing, + "wb": predict.Nothing, }, Args: goFiles, }, "cover": { - Flags: complete.Flags{ - "-func": complete.PredictAnything, - "-html": complete.PredictAnything, - "-mode": complete.PredictSet("set", "count", "atomic"), - "-o": anyFile, - "-var": complete.PredictAnything, + Flags: map[string]complete.Predictor{ + "func": predict.Something, + "html": predict.Something, + "mode": predict.Set{"set", "count", "atomic"}, + "o": anyFile, + "var": predict.Something, }, Args: anyFile, }, "dist": { - Sub: complete.Commands{ - "banner": {Flags: complete.Flags{"-v": complete.PredictNothing}}, - "bootstrap": {Flags: complete.Flags{"-v": complete.PredictNothing}}, - "clean": {Flags: complete.Flags{"-v": complete.PredictNothing}}, - "env": {Flags: complete.Flags{"-v": complete.PredictNothing, "-p": complete.PredictNothing}}, - "install": {Flags: complete.Flags{"-v": complete.PredictNothing}, Args: complete.PredictDirs("*")}, - "list": {Flags: complete.Flags{"-v": complete.PredictNothing, "-json": complete.PredictNothing}}, - "test": {Flags: complete.Flags{"-v": complete.PredictNothing, "-h": complete.PredictNothing}}, - "version": {Flags: complete.Flags{"-v": complete.PredictNothing}}, + Sub: map[string]*complete.Command{ + "banner": {Flags: map[string]complete.Predictor{"v": predict.Nothing}}, + "bootstrap": {Flags: map[string]complete.Predictor{"v": predict.Nothing}}, + "clean": {Flags: map[string]complete.Predictor{"v": predict.Nothing}}, + "env": {Flags: map[string]complete.Predictor{"v": predict.Nothing, "p": predict.Nothing}}, + "install": {Flags: map[string]complete.Predictor{"v": predict.Nothing}, Args: predict.Dirs("*")}, + "list": {Flags: map[string]complete.Predictor{"v": predict.Nothing, "json": predict.Nothing}}, + "test": {Flags: map[string]complete.Predictor{"v": predict.Nothing, "h": predict.Nothing}}, + "version": {Flags: map[string]complete.Predictor{"v": predict.Nothing}}, }, }, "doc": doc, "fix": { - Flags: complete.Flags{ - "-diff": complete.PredictNothing, - "-force": complete.PredictAnything, - "-r": complete.PredictSet("context", "gotypes", "netipv6zone", "printerconfig"), + Flags: map[string]complete.Predictor{ + "diff": predict.Nothing, + "force": predict.Something, + "r": predict.Set{"context", "gotypes", "netipv6zone", "printerconfig"}, }, Args: anyGo, }, "link": { - Flags: complete.Flags{ - "-B": complete.PredictAnything, // note - "-D": complete.PredictAnything, // address (default -1) - "-E": complete.PredictAnything, // entry symbol name - "-H": complete.PredictAnything, // header type - "-I": complete.PredictAnything, // linker binary - "-L": complete.PredictDirs("*"), // directory - "-R": complete.PredictAnything, // quantum (default -1) - "-T": complete.PredictAnything, // address (default -1) - "-V": complete.PredictNothing, - "-X": complete.PredictAnything, - "-a": complete.PredictAnything, - "-buildid": complete.PredictAnything, // build id - "-buildmode": complete.PredictAnything, - "-c": complete.PredictNothing, - "-cpuprofile": anyFile, - "-d": complete.PredictNothing, - "-debugtramp": complete.PredictAnything, // int - "-dumpdep": complete.PredictNothing, - "-extar": complete.PredictAnything, - "-extld": complete.PredictAnything, - "-extldflags": complete.PredictAnything, // flags - "-f": complete.PredictNothing, - "-g": complete.PredictNothing, - "-importcfg": anyFile, - "-installsuffix": complete.PredictAnything, // dir suffix - "-k": complete.PredictAnything, // symbol - "-libgcc": complete.PredictAnything, // maybe "none" - "-linkmode": complete.PredictAnything, // mode - "-linkshared": complete.PredictNothing, - "-memprofile": anyFile, - "-memprofilerate": complete.PredictAnything, // rate - "-msan": complete.PredictNothing, - "-n": complete.PredictNothing, - "-o": complete.PredictAnything, - "-pluginpath": complete.PredictAnything, - "-r": complete.PredictAnything, // "dir1:dir2:..." - "-race": complete.PredictNothing, - "-s": complete.PredictNothing, - "-tmpdir": complete.PredictDirs("*"), - "-u": complete.PredictNothing, - "-v": complete.PredictNothing, - "-w": complete.PredictNothing, - // "-h": complete.PredictAnything, // halt on error + Flags: map[string]complete.Predictor{ + "B": predict.Something, // note + "D": predict.Something, // address (default -1) + "E": predict.Something, // entry symbol name + "H": predict.Something, // header type + "I": predict.Something, // linker binary + "L": predict.Dirs("*"), // directory + "R": predict.Something, // quantum (default -1) + "T": predict.Something, // address (default -1) + "V": predict.Nothing, + "X": predict.Something, + "a": predict.Something, + "buildid": predict.Something, // build id + "buildmode": predict.Something, + "c": predict.Nothing, + "cpuprofile": anyFile, + "d": predict.Nothing, + "debugtramp": predict.Something, // int + "dumpdep": predict.Nothing, + "extar": predict.Something, + "extld": predict.Something, + "extldflags": predict.Something, // flags + "f": predict.Nothing, + "g": predict.Nothing, + "importcfg": anyFile, + "installsuffix": predict.Something, // dir suffix + "k": predict.Something, // symbol + "libgcc": predict.Something, // maybe "none" + "linkmode": predict.Something, // mode + "linkshared": predict.Nothing, + "memprofile": anyFile, + "memprofilerate": predict.Something, // rate + "msan": predict.Nothing, + "n": predict.Nothing, + "o": predict.Something, + "pluginpath": predict.Something, + "r": predict.Something, // "dir1:dir2:..." + "race": predict.Nothing, + "s": predict.Nothing, + "tmpdir": predict.Dirs("*"), + "u": predict.Nothing, + "v": predict.Nothing, + "w": predict.Nothing, + // "h": predict.Something, // halt on error }, - Args: complete.PredictOr( - complete.PredictFiles("*.a"), - complete.PredictFiles("*.o"), + Args: predict.Or( + predict.Files("*.a"), + predict.Files("*.o"), ), }, "nm": { - Flags: complete.Flags{ - "-n": complete.PredictNothing, - "-size": complete.PredictNothing, - "-sort": complete.PredictAnything, - "-type": complete.PredictNothing, + Flags: map[string]complete.Predictor{ + "n": predict.Nothing, + "size": predict.Nothing, + "sort": predict.Something, + "type": predict.Nothing, }, Args: anyGo, }, "objdump": { - Flags: complete.Flags{ - "-s": complete.PredictAnything, - "-S": complete.PredictNothing, + Flags: map[string]complete.Predictor{ + "s": predict.Something, + "S": predict.Nothing, }, Args: anyFile, }, "pack": { /* this lacks the positional aspect of all these params */ - Flags: complete.Flags{ - "c": complete.PredictNothing, - "p": complete.PredictNothing, - "r": complete.PredictNothing, - "t": complete.PredictNothing, - "x": complete.PredictNothing, - "cv": complete.PredictNothing, - "pv": complete.PredictNothing, - "rv": complete.PredictNothing, - "tv": complete.PredictNothing, - "xv": complete.PredictNothing, + Flags: map[string]complete.Predictor{ + "c": predict.Nothing, + "p": predict.Nothing, + "r": predict.Nothing, + "t": predict.Nothing, + "x": predict.Nothing, + "cv": predict.Nothing, + "pv": predict.Nothing, + "rv": predict.Nothing, + "tv": predict.Nothing, + "xv": predict.Nothing, }, - Args: complete.PredictOr( - complete.PredictFiles("*.a"), - complete.PredictFiles("*.o"), + Args: predict.Or( + predict.Files("*.a"), + predict.Files("*.o"), ), }, "pprof": { - Flags: complete.Flags{ - "-callgrind": complete.PredictNothing, - "-disasm": complete.PredictAnything, - "-dot": complete.PredictNothing, - "-eog": complete.PredictNothing, - "-evince": complete.PredictNothing, - "-gif": complete.PredictNothing, - "-gv": complete.PredictNothing, - "-list": complete.PredictAnything, - "-pdf": complete.PredictNothing, - "-peek": complete.PredictAnything, - "-png": complete.PredictNothing, - "-proto": complete.PredictNothing, - "-ps": complete.PredictNothing, - "-raw": complete.PredictNothing, - "-svg": complete.PredictNothing, - "-tags": complete.PredictNothing, - "-text": complete.PredictNothing, - "-top": complete.PredictNothing, - "-tree": complete.PredictNothing, - "-web": complete.PredictNothing, - "-weblist": complete.PredictAnything, - "-output": anyFile, - "-functions": complete.PredictNothing, - "-files": complete.PredictNothing, - "-lines": complete.PredictNothing, - "-addresses": complete.PredictNothing, - "-base": complete.PredictAnything, - "-drop_negative": complete.PredictNothing, - "-cum": complete.PredictNothing, - "-seconds": complete.PredictAnything, - "-nodecount": complete.PredictAnything, - "-nodefraction": complete.PredictAnything, - "-edgefraction": complete.PredictAnything, - "-sample_index": complete.PredictNothing, - "-mean": complete.PredictNothing, - "-inuse_space": complete.PredictNothing, - "-inuse_objects": complete.PredictNothing, - "-alloc_space": complete.PredictNothing, - "-alloc_objects": complete.PredictNothing, - "-total_delay": complete.PredictNothing, - "-contentions": complete.PredictNothing, - "-mean_delay": complete.PredictNothing, - "-runtime": complete.PredictNothing, - "-focus": complete.PredictAnything, - "-ignore": complete.PredictAnything, - "-tagfocus": complete.PredictAnything, - "-tagignore": complete.PredictAnything, - "-call_tree": complete.PredictNothing, - "-unit": complete.PredictAnything, - "-divide_by": complete.PredictAnything, - "-buildid": complete.PredictAnything, - "-tools": complete.PredictDirs("*"), - "-help": complete.PredictNothing, + Flags: map[string]complete.Predictor{ + "callgrind": predict.Nothing, + "disasm": predict.Something, + "dot": predict.Nothing, + "eog": predict.Nothing, + "evince": predict.Nothing, + "gif": predict.Nothing, + "gv": predict.Nothing, + "list": predict.Something, + "pdf": predict.Nothing, + "peek": predict.Something, + "png": predict.Nothing, + "proto": predict.Nothing, + "ps": predict.Nothing, + "raw": predict.Nothing, + "svg": predict.Nothing, + "tags": predict.Nothing, + "text": predict.Nothing, + "top": predict.Nothing, + "tree": predict.Nothing, + "web": predict.Nothing, + "weblist": predict.Something, + "output": anyFile, + "functions": predict.Nothing, + "files": predict.Nothing, + "lines": predict.Nothing, + "addresses": predict.Nothing, + "base": predict.Something, + "drop_negative": predict.Nothing, + "cum": predict.Nothing, + "seconds": predict.Something, + "nodecount": predict.Something, + "nodefraction": predict.Something, + "edgefraction": predict.Something, + "sample_index": predict.Nothing, + "mean": predict.Nothing, + "inuse_space": predict.Nothing, + "inuse_objects": predict.Nothing, + "alloc_space": predict.Nothing, + "alloc_objects": predict.Nothing, + "total_delay": predict.Nothing, + "contentions": predict.Nothing, + "mean_delay": predict.Nothing, + "runtime": predict.Nothing, + "focus": predict.Something, + "ignore": predict.Something, + "tagfocus": predict.Something, + "tagignore": predict.Something, + "call_tree": predict.Nothing, + "unit": predict.Something, + "divide_by": predict.Something, + "buildid": predict.Something, + "tools": predict.Dirs("*"), + "help": predict.Nothing, }, Args: anyFile, }, "tour": { - Flags: complete.Flags{ - "-http": complete.PredictAnything, - "-openbrowser": complete.PredictNothing, + Flags: map[string]complete.Predictor{ + "http": predict.Something, + "openbrowser": predict.Nothing, }, }, "trace": { - Flags: complete.Flags{ - "-http": complete.PredictAnything, - "-pprof": complete.PredictSet("net", "sync", "syscall", "sched"), + Flags: map[string]complete.Predictor{ + "http": predict.Something, + "pprof": predict.Set{"net", "sync", "syscall", "sched"}, }, Args: anyFile, }, "vet": { - Flags: complete.Flags{ - "-all": complete.PredictNothing, - "-asmdecl": complete.PredictNothing, - "-assign": complete.PredictNothing, - "-atomic": complete.PredictNothing, - "-bool": complete.PredictNothing, - "-buildtags": complete.PredictNothing, - "-cgocall": complete.PredictNothing, - "-composites": complete.PredictNothing, - "-compositewhitelist": complete.PredictNothing, - "-copylocks": complete.PredictNothing, - "-httpresponse": complete.PredictNothing, - "-lostcancel": complete.PredictNothing, - "-methods": complete.PredictNothing, - "-nilfunc": complete.PredictNothing, - "-printf": complete.PredictNothing, - "-printfuncs": complete.PredictAnything, - "-rangeloops": complete.PredictNothing, - "-shadow": complete.PredictNothing, - "-shadowstrict": complete.PredictNothing, - "-shift": complete.PredictNothing, - "-structtags": complete.PredictNothing, - "-tags": complete.PredictAnything, - "-tests": complete.PredictNothing, - "-unreachable": complete.PredictNothing, - "-unsafeptr": complete.PredictNothing, - "-unusedfuncs": complete.PredictAnything, - "-unusedresult": complete.PredictNothing, - "-unusedstringmethods": complete.PredictAnything, - "-v": complete.PredictNothing, + Flags: map[string]complete.Predictor{ + "all": predict.Nothing, + "asmdecl": predict.Nothing, + "assign": predict.Nothing, + "atomic": predict.Nothing, + "bool": predict.Nothing, + "buildtags": predict.Nothing, + "cgocall": predict.Nothing, + "composites": predict.Nothing, + "compositewhitelist": predict.Nothing, + "copylocks": predict.Nothing, + "httpresponse": predict.Nothing, + "lostcancel": predict.Nothing, + "methods": predict.Nothing, + "nilfunc": predict.Nothing, + "printf": predict.Nothing, + "printfuncs": predict.Something, + "rangeloops": predict.Nothing, + "shadow": predict.Nothing, + "shadowstrict": predict.Nothing, + "shift": predict.Nothing, + "structtags": predict.Nothing, + "tags": predict.Something, + "tests": predict.Nothing, + "unreachable": predict.Nothing, + "unsafeptr": predict.Nothing, + "unusedfuncs": predict.Something, + "unusedresult": predict.Nothing, + "unusedstringmethods": predict.Something, + "v": predict.Nothing, }, Args: anyGo, }, }, } - clean := complete.Command{ - Flags: complete.Flags{ - "-i": complete.PredictNothing, - "-r": complete.PredictNothing, - "-n": complete.PredictNothing, - "-x": complete.PredictNothing, - "-cache": complete.PredictNothing, - "-testcache": complete.PredictNothing, - "-modcache": complete.PredictNothing, + clean := &complete.Command{ + Flags: map[string]complete.Predictor{ + "i": predict.Nothing, + "r": predict.Nothing, + "n": predict.Nothing, + "x": predict.Nothing, + "cache": predict.Nothing, + "testcache": predict.Nothing, + "modcache": predict.Nothing, }, - Args: complete.PredictOr(anyPackage, ellipsis), + Args: predict.Or(anyPackage, ellipsis), } - env := complete.Command{ - Args: complete.PredictAnything, + env := &complete.Command{ + Args: predict.Something, } - bug := complete.Command{} - version := complete.Command{} + bug := &complete.Command{} + version := &complete.Command{} - fix := complete.Command{ + fix := &complete.Command{ Args: anyGo, } - modDownload := complete.Command{ - Flags: complete.Flags{ - "-json": complete.PredictNothing, + modDownload := &complete.Command{ + Flags: map[string]complete.Predictor{ + "json": predict.Nothing, }, Args: anyPackage, } - modEdit := complete.Command{ - Flags: complete.Flags{ - "-fmt": complete.PredictNothing, - "-module": complete.PredictNothing, - "-print": complete.PredictNothing, + modEdit := &complete.Command{ + Flags: map[string]complete.Predictor{ + "fmt": predict.Nothing, + "module": predict.Nothing, + "print": predict.Nothing, - "-exclude": anyPackage, - "-dropexclude": anyPackage, - "-replace": anyPackage, - "-dropreplace": anyPackage, - "-require": anyPackage, - "-droprequire": anyPackage, + "exclude": anyPackage, + "dropexclude": anyPackage, + "replace": anyPackage, + "dropreplace": anyPackage, + "require": anyPackage, + "droprequire": anyPackage, }, - Args: complete.PredictFiles("go.mod"), + Args: predict.Files("go.mod"), } - modGraph := complete.Command{} + modGraph := &complete.Command{} - modInit := complete.Command{ - Args: complete.PredictAnything, + modInit := &complete.Command{ + Args: predict.Something, } - modTidy := complete.Command{ - Flags: complete.Flags{ - "-v": complete.PredictNothing, + modTidy := &complete.Command{ + Flags: map[string]complete.Predictor{ + "v": predict.Nothing, }, } - modVendor := complete.Command{ - Flags: complete.Flags{ - "-v": complete.PredictNothing, + modVendor := &complete.Command{ + Flags: map[string]complete.Predictor{ + "v": predict.Nothing, }, } - modVerify := complete.Command{} + modVerify := &complete.Command{} - modWhy := complete.Command{ - Flags: complete.Flags{ - "-m": complete.PredictNothing, - "-vendor": complete.PredictNothing, + modWhy := &complete.Command{ + Flags: map[string]complete.Predictor{ + "m": predict.Nothing, + "vendor": predict.Nothing, }, Args: anyPackage, } - modHelp := complete.Command{ - Sub: complete.Commands{ - "download": complete.Command{}, - "edit": complete.Command{}, - "graph": complete.Command{}, - "init": complete.Command{}, - "tidy": complete.Command{}, - "vendor": complete.Command{}, - "verify": complete.Command{}, - "why": complete.Command{}, + modHelp := &complete.Command{ + Sub: map[string]*complete.Command{ + "download": &complete.Command{}, + "edit": &complete.Command{}, + "graph": &complete.Command{}, + "init": &complete.Command{}, + "tidy": &complete.Command{}, + "vendor": &complete.Command{}, + "verify": &complete.Command{}, + "why": &complete.Command{}, }, } - mod := complete.Command{ - Sub: complete.Commands{ + mod := &complete.Command{ + Sub: map[string]*complete.Command{ "download": modDownload, "edit": modEdit, "graph": modGraph, @@ -558,40 +561,40 @@ func main() { }, } - help := complete.Command{ - Sub: complete.Commands{ - "bug": complete.Command{}, - "build": complete.Command{}, - "clean": complete.Command{}, - "doc": complete.Command{}, - "env": complete.Command{}, - "fix": complete.Command{}, - "fmt": complete.Command{}, - "generate": complete.Command{}, - "get": complete.Command{}, - "install": complete.Command{}, - "list": complete.Command{}, + help := &complete.Command{ + Sub: map[string]*complete.Command{ + "bug": &complete.Command{}, + "build": &complete.Command{}, + "clean": &complete.Command{}, + "doc": &complete.Command{}, + "env": &complete.Command{}, + "fix": &complete.Command{}, + "fmt": &complete.Command{}, + "generate": &complete.Command{}, + "get": &complete.Command{}, + "install": &complete.Command{}, + "list": &complete.Command{}, "mod": modHelp, - "run": complete.Command{}, - "test": complete.Command{}, - "tool": complete.Command{}, - "version": complete.Command{}, - "vet": complete.Command{}, - "buildmode": complete.Command{}, - "c": complete.Command{}, - "cache": complete.Command{}, - "environment": complete.Command{}, - "filetype": complete.Command{}, - "go.mod": complete.Command{}, - "gopath": complete.Command{}, - "gopath-get": complete.Command{}, - "goproxy": complete.Command{}, - "importpath": complete.Command{}, - "modules": complete.Command{}, - "module-get": complete.Command{}, - "packages": complete.Command{}, - "testflag": complete.Command{}, - "testfunc": complete.Command{}, + "run": &complete.Command{}, + "test": &complete.Command{}, + "tool": &complete.Command{}, + "version": &complete.Command{}, + "vet": &complete.Command{}, + "buildmode": &complete.Command{}, + "c": &complete.Command{}, + "cache": &complete.Command{}, + "environment": &complete.Command{}, + "filetype": &complete.Command{}, + "go.mod": &complete.Command{}, + "gopath": &complete.Command{}, + "gopath-get": &complete.Command{}, + "goproxy": &complete.Command{}, + "importpath": &complete.Command{}, + "modules": &complete.Command{}, + "module-get": &complete.Command{}, + "packages": &complete.Command{}, + "testflag": &complete.Command{}, + "testfunc": &complete.Command{}, }, } @@ -604,8 +607,8 @@ func main() { get.Flags[name] = options } - gogo := complete.Command{ - Sub: complete.Commands{ + gogo := &complete.Command{ + Sub: map[string]*complete.Command{ "build": build, "install": build, // install and build have the same flags "run": run, @@ -625,10 +628,10 @@ func main() { "mod": mod, "help": help, }, - GlobalFlags: complete.Flags{ - "-h": complete.PredictNothing, + Flags: map[string]complete.Predictor{ + "h": predict.Nothing, }, } - complete.New("go", gogo).Run() + gogo.Complete("go") } diff --git a/gocomplete/parse.go b/gocomplete/parse.go index 8111b74..faf1907 100644 --- a/gocomplete/parse.go +++ b/gocomplete/parse.go @@ -4,16 +4,15 @@ import ( "go/ast" "go/parser" "go/token" + "log" "regexp" - - "github.com/posener/complete" ) func functionsInFile(path string, regexp *regexp.Regexp) (tests []string) { fset := token.NewFileSet() f, err := parser.ParseFile(fset, path, nil, 0) if err != nil { - complete.Log("Failed parsing %s: %s", path, err) + log.Printf("Failed parsing %s: %s", path, err) return nil } for _, d := range f.Decls { diff --git a/gocomplete/pkgs.go b/gocomplete/pkgs.go index 2f95046..bfdfa04 100644 --- a/gocomplete/pkgs.go +++ b/gocomplete/pkgs.go @@ -3,18 +3,19 @@ package main import ( "go/build" "io/ioutil" + "log" "os" "os/user" "path/filepath" "strings" - "github.com/posener/complete" + "github.com/posener/complete/predict" ) // predictPackages completes packages in the directory pointed by a.Last // and packages that are one level below that package. -func predictPackages(a complete.Args) (prediction []string) { - prediction = []string{a.Last} +func predictPackages(prefix string) (prediction []string) { + prediction = []string{prefix} lastPrediction := "" for len(prediction) == 1 && (lastPrediction == "" || lastPrediction != prediction[0]) { // if only one prediction, predict files within this prediction, @@ -23,19 +24,19 @@ func predictPackages(a complete.Args) (prediction []string) { // level deeper and give the user the 'pkg' and all the nested packages within // that package. lastPrediction = prediction[0] - a.Last = prediction[0] - prediction = predictLocalAndSystem(a) + prefix = prediction[0] + prediction = predictLocalAndSystem(prefix) } return } -func predictLocalAndSystem(a complete.Args) []string { - localDirs := complete.PredictFilesSet(listPackages(a.Directory())).Predict(a) +func predictLocalAndSystem(prefix string) []string { + localDirs := predict.FilesSet(listPackages(directory(prefix))).Predict(prefix) // System directories are not actual file names, for example: 'github.com/posener/complete' could // be the argument, but the actual filename is in $GOPATH/src/github.com/posener/complete'. this // is the reason to use the PredictSet and not the PredictDirs in this case. - s := systemDirs(a.Last) - sysDirs := complete.PredictSet(s...).Predict(a) + s := systemDirs(prefix) + sysDirs := predict.Set(s).Predict(prefix) return append(localDirs, sysDirs...) } @@ -45,7 +46,7 @@ func listPackages(dir string) (directories []string) { // add subdirectories files, err := ioutil.ReadDir(dir) if err != nil { - complete.Log("failed reading directory %s: %s", dir, err) + log.Printf("failed reading directory %s: %s", dir, err) return } @@ -62,7 +63,7 @@ func listPackages(dir string) (directories []string) { for _, p := range paths { pkg, err := build.ImportDir(p, 0) if err != nil { - complete.Log("failed importing directory %s: %s", p, err) + log.Printf("failed importing directory %s: %s", p, err) continue } directories = append(directories, pkg.Dir) @@ -124,3 +125,53 @@ func findGopath() []string { entries := strings.Split(gopath, listsep) return entries } + +func directory(prefix string) string { + if info, err := os.Stat(prefix); err == nil && info.IsDir() { + return fixPathForm(prefix, prefix) + } + dir := filepath.Dir(prefix) + if info, err := os.Stat(dir); err != nil || !info.IsDir() { + return "./" + } + return fixPathForm(prefix, dir) +} + +// fixPathForm changes a file name to a relative name +func fixPathForm(last string, file string) string { + // get wording directory for relative name + workDir, err := os.Getwd() + if err != nil { + return file + } + + abs, err := filepath.Abs(file) + if err != nil { + return file + } + + // if last is absolute, return path as absolute + if filepath.IsAbs(last) { + return fixDirPath(abs) + } + + rel, err := filepath.Rel(workDir, abs) + if err != nil { + return file + } + + // fix ./ prefix of path + if rel != "." && strings.HasPrefix(last, ".") { + rel = "./" + rel + } + + return fixDirPath(rel) +} + +func fixDirPath(path string) string { + info, err := os.Stat(path) + if err == nil && info.IsDir() && !strings.HasSuffix(path, "/") { + path += "/" + } + return path +} diff --git a/gocomplete/tests.go b/gocomplete/tests.go index e755ae5..fc3a3ad 100644 --- a/gocomplete/tests.go +++ b/gocomplete/tests.go @@ -20,7 +20,7 @@ var ( // for test names use prefix of 'Test' or 'Example', and for benchmark // test names use 'Benchmark' func funcPredict(funcRegexp *regexp.Regexp) complete.Predictor { - return complete.PredictFunc(func(a complete.Args) []string { + return complete.PredictFunc(func(prefix string) []string { return funcNames(funcRegexp) }) } diff --git a/gocomplete/tests_test.go b/gocomplete/tests_test.go index 150e2e2..b09ca6f 100644 --- a/gocomplete/tests_test.go +++ b/gocomplete/tests_test.go @@ -14,7 +14,7 @@ func TestPredictions(t *testing.T) { tests := []struct { name string predictor complete.Predictor - last string + prefix string want []string }{ { @@ -31,8 +31,7 @@ func TestPredictions(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - a := complete.Args{Last: tt.last} - got := tt.predictor.Predict(a) + got := tt.predictor.Predict(tt.prefix) if !equal(got, tt.want) { t.Errorf("Failed %s: got: %q, want: %q", t.Name(), got, tt.want) } @@ -44,9 +43,9 @@ func BenchmarkFake(b *testing.B) {} func Example() { os.Setenv("COMP_LINE", "go ru") + os.Setenv("COMP_POINT", "5") main() // output: run - } func equal(s1, s2 []string) bool { diff --git a/internal/arg/arg.go b/internal/arg/arg.go new file mode 100644 index 0000000..0577d74 --- /dev/null +++ b/internal/arg/arg.go @@ -0,0 +1,124 @@ +package arg + +import "strings" + +import "github.com/posener/complete/internal/tokener" + +// Arg is typed a command line argument. +type Arg struct { + Text string + Completed bool + Parsed +} + +// Parsed contains information about the argument. +type Parsed struct { + Flag string + HasFlag bool + Value string + Dashes string + HasValue bool +} + +// Parse parses a typed command line argument list, and returns a list of arguments. +func Parse(line string) []Arg { + var args []Arg + for { + arg, after := next(line) + if arg.Text != "" { + args = append(args, arg) + } + line = after + if line == "" { + break + } + } + return args +} + +// next returns the first argument in the line and the rest of the line. +func next(line string) (arg Arg, after string) { + defer arg.parse() + // Start and end of the argument term. + var start, end int + // Stack of quote marks met during the paring of the argument. + var token tokener.Tokener + // Skip prefix spaces. + for start = 0; start < len(line); start++ { + token.Visit(line[start]) + if !token.LastSpace() { + break + } + } + // If line is only spaces, return empty argument and empty leftovers. + if start == len(line) { + return + } + + for end = start + 1; end < len(line); end++ { + token.Visit(line[end]) + if token.LastSpace() { + arg.Completed = true + break + } + } + arg.Text = line[start:end] + if !arg.Completed { + return + } + start2 := end + + // Skip space after word. + for start2 < len(line) { + token.Visit(line[start2]) + if !token.LastSpace() { + break + } + start2++ + } + after = line[start2:] + return +} + +// parse a flag from an argument. The flag can have value attached when it is given in the +// `-key=value` format. +func (a *Arg) parse() { + if len(a.Text) == 0 { + return + } + + // A pure value, no flag. + if a.Text[0] != '-' { + a.Value = a.Text + a.HasValue = true + return + } + + // Seprate the dashes from the flag name. + dahsI := 1 + if len(a.Text) > 1 && a.Text[1] == '-' { + dahsI = 2 + } + a.Dashes = a.Text[:dahsI] + a.HasFlag = true + a.Flag = a.Text[dahsI:] + + // Empty flag + if a.Flag == "" { + return + } + // Third dash or empty flag with equal is forbidden. + if a.Flag[0] == '-' || a.Flag[0] == '=' { + a.Parsed = Parsed{} + return + } + // The flag is valid. + + // Check if flag has a value. + if equal := strings.IndexRune(a.Flag, '='); equal != -1 { + a.Flag, a.Value = a.Flag[:equal], a.Flag[equal+1:] + a.HasValue = true + return + } + +} diff --git a/internal/arg/arg_test.go b/internal/arg/arg_test.go new file mode 100644 index 0000000..11130ce --- /dev/null +++ b/internal/arg/arg_test.go @@ -0,0 +1,122 @@ +package arg + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestParse(t *testing.T) { + t.Parallel() + + tests := []struct { + line string + args []Arg + }{ + { + line: "a b", + args: []Arg{{Text: "a", Completed: true}, {Text: "b", Completed: false}}, + }, + { + line: " a b ", + args: []Arg{{Text: "a", Completed: true}, {Text: "b", Completed: true}}, + }, + { + line: "a b", + args: []Arg{{Text: "a", Completed: true}, {Text: "b", Completed: false}}, + }, + { + line: " a ", + args: []Arg{{Text: "a", Completed: true}}, + }, + { + line: " a", + args: []Arg{{Text: "a", Completed: false}}, + }, + { + line: " ", + args: nil, + }, + { + line: "", + args: nil, + }, + { + line: `\ a\ b c\ `, + args: []Arg{{Text: `\ a\ b`, Completed: true}, {Text: `c\ `, Completed: false}}, + }, + { + line: `"\"'\''" '"'`, + args: []Arg{{Text: `"\"'\''"`, Completed: true}, {Text: `'"'`, Completed: false}}, + }, + { + line: `"a b"`, + args: []Arg{{Text: `"a b"`, Completed: false}}, + }, + { + line: `"a b" `, + args: []Arg{{Text: `"a b"`, Completed: true}}, + }, + { + line: `"a b"c`, + args: []Arg{{Text: `"a b"c`, Completed: false}}, + }, + { + line: `"a b"c `, + args: []Arg{{Text: `"a b"c`, Completed: true}}, + }, + { + line: `"a b" c`, + args: []Arg{{Text: `"a b"`, Completed: true}, {Text: "c", Completed: false}}, + }, + { + line: `"a `, + args: []Arg{{Text: `"a `, Completed: false}}, + }, + { + line: `\"a b`, + args: []Arg{{Text: `\"a`, Completed: true}, {Text: "b", Completed: false}}, + }, + } + + for _, tt := range tests { + t.Run(tt.line, func(t *testing.T) { + args := Parse(tt.line) + // Clear parsed part of the arguments. It is tested in the TestArgsParsed test. + for i := range args { + arg := args[i] + arg.Parsed = Parsed{} + args[i] = arg + } + assert.Equal(t, tt.args, args) + }) + } +} + +func TestArgsParsed(t *testing.T) { + t.Parallel() + + tests := []struct { + text string + parsed Parsed + }{ + {text: "-", parsed: Parsed{Dashes: "-", HasFlag: true}}, + {text: "--", parsed: Parsed{Dashes: "--", HasFlag: true}}, + {text: "---"}, // Forbidden. + {text: "--="}, // Forbidden. + {text: "-="}, // Forbidden. + {text: "-a-b", parsed: Parsed{Dashes: "-", Flag: "a-b", HasFlag: true}}, + {text: "--a-b", parsed: Parsed{Dashes: "--", Flag: "a-b", HasFlag: true}}, + {text: "-a-b=c-d=e", parsed: Parsed{Dashes: "-", Flag: "a-b", HasFlag: true, Value: "c-d=e", HasValue: true}}, + {text: "--a-b=c-d=e", parsed: Parsed{Dashes: "--", Flag: "a-b", HasFlag: true, Value: "c-d=e", HasValue: true}}, + {text: "--a-b=", parsed: Parsed{Dashes: "--", Flag: "a-b", HasFlag: true, Value: "", HasValue: true}}, + {text: "a", parsed: Parsed{Value: "a", HasValue: true}}, + } + + for _, tt := range tests { + t.Run(tt.text, func(t *testing.T) { + arg := Parse(tt.text)[0] + assert.Equal(t, tt.parsed, arg.Parsed) + }) + } +} diff --git a/cmd/install/bash.go b/internal/install/bash.go similarity index 100% rename from cmd/install/bash.go rename to internal/install/bash.go diff --git a/cmd/install/fish.go b/internal/install/fish.go similarity index 100% rename from cmd/install/fish.go rename to internal/install/fish.go diff --git a/cmd/install/install.go b/internal/install/install.go similarity index 84% rename from cmd/install/install.go rename to internal/install/install.go index 884c23f..e4c5c0e 100644 --- a/cmd/install/install.go +++ b/internal/install/install.go @@ -2,14 +2,42 @@ package install import ( "errors" + "fmt" + "io" "os" "os/user" "path/filepath" "runtime" + "strings" "github.com/hashicorp/go-multierror" ) +func Run(name string, uninstall, yes bool, out io.Writer, in io.Reader) { + action := "install" + if uninstall { + action = "uninstall" + } + if !yes { + fmt.Fprintf(out, "%s completion for %s? ", action, name) + var answer string + fmt.Fscanln(in, &answer) + switch strings.ToLower(answer) { + case "y", "yes": + default: + fmt.Fprintf(out, "Cancelling...") + return + } + } + fmt.Fprintf(out, action+"ing...") + + if uninstall { + Uninstall(name) + } else { + Install(name) + } +} + type installer interface { IsInstalled(cmd, bin string) bool Install(cmd, bin string) error diff --git a/cmd/install/utils.go b/internal/install/utils.go similarity index 100% rename from cmd/install/utils.go rename to internal/install/utils.go diff --git a/cmd/install/zsh.go b/internal/install/zsh.go similarity index 100% rename from cmd/install/zsh.go rename to internal/install/zsh.go diff --git a/internal/tokener/tokener.go b/internal/tokener/tokener.go new file mode 100644 index 0000000..0886341 --- /dev/null +++ b/internal/tokener/tokener.go @@ -0,0 +1,67 @@ +package tokener + +type Tokener struct { + quotes []byte + escaped bool + fixed string + space bool +} + +// Visit visit a byte and update the state of the quotes. +// It returns true if the byte was quotes or escape character. +func (t *Tokener) Visit(b byte) { + // Check space. + if b == ' ' { + if !t.escaped && !t.Quoted() { + t.space = true + } + } else { + t.space = false + } + + // Check escaping + if b == '\\' { + t.escaped = !t.escaped + } else { + defer func() { t.escaped = false }() + } + + // Check quotes. + if !t.escaped && (b == '"' || b == '\'') { + if t.Quoted() && t.quotes[len(t.quotes)-1] == b { + t.quotes = t.quotes[:len(t.quotes)-1] + } else { + t.quotes = append(t.quotes, b) + } + } + + // If not quoted, insert escape before inserting space. + if t.LastSpace() { + t.fixed += "\\" + } + t.fixed += string(b) +} + +func (t *Tokener) Escaped() bool { + return t.escaped +} + +func (t *Tokener) Quoted() bool { + return len(t.quotes) > 0 +} + +func (t *Tokener) Fixed() string { + return t.fixed +} + +func (t *Tokener) Closed() string { + fixed := t.fixed + for i := len(t.quotes) - 1; i >= 0; i-- { + fixed += string(t.quotes[i]) + } + return fixed +} + +func (t *Tokener) LastSpace() bool { + return t.space +} diff --git a/log.go b/log.go deleted file mode 100644 index c302955..0000000 --- a/log.go +++ /dev/null @@ -1,22 +0,0 @@ -package complete - -import ( - "io/ioutil" - "log" - "os" -) - -// Log is used for debugging purposes -// since complete is running on tab completion, it is nice to -// have logs to the stderr (when writing your own completer) -// to write logs, set the COMP_DEBUG environment variable and -// use complete.Log in the complete program -var Log = getLogger() - -func getLogger() func(format string, args ...interface{}) { - var logfile = ioutil.Discard - if os.Getenv(envDebug) != "" { - logfile = os.Stderr - } - return log.New(logfile, "complete ", log.Flags()).Printf -} diff --git a/match/match.go b/match/match.go deleted file mode 100644 index b5f1814..0000000 --- a/match/match.go +++ /dev/null @@ -1,39 +0,0 @@ -// Package match contains matchers that decide if to apply completion. -// -// This package is deprecated. -package match - -import "strings" - -// 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. -// -// Deprecated. -type Match func(term, prefix string) bool - -// Prefix is a simple Matcher, if the word is it's prefix, there is a match -// Match returns true if a has the prefix as prefix -// -// Deprecated. -func Prefix(long, prefix string) bool { - return strings.HasPrefix(long, prefix) -} - -// File returns true if prefix can match the file -// -// Deprecated. -func File(file, prefix string) bool { - // special case for current directory completion - if file == "./" && (prefix == "." || prefix == "") { - return true - } - if prefix == "." && strings.HasPrefix(file, ".") { - return true - } - - file = strings.TrimPrefix(file, "./") - prefix = strings.TrimPrefix(prefix, "./") - - return strings.HasPrefix(file, prefix) -} diff --git a/match/match_test.go b/match/match_test.go deleted file mode 100644 index b5a0d87..0000000 --- a/match/match_test.go +++ /dev/null @@ -1,129 +0,0 @@ -package match - -import ( - "fmt" - "os" - "testing" -) - -func TestMatch(t *testing.T) { - t.Parallel() - - // Change to tests directory for testing completion of - // files and directories - err := os.Chdir("../tests") - if err != nil { - panic(err) - } - - type matcherTest struct { - prefix string - want bool - } - - tests := []struct { - m Match - long string - tests []matcherTest - }{ - { - m: Prefix, - long: "abcd", - tests: []matcherTest{ - {prefix: "", want: true}, - {prefix: "ab", want: true}, - {prefix: "ac", want: false}, - }, - }, - { - m: Prefix, - long: "", - tests: []matcherTest{ - {prefix: "ac", want: false}, - {prefix: "", want: true}, - }, - }, - { - m: File, - long: "file.txt", - tests: []matcherTest{ - {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}, - {prefix: "./file.txt", want: true}, - {prefix: "other.txt", want: false}, - {prefix: "/other.txt", want: false}, - {prefix: "/file.txt", want: false}, - {prefix: "/fil", want: false}, - {prefix: "/file.txt2", want: false}, - {prefix: "/.", want: false}, - }, - }, - { - m: File, - long: "./file.txt", - tests: []matcherTest{ - {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}, - {prefix: "./file.txt", want: true}, - {prefix: "other.txt", want: false}, - {prefix: "/other.txt", want: false}, - {prefix: "/file.txt", want: false}, - {prefix: "/fil", want: false}, - {prefix: "/file.txt2", want: false}, - {prefix: "/.", want: false}, - }, - }, - { - m: File, - long: "/file.txt", - tests: []matcherTest{ - {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}, - {prefix: "./file.txt", want: false}, - {prefix: "other.txt", want: false}, - {prefix: "/other.txt", want: false}, - {prefix: "/file.txt", want: true}, - {prefix: "/fil", want: true}, - {prefix: "/file.txt2", want: false}, - {prefix: "/.", want: false}, - }, - }, - { - m: File, - long: "./", - tests: []matcherTest{ - {prefix: "", want: true}, - {prefix: ".", want: true}, - {prefix: "./", want: true}, - {prefix: "./.", want: false}, - }, - }, - } - - for _, tt := range tests { - for _, ttt := range tt.tests { - 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(tt.long, ttt.prefix) - if got != ttt.want { - t.Errorf("Failed %s: got = %t, want: %t", name, got, ttt.want) - } - }) - } - } -} diff --git a/predict.go b/predict.go deleted file mode 100644 index 8207063..0000000 --- a/predict.go +++ /dev/null @@ -1,41 +0,0 @@ -package complete - -// 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 -// 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(a) -} - -// PredictNothing does not expect anything after. -var PredictNothing Predictor - -// PredictAnything expects something, but nothing particular, such as a number -// or arbitrary name. -var PredictAnything = PredictFunc(func(Args) []string { return nil }) diff --git a/predict/files.go b/predict/files.go new file mode 100644 index 0000000..4654ec4 --- /dev/null +++ b/predict/files.go @@ -0,0 +1,175 @@ +package predict + +import ( + "io/ioutil" + "os" + "path/filepath" + "strings" +) + +// Dirs returns a predictor that predict directory paths. If a non-empty pattern is given, the +// predicted paths will match that pattern. +func Dirs(pattern string) FilesPredictor { + return FilesPredictor{pattern: pattern, includeFiles: false} +} + +// Dirs returns a predictor that predict file or directory paths. If a non-empty pattern is given, +// the predicted paths will match that pattern. +func Files(pattern string) FilesPredictor { + return FilesPredictor{pattern: pattern, includeFiles: true} +} + +type FilesPredictor struct { + pattern string + includeFiles bool +} + +// Predict searches for files according to the given prefix. +// If the only predicted path is a single directory, the search will continue another recursive +// layer into that directory. +func (f FilesPredictor) Predict(prefix string) (options []string) { + options = f.predictFiles(prefix) + + // If the number of prediction is not 1, we either have many results or have no results, so we + // return it. + if len(options) != 1 { + return + } + + // Only try deeper, if the one item is a directory. + if stat, err := os.Stat(options[0]); err != nil || !stat.IsDir() { + return + } + + return f.predictFiles(options[0]) +} + +func (f FilesPredictor) predictFiles(prefix string) []string { + if strings.HasSuffix(prefix, "/..") { + return nil + } + + dir := directory(prefix) + files := f.listFiles(dir) + + // Add dir if match. + files = append(files, dir) + + return FilesSet(files).Predict(prefix) +} + +func (f FilesPredictor) listFiles(dir string) []string { + // Set of all file names. + m := map[string]bool{} + + // List files. + if files, err := filepath.Glob(filepath.Join(dir, f.pattern)); err == nil { + for _, file := range files { + if stat, err := os.Stat(file); err != nil || stat.IsDir() || f.includeFiles { + m[file] = true + } + } + } + + // List directories. + if dirs, err := ioutil.ReadDir(dir); err == nil { + for _, d := range dirs { + if d.IsDir() { + m[filepath.Join(dir, d.Name())] = true + } + } + } + + list := make([]string, 0, len(m)) + for k := range m { + list = append(list, k) + } + return list +} + +// directory gives the directory of the given partial path in case that it is not, we fall back to +// the current directory. +func directory(path string) string { + if info, err := os.Stat(path); err == nil && info.IsDir() { + return fixPathForm(path, path) + } + dir := filepath.Dir(path) + if info, err := os.Stat(dir); err == nil && info.IsDir() { + return fixPathForm(path, dir) + } + return "./" +} + +// FilesSet predict according to file rules to a given fixed set of file names. +type FilesSet []string + +func (s FilesSet) Predict(prefix string) (prediction []string) { + // add all matching files to prediction + for _, f := range s { + f = fixPathForm(prefix, f) + + // test matching of file to the argument + if matchFile(f, prefix) { + prediction = append(prediction, f) + } + } + if len(prediction) == 0 { + return s + } + return +} + +// MatchFile returns true if prefix can match the file +func matchFile(file, prefix string) bool { + // special case for current directory completion + if file == "./" && (prefix == "." || prefix == "") { + return true + } + if prefix == "." && strings.HasPrefix(file, ".") { + return true + } + + file = strings.TrimPrefix(file, "./") + prefix = strings.TrimPrefix(prefix, "./") + + return strings.HasPrefix(file, prefix) +} + +// fixPathForm changes a file name to a relative name +func fixPathForm(last string, file string) string { + // Get wording directory for relative name. + workDir, err := os.Getwd() + if err != nil { + return file + } + + abs, err := filepath.Abs(file) + if err != nil { + return file + } + + // If last is absolute, return path as absolute. + if filepath.IsAbs(last) { + return fixDirPath(abs) + } + + rel, err := filepath.Rel(workDir, abs) + if err != nil { + return file + } + + // Fix ./ prefix of path. + if rel != "." && strings.HasPrefix(last, ".") { + rel = "./" + rel + } + + return fixDirPath(rel) +} + +func fixDirPath(path string) string { + info, err := os.Stat(path) + if err == nil && info.IsDir() && !strings.HasSuffix(path, "/") { + path += "/" + } + return path +} diff --git a/predict/files_test.go b/predict/files_test.go new file mode 100644 index 0000000..6d6cba2 --- /dev/null +++ b/predict/files_test.go @@ -0,0 +1,233 @@ +package predict + +import ( + "fmt" + "os" + "sort" + "strings" + "testing" +) + +func TestFiles(t *testing.T) { + err := os.Chdir("testdata") + if err != nil { + panic(err) + } + defer os.Chdir("..") + + tests := []struct { + name string + p FilesPredictor + prefixes []string + want []string + }{ + { + name: "files/txt", + p: Files("*.txt"), + prefixes: []string{""}, + want: []string{"./", "dir/", "outer/", "a.txt", "b.txt", "c.txt", ".dot.txt"}, + }, + { + name: "files/txt", + p: Files("*.txt"), + prefixes: []string{"./dir/"}, + want: []string{"./dir/"}, + }, + { + name: "complete files inside dir if it is the only match", + p: Files("foo"), + prefixes: []string{"./dir/", "./d"}, + want: []string{"./dir/", "./dir/foo"}, + }, + { + name: "complete files inside dir when argList includes file name", + p: Files("*"), + prefixes: []string{"./dir/f", "./dir/foo"}, + want: []string{"./dir/foo"}, + }, + { + name: "files/md", + p: Files("*.md"), + prefixes: []string{""}, + want: []string{"./", "dir/", "outer/", "readme.md"}, + }, + { + name: "files/md with ./ prefix", + p: Files("*.md"), + prefixes: []string{".", "./"}, + want: []string{"./", "./dir/", "./outer/", "./readme.md"}, + }, + { + name: "dirs", + p: Dirs("*"), + prefixes: []string{"di", "dir", "dir/"}, + want: []string{"dir/"}, + }, + { + name: "dirs with ./ prefix", + p: Dirs("*"), + prefixes: []string{"./di", "./dir", "./dir/"}, + want: []string{"./dir/"}, + }, + { + name: "predict anything in dir", + p: Files("*"), + prefixes: []string{"dir", "dir/", "di"}, + want: []string{"dir/", "dir/foo", "dir/bar"}, + }, + { + name: "predict anything in dir with ./ prefix", + p: Files("*"), + prefixes: []string{"./dir", "./dir/", "./di"}, + want: []string{"./dir/", "./dir/foo", "./dir/bar"}, + }, + { + name: "root directories", + p: Dirs("*"), + prefixes: []string{""}, + want: []string{"./", "dir/", "outer/"}, + }, + { + name: "root directories with ./ prefix", + p: Dirs("*"), + prefixes: []string{".", "./"}, + want: []string{"./", "./dir/", "./outer/"}, + }, + { + name: "nested directories", + p: Dirs("*.md"), + prefixes: []string{"ou", "outer", "outer/"}, + want: []string{"outer/", "outer/inner/"}, + }, + { + name: "nested directories with ./ prefix", + p: Dirs("*.md"), + prefixes: []string{"./ou", "./outer", "./outer/"}, + want: []string{"./outer/", "./outer/inner/"}, + }, + { + name: "nested inner directory", + p: Files("*.md"), + prefixes: []string{"outer/i"}, + want: []string{"outer/inner/", "outer/inner/readme.md"}, + }, + } + + for _, tt := range tests { + for _, prefix := range tt.prefixes { + t.Run(tt.name+"/prefix="+prefix, func(t *testing.T) { + + matches := tt.p.Predict(prefix) + + sort.Strings(matches) + sort.Strings(tt.want) + + got := strings.Join(matches, ",") + want := strings.Join(tt.want, ",") + + if got != want { + t.Errorf("failed %s\ngot = %s\nwant: %s", t.Name(), got, want) + } + }) + } + } +} + +func TestMatchFile(t *testing.T) { + // Change to tests directory for testing completion of + // files and directories + err := os.Chdir("testdata") + if err != nil { + panic(err) + } + defer os.Chdir("..") + + type matcherTest struct { + prefix string + want bool + } + + tests := []struct { + long string + tests []matcherTest + }{ + { + long: "file.txt", + tests: []matcherTest{ + {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}, + {prefix: "./file.txt", want: true}, + {prefix: "other.txt", want: false}, + {prefix: "/other.txt", want: false}, + {prefix: "/file.txt", want: false}, + {prefix: "/fil", want: false}, + {prefix: "/file.txt2", want: false}, + {prefix: "/.", want: false}, + }, + }, + { + long: "./file.txt", + tests: []matcherTest{ + {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}, + {prefix: "./file.txt", want: true}, + {prefix: "other.txt", want: false}, + {prefix: "/other.txt", want: false}, + {prefix: "/file.txt", want: false}, + {prefix: "/fil", want: false}, + {prefix: "/file.txt2", want: false}, + {prefix: "/.", want: false}, + }, + }, + { + long: "/file.txt", + tests: []matcherTest{ + {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}, + {prefix: "./file.txt", want: false}, + {prefix: "other.txt", want: false}, + {prefix: "/other.txt", want: false}, + {prefix: "/file.txt", want: true}, + {prefix: "/fil", want: true}, + {prefix: "/file.txt2", want: false}, + {prefix: "/.", want: false}, + }, + }, + { + long: "./", + tests: []matcherTest{ + {prefix: "", want: true}, + {prefix: ".", want: true}, + {prefix: "./", want: true}, + {prefix: "./.", want: false}, + }, + }, + } + + for _, tt := range tests { + for _, ttt := range tt.tests { + name := fmt.Sprintf("long=%q&prefix=%q", tt.long, ttt.prefix) + t.Run(name, func(t *testing.T) { + got := matchFile(tt.long, ttt.prefix) + if got != ttt.want { + t.Errorf("Failed %s: got = %t, want: %t", name, got, ttt.want) + } + }) + } + } +} diff --git a/predict/predict.go b/predict/predict.go new file mode 100644 index 0000000..f4d5bb7 --- /dev/null +++ b/predict/predict.go @@ -0,0 +1,34 @@ +// Package predict provides helper functions for completion predictors. +package predict + +import "github.com/posener/complete" + +// Set predicts a set of predefined values. +type Set []string + +func (p Set) Predict(_ string) (options []string) { + return p +} + +var ( + // Something is used to indicate that does not completes somthing. Such that other prediction + // wont be applied. + Something = Set{""} + + // Nothing is used to indicate that does not completes anything. + Nothing = Set{} +) + +// Or unions prediction functions, so that the result predication is the union of their +// predications. +func Or(ps ...complete.Predictor) complete.Predictor { + return complete.PredictFunc(func(prefix string) (options []string) { + for _, p := range ps { + if p == nil { + continue + } + options = append(options, p.Predict(prefix)...) + } + return + }) +} diff --git a/predict/predict_test.go b/predict/predict_test.go new file mode 100644 index 0000000..af3bf69 --- /dev/null +++ b/predict/predict_test.go @@ -0,0 +1,61 @@ +package predict + +import ( + "testing" + + "github.com/posener/complete" + "github.com/stretchr/testify/assert" +) + +func TestPredict(t *testing.T) { + tests := []struct { + name string + p complete.Predictor + prefix string + want []string + }{ + { + name: "set", + p: Set{"a", "b", "c"}, + want: []string{"a", "b", "c"}, + }, + { + name: "set/empty", + p: Set{}, + want: []string{}, + }, + { + name: "or: word with nil", + p: Or(Set{"a"}, nil), + want: []string{"a"}, + }, + { + name: "or: nil with word", + p: Or(nil, Set{"a"}), + want: []string{"a"}, + }, + { + name: "or: word with word with word", + p: Or(Set{"a"}, Set{"b"}, Set{"c"}), + want: []string{"a", "b", "c"}, + }, + { + name: "something", + p: Something, + want: []string{""}, + }, + { + name: "nothing", + p: Nothing, + prefix: "a", + want: []string{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.p.Predict(tt.prefix) + assert.ElementsMatch(t, tt.want, got, "Got: %+v", got) + }) + } +} diff --git a/tests/.dot.txt b/predict/testdata/.dot.txt similarity index 100% rename from tests/.dot.txt rename to predict/testdata/.dot.txt diff --git a/tests/a.txt b/predict/testdata/a.txt similarity index 100% rename from tests/a.txt rename to predict/testdata/a.txt diff --git a/tests/b.txt b/predict/testdata/b.txt similarity index 100% rename from tests/b.txt rename to predict/testdata/b.txt diff --git a/tests/c.txt b/predict/testdata/c.txt similarity index 100% rename from tests/c.txt rename to predict/testdata/c.txt diff --git a/tests/dir/bar b/predict/testdata/dir/bar similarity index 100% rename from tests/dir/bar rename to predict/testdata/dir/bar diff --git a/tests/dir/foo b/predict/testdata/dir/foo similarity index 100% rename from tests/dir/foo rename to predict/testdata/dir/foo diff --git a/tests/outer/inner/readme.md b/predict/testdata/outer/inner/readme.md similarity index 100% rename from tests/outer/inner/readme.md rename to predict/testdata/outer/inner/readme.md diff --git a/tests/readme.md b/predict/testdata/readme.md similarity index 100% rename from tests/readme.md rename to predict/testdata/readme.md diff --git a/predict_files.go b/predict_files.go deleted file mode 100644 index 25ae2d5..0000000 --- a/predict_files.go +++ /dev/null @@ -1,174 +0,0 @@ -package complete - -import ( - "io/ioutil" - "os" - "path/filepath" - "strings" -) - -// 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) Predictor { - return files(pattern, false) -} - -// PredictFiles will search for files matching the given pattern in the started to -// 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) Predictor { - return files(pattern, true) -} - -func files(pattern string, allowFiles bool) PredictFunc { - - // search for files according to arguments, - // if only one directory has matched the result, search recursively into - // this directory to give more results. - return func(a Args) (prediction []string) { - prediction = predictFiles(a, pattern, allowFiles) - - // if the number of prediction is not 1, we either have many results or - // have no results, so we return it. - if len(prediction) != 1 { - return - } - - // only try deeper, if the one item is a directory - if stat, err := os.Stat(prediction[0]); err != nil || !stat.IsDir() { - return - } - - a.Last = prediction[0] - return predictFiles(a, pattern, allowFiles) - } -} - -func predictFiles(a Args, pattern string, allowFiles bool) []string { - if strings.HasSuffix(a.Last, "/..") { - return nil - } - - dir := directory(a.Last) - files := listFiles(dir, pattern, allowFiles) - - // add dir if match - files = append(files, dir) - - return PredictFilesSet(files).Predict(a) -} - -// directory gives the directory of the given partial path -// in case that it is not, we fall back to the current directory. -func directory(path string) string { - if info, err := os.Stat(path); err == nil && info.IsDir() { - return fixPathForm(path, path) - } - dir := filepath.Dir(path) - if info, err := os.Stat(dir); err == nil && info.IsDir() { - return fixPathForm(path, dir) - } - return "./" -} - -// PredictFilesSet predict according to file rules to a given set of file names -func PredictFilesSet(files []string) PredictFunc { - return func(a Args) (prediction []string) { - // add all matching files to prediction - for _, f := range files { - f = fixPathForm(a.Last, f) - - // test matching of file to the argument - if matchFile(f, a.Last) { - prediction = append(prediction, f) - } - } - return - } -} - -func listFiles(dir, pattern string, allowFiles bool) []string { - // set of all file names - m := map[string]bool{} - - // list files - if files, err := filepath.Glob(filepath.Join(dir, pattern)); err == nil { - for _, f := range files { - if stat, err := os.Stat(f); err != nil || stat.IsDir() || allowFiles { - m[f] = true - } - } - } - - // list directories - if dirs, err := ioutil.ReadDir(dir); err == nil { - for _, d := range dirs { - if d.IsDir() { - m[filepath.Join(dir, d.Name())] = true - } - } - } - - list := make([]string, 0, len(m)) - for k := range m { - list = append(list, k) - } - return list -} - -// MatchFile returns true if prefix can match the file -func matchFile(file, prefix string) bool { - // special case for current directory completion - if file == "./" && (prefix == "." || prefix == "") { - return true - } - if prefix == "." && strings.HasPrefix(file, ".") { - return true - } - - file = strings.TrimPrefix(file, "./") - prefix = strings.TrimPrefix(prefix, "./") - - return strings.HasPrefix(file, prefix) -} - -// fixPathForm changes a file name to a relative name -func fixPathForm(last string, file string) string { - // get wording directory for relative name - workDir, err := os.Getwd() - if err != nil { - return file - } - - abs, err := filepath.Abs(file) - if err != nil { - return file - } - - // if last is absolute, return path as absolute - if filepath.IsAbs(last) { - return fixDirPath(abs) - } - - rel, err := filepath.Rel(workDir, abs) - if err != nil { - return file - } - - // fix ./ prefix of path - if rel != "." && strings.HasPrefix(last, ".") { - rel = "./" + rel - } - - return fixDirPath(rel) -} - -func fixDirPath(path string) string { - info, err := os.Stat(path) - if err == nil && info.IsDir() && !strings.HasSuffix(path, "/") { - path += "/" - } - return path -} diff --git a/predict_set.go b/predict_set.go deleted file mode 100644 index fa4a34a..0000000 --- a/predict_set.go +++ /dev/null @@ -1,12 +0,0 @@ -package complete - -// PredictSet expects specific set of terms, given in the options argument. -func PredictSet(options ...string) Predictor { - return predictSet(options) -} - -type predictSet []string - -func (p predictSet) Predict(a Args) []string { - return p -} diff --git a/predict_test.go b/predict_test.go deleted file mode 100644 index c376207..0000000 --- a/predict_test.go +++ /dev/null @@ -1,271 +0,0 @@ -package complete - -import ( - "fmt" - "os" - "sort" - "strings" - "testing" -) - -func TestPredicate(t *testing.T) { - t.Parallel() - initTests() - - tests := []struct { - name string - p Predictor - argList []string - want []string - }{ - { - name: "set", - p: PredictSet("a", "b", "c"), - want: []string{"a", "b", "c"}, - }, - { - name: "set/empty", - p: PredictSet(), - want: []string{}, - }, - { - name: "anything", - p: PredictAnything, - want: []string{}, - }, - { - name: "or: word with nil", - p: PredictOr(PredictSet("a"), nil), - want: []string{"a"}, - }, - { - name: "or: nil with word", - p: PredictOr(nil, PredictSet("a")), - want: []string{"a"}, - }, - { - name: "or: nil with nil", - p: PredictOr(PredictNothing, PredictNothing), - want: []string{}, - }, - { - name: "or: word with word with word", - p: PredictOr(PredictSet("a"), PredictSet("b"), PredictSet("c")), - want: []string{"a", "b", "c"}, - }, - { - name: "files/txt", - p: PredictFiles("*.txt"), - want: []string{"./", "dir/", "outer/", "a.txt", "b.txt", "c.txt", ".dot.txt"}, - }, - { - name: "files/txt", - p: PredictFiles("*.txt"), - argList: []string{"./dir/"}, - want: []string{"./dir/"}, - }, - { - name: "complete files inside dir if it is the only match", - p: PredictFiles("foo"), - argList: []string{"./dir/", "./d"}, - want: []string{"./dir/", "./dir/foo"}, - }, - { - name: "complete files inside dir when argList includes file name", - p: PredictFiles("*"), - argList: []string{"./dir/f", "./dir/foo"}, - want: []string{"./dir/foo"}, - }, - { - name: "files/md", - p: PredictFiles("*.md"), - argList: []string{""}, - want: []string{"./", "dir/", "outer/", "readme.md"}, - }, - { - name: "files/md with ./ prefix", - p: PredictFiles("*.md"), - argList: []string{".", "./"}, - want: []string{"./", "./dir/", "./outer/", "./readme.md"}, - }, - { - name: "dirs", - p: PredictDirs("*"), - argList: []string{"di", "dir", "dir/"}, - want: []string{"dir/"}, - }, - { - name: "dirs with ./ prefix", - p: PredictDirs("*"), - argList: []string{"./di", "./dir", "./dir/"}, - want: []string{"./dir/"}, - }, - { - name: "predict anything in dir", - p: PredictFiles("*"), - argList: []string{"dir", "dir/", "di"}, - want: []string{"dir/", "dir/foo", "dir/bar"}, - }, - { - name: "predict anything in dir with ./ prefix", - p: PredictFiles("*"), - argList: []string{"./dir", "./dir/", "./di"}, - want: []string{"./dir/", "./dir/foo", "./dir/bar"}, - }, - { - name: "root directories", - p: PredictDirs("*"), - argList: []string{""}, - want: []string{"./", "dir/", "outer/"}, - }, - { - name: "root directories with ./ prefix", - p: PredictDirs("*"), - argList: []string{".", "./"}, - want: []string{"./", "./dir/", "./outer/"}, - }, - { - name: "nested directories", - p: PredictDirs("*.md"), - argList: []string{"ou", "outer", "outer/"}, - want: []string{"outer/", "outer/inner/"}, - }, - { - name: "nested directories with ./ prefix", - p: PredictDirs("*.md"), - argList: []string{"./ou", "./outer", "./outer/"}, - want: []string{"./outer/", "./outer/inner/"}, - }, - { - name: "nested inner directory", - p: PredictFiles("*.md"), - argList: []string{"outer/i"}, - want: []string{"outer/inner/", "outer/inner/readme.md"}, - }, - } - - for _, tt := range tests { - - // no args in argList, means an empty argument - if len(tt.argList) == 0 { - tt.argList = append(tt.argList, "") - } - - for _, arg := range tt.argList { - t.Run(tt.name+"/arg="+arg, func(t *testing.T) { - - matches := tt.p.Predict(newArgs(arg)) - - sort.Strings(matches) - sort.Strings(tt.want) - - got := strings.Join(matches, ",") - want := strings.Join(tt.want, ",") - - if got != want { - t.Errorf("failed %s\ngot = %s\nwant: %s", t.Name(), got, want) - } - }) - } - } -} - -func TestMatchFile(t *testing.T) { - t.Parallel() - - // Change to tests directory for testing completion of - // files and directories - err := os.Chdir("../tests") - if err != nil { - panic(err) - } - - type matcherTest struct { - prefix string - want bool - } - - tests := []struct { - long string - tests []matcherTest - }{ - { - long: "file.txt", - tests: []matcherTest{ - {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}, - {prefix: "./file.txt", want: true}, - {prefix: "other.txt", want: false}, - {prefix: "/other.txt", want: false}, - {prefix: "/file.txt", want: false}, - {prefix: "/fil", want: false}, - {prefix: "/file.txt2", want: false}, - {prefix: "/.", want: false}, - }, - }, - { - long: "./file.txt", - tests: []matcherTest{ - {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}, - {prefix: "./file.txt", want: true}, - {prefix: "other.txt", want: false}, - {prefix: "/other.txt", want: false}, - {prefix: "/file.txt", want: false}, - {prefix: "/fil", want: false}, - {prefix: "/file.txt2", want: false}, - {prefix: "/.", want: false}, - }, - }, - { - long: "/file.txt", - tests: []matcherTest{ - {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}, - {prefix: "./file.txt", want: false}, - {prefix: "other.txt", want: false}, - {prefix: "/other.txt", want: false}, - {prefix: "/file.txt", want: true}, - {prefix: "/fil", want: true}, - {prefix: "/file.txt2", want: false}, - {prefix: "/.", want: false}, - }, - }, - { - long: "./", - tests: []matcherTest{ - {prefix: "", want: true}, - {prefix: ".", want: true}, - {prefix: "./", want: true}, - {prefix: "./.", want: false}, - }, - }, - } - - for _, tt := range tests { - for _, ttt := range tt.tests { - name := fmt.Sprintf("long=%q&prefix=%q", tt.long, ttt.prefix) - t.Run(name, func(t *testing.T) { - got := matchFile(tt.long, ttt.prefix) - if got != ttt.want { - t.Errorf("Failed %s: got = %t, want: %t", name, got, ttt.want) - } - }) - } - } -} diff --git a/testing.go b/testing.go new file mode 100644 index 0000000..3336aa6 --- /dev/null +++ b/testing.go @@ -0,0 +1,29 @@ +package complete + +import ( + "sort" + "testing" + + "github.com/posener/complete/internal/arg" +) + +// Test is a testing helper function for testing bash completion of a given completer. +func Test(t *testing.T, cmp Completer, args string, want []string) { + t.Helper() + got, err := completer{Completer: cmp, args: arg.Parse(args)}.complete() + if err != nil { + t.Fatal(err) + } + sort.Strings(got) + sort.Strings(want) + if len(want) != len(got) { + t.Errorf("got != want: want = %+v, got = %+v", want, got) + return + } + for i := range want { + if want[i] != got[i] { + t.Errorf("got != want: want = %+v, got = %+v", want, got) + return + } + } +}