summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEyal Posener <[email protected]>2019-11-14 06:51:44 +0200
committerEyal Posener <[email protected]>2019-11-18 01:05:47 +0200
commit8724aaf18312e54750540a9578e00d61b1c545d8 (patch)
treed3e736b4fb279975bbcc017ae1bad53e454c5773
parent05b68ffc813dd10c420993cb1cf927b346c057b8 (diff)
V2
-rw-r--r--.travis.yml1
-rw-r--r--args.go114
-rw-r--r--args_test.go213
-rw-r--r--cmd/cmd.go128
-rw-r--r--command.go139
-rw-r--r--common_test.go26
-rw-r--r--compflag/compflag.go245
-rw-r--r--compflag/compflag_test.go105
-rw-r--r--complete.go364
-rw-r--r--complete_test.go576
-rw-r--r--doc.go156
-rw-r--r--example/command/main.go45
-rw-r--r--example/compflag/main.go31
-rw-r--r--example/self/main.go53
-rw-r--r--example/stdlib/main.go35
-rw-r--r--flags.go44
-rw-r--r--flags_test.go57
-rw-r--r--gocomplete/complete.go883
-rw-r--r--gocomplete/parse.go5
-rw-r--r--gocomplete/pkgs.go73
-rw-r--r--gocomplete/tests.go2
-rw-r--r--gocomplete/tests_test.go7
-rw-r--r--internal/arg/arg.go124
-rw-r--r--internal/arg/arg_test.go122
-rw-r--r--internal/install/bash.go (renamed from cmd/install/bash.go)0
-rw-r--r--internal/install/fish.go (renamed from cmd/install/fish.go)0
-rw-r--r--internal/install/install.go (renamed from cmd/install/install.go)28
-rw-r--r--internal/install/utils.go (renamed from cmd/install/utils.go)0
-rw-r--r--internal/install/zsh.go (renamed from cmd/install/zsh.go)0
-rw-r--r--internal/tokener/tokener.go67
-rw-r--r--log.go22
-rw-r--r--match/match.go39
-rw-r--r--match/match_test.go129
-rw-r--r--predict.go41
-rw-r--r--predict/files.go175
-rw-r--r--predict/files_test.go233
-rw-r--r--predict/predict.go34
-rw-r--r--predict/predict_test.go61
-rw-r--r--predict/testdata/.dot.txt (renamed from tests/.dot.txt)0
-rw-r--r--predict/testdata/a.txt (renamed from tests/a.txt)0
-rw-r--r--predict/testdata/b.txt (renamed from tests/b.txt)0
-rw-r--r--predict/testdata/c.txt (renamed from tests/c.txt)0
-rw-r--r--predict/testdata/dir/bar (renamed from tests/dir/bar)0
-rw-r--r--predict/testdata/dir/foo (renamed from tests/dir/foo)0
-rw-r--r--predict/testdata/outer/inner/readme.md (renamed from tests/outer/inner/readme.md)0
-rw-r--r--predict/testdata/readme.md (renamed from tests/readme.md)0
-rw-r--r--predict_files.go174
-rw-r--r--predict_set.go12
-rw-r--r--predict_test.go271
-rw-r--r--testing.go29
50 files changed, 2581 insertions, 2282 deletions
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
-
-// 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
- }
- prediction = append(prediction, flag)
+func (c *Command) SubCmdGet(cmd string) Completer {
+ if c.Sub[cmd] == nil {
+ return nil
}
- return
+ return c.Sub[cmd]
}
-
-// 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
- }
- }
-
- // 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
+func (c *Command) FlagList() []string {
+ flags := make([]string, 0, len(c.Flags))
+ for flag := range c.Flags {
+ flags = append(flags, flag)
}
+ return flags
+}
- 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)...)
- }
+func (c *Command) FlagGet(flag string) Predictor {
+ return PredictFunc(func(prefix string) (options []string) {
+ f := c.Flags[flag]
+ if f == nil {
+ return nil
+ }
+ return f.Predict(prefix)
+ })
+}
- return
+func (c *Command) ArgsGet() Predictor {
+ return PredictFunc(func(prefix string) (options []string) {
+ if c.Args == nil {
+ return nil
+ }
+ 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
+// 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
+ }
+ if line == "" {
+ return
+ }
+ i, err := strconv.Atoi(point)
+ if err != nil {
+ panic("COMP_POINT env should be integer, got: " + point)
+ }
+
+ // 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)
+}
+
+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("", "")
}
-// 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,
+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...)
}
-// 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()
+func (c completer) iterateStack(f func(Completer)) {
+ for _, cmd := range append([]Completer{c.Completer}, c.stack...) {
+ f(cmd)
+ }
}
-// 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()
+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)
}
- if point >= 0 && point < len(line) {
- line = line[:point]
+ // Add help flag if needed.
+ help := helpFlag(dashes + prefix)
+ if len(options) == 0 || strings.HasPrefix(help, dashes+prefix) {
+ options = append(options, help)
}
- Log("Completing phrase: %s", line)
- a := newArgs(line)
- Log("Completing last field: %s", a.Last)
- options := c.Command.Predict(a)
- Log("Options: %s", options)
+ return options
+}
- // filter only options that match the last argument
- matches := []string{}
+func filterByPrefix(prefix string, options ...string) []string {
+ var filtered []string
for _, option := range options {
- if strings.HasPrefix(option, a.Last) {
- matches = append(matches, option)
+ if fixed, ok := hasPrefix(option, prefix); ok {
+ filtered = append(filtered, fixed)
}
}
- Log("Matches: %s", matches)
- c.output(matches)
- return true
+ if len(filtered) > 0 {
+ return filtered
+ }
+ return options
}
-func getEnv() (line string, point int, ok bool) {
- line = os.Getenv(envLine)
- if line == "" {
- return
+// 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++
}
- point, err := strconv.Atoi(os.Getenv(envPoint))
- 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)
+
+ if pi < len(prefix) {
+ return "", false
}
- return line, point, true
+
+ for ; si < len(s); si++ {
+ token.Visit(s[si])
+ }
+
+ return token.Closed(), true
}
-func (c *Complete) output(options []string) {
- // stdout of program defines the complete options
- for _, option := range options {
- fmt.Fprintln(c.Out, option)
+// 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"
-)
-func TestCompleter_Complete(t *testing.T) {
- initTests()
+ "github.com/posener/complete/internal/arg"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
- c := Command{
- Sub: Commands{
- "sub1": {
- Flags: Flags{
- "-flag1": PredictAnything,
- "-flag2": PredictNothing,
- },
- Sub: Commands{
- "sub11": {},
- },
+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,
},
- "sub2": {
- Flags: Flags{
- "-flag2": PredictNothing,
- "-flag3": PredictSet("opt1", "opt2", "opt12"),
- },
- Args: PredictFiles("*.md"),
- },
- "sub3": {
- Sub: Commands{
- "sub3": {},
+ },
+ "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"},
},
- Flags: Flags{
- "-o": PredictFiles("*.txt"),
+ "sub2": &Command{},
+ "args": &Command{
+ Args: set{"a", "a a", "b"},
},
- 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.
- 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)
+ {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"}},
- sort.Strings(tt.want)
- sort.Strings(got)
+ // Test sub command completion.
- if !equalSlices(got, tt.want) {
- t.Errorf("failed '%s'\ngot: %s\nwant: %s", t.Name(), got, tt.want)
- }
+ {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(tt.args, func(t *testing.T) {
+ Test(t, testCmd, tt.args, tt.want)
})
}
}
-func TestCompleter_Complete_SharedPrefix(t *testing.T) {
- initTests()
+func TestCompleter_error(t *testing.T) {
+ t.Parallel()
- 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,
- },
+ tests := []struct {
+ args string
+ err string
+ }{
+ // Sub command already fully typed but unknown.
+ {args: "x ", err: "unknown subcommand: x"},
}
- cmp := New("cmd", c)
+ 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 string
- point int // -1 indicates len(line)
- want []string
+ line, point string
+ shouldExit bool
+ shouldPanic bool
+ install string
+ uninstall string
}{
- {
- 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"},
- },
+ {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, 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.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)
}
})
}
}
-// 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
-}
+type set []string
-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 (s set) Predict(_ string) []string {
+ return s
}
-func equalSlices(a, b []string) bool {
- if len(a) != len(b) {
- return false
+func TestHasPrefix(t *testing.T) {
+ t.Parallel()
+
+ tests := []struct {
+ s string
+ prefix string
+ want string
+ wantOK bool
+ }{
+ {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 i := range a {
- if a[i] != b[i] {
- return false
- }
+
+ for _, tt := range tests {
+ 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)
+ })
}
- 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:
-Go Command Bash Completion
+* 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.
-In ./cmd/gocomplete there is an example for bash completion for the `go` command line.
+The library and tools are extensible such that any program can add its one logic, completion types
+or methologies.
-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.
+Go Command Bash Completion
+
+./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.
-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.
-
-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`.
+Add bash completion capabilities to any Go program. See ./example/command.
-So here it is:
+ import (
+ "flag"
+ "github.com/posener/complete"
+ "github.com/posener/complete/predict"
+ )
- 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 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 - provide it with the binary name.
+ cmd.Complete("my-program")
+ // Parse the flags.
+ flag.Parse()
+ // Program logic...
+ }
- // 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 {
+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).
- // 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,
- },
- },
- },
+ import (
+ "flag"
+ + "github.com/posener/complete"
+ )
+ var (
+ // Define flags here...
+ foo = flag.Bool("foo", false, "")
+ )
- // 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"),
- },
+ func main() {
+ // Call command line completion before parsing the flags - provide it with the binary name.
+ + complete.CommandLine("my-program")
+ flag.Parse()
+ }
- // 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{
+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).
- // a flag '-h' which does not expects anything after it
- "-h": complete.PredictNothing,
- },
- }
+ 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, "")
+ )
- // 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()
+ func main() {
+ // Call command line completion before parsing the flags.
+ + complete.CommandLine("my-program")
+ flag.Parse()
}
-Self completing program
+Instead of calling both `complete.CommandLine` and `flag.Parse`, one can call just `compflag.Parse`
+which does them both.
-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 .
+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
index 17c64de..17c64de 100644
--- a/cmd/install/bash.go
+++ b/internal/install/bash.go
diff --git a/cmd/install/fish.go b/internal/install/fish.go
index 2b64bfc..2b64bfc 100644
--- a/cmd/install/fish.go
+++ b/internal/install/fish.go
diff --git a/cmd/install/install.go b/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
index d34ac8c..d34ac8c 100644
--- a/cmd/install/utils.go
+++ b/internal/install/utils.go
diff --git a/cmd/install/zsh.go b/internal/install/zsh.go
index 29950ab..29950ab 100644
--- a/cmd/install/zsh.go
+++ b/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
index e69de29..e69de29 100644
--- a/tests/.dot.txt
+++ b/predict/testdata/.dot.txt
diff --git a/tests/a.txt b/predict/testdata/a.txt
index e69de29..e69de29 100644
--- a/tests/a.txt
+++ b/predict/testdata/a.txt
diff --git a/tests/b.txt b/predict/testdata/b.txt
index e69de29..e69de29 100644
--- a/tests/b.txt
+++ b/predict/testdata/b.txt
diff --git a/tests/c.txt b/predict/testdata/c.txt
index e69de29..e69de29 100644
--- a/tests/c.txt
+++ b/predict/testdata/c.txt
diff --git a/tests/dir/bar b/predict/testdata/dir/bar
index e69de29..e69de29 100644
--- a/tests/dir/bar
+++ b/predict/testdata/dir/bar
diff --git a/tests/dir/foo b/predict/testdata/dir/foo
index e69de29..e69de29 100644
--- a/tests/dir/foo
+++ b/predict/testdata/dir/foo
diff --git a/tests/outer/inner/readme.md b/predict/testdata/outer/inner/readme.md
index e69de29..e69de29 100644
--- a/tests/outer/inner/readme.md
+++ b/predict/testdata/outer/inner/readme.md
diff --git a/tests/readme.md b/predict/testdata/readme.md
index 25ea22c..25ea22c 100644
--- a/tests/readme.md
+++ b/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
+ }
+ }
+}