Merge pull request #104 from posener/v2

Complete v2
This commit is contained in:
Eyal Posener 2019-11-18 01:10:06 +02:00 committed by GitHub
commit 00c7945465
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
50 changed files with 2608 additions and 2309 deletions

View File

@ -1,6 +1,7 @@
language: go language: go
go: go:
- tip - tip
- 1.13.x
- 1.12.x - 1.12.x
- 1.11.x - 1.11.x
- 1.10.x - 1.10.x

114
args.go
View File

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

View File

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

View File

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

View File

@ -1,111 +1,62 @@
package complete package complete
// Command represents a command line // Command is an object that can be used to create complete options for a go executable that does
// It holds the data that enables auto completion of command line // not have a good binding to the `Completer` interface, or to use a Go program as complete binary
// Command can also be a sub command. // for another executable (see ./gocomplete as an example.)
type Command struct { type Command struct {
// Sub is map of sub commands of the current command // Sub is map of sub commands of the current command. The key refer to the sub command name, and
// The key refer to the sub command name, and the value is it's // the value is it's command descriptive struct.
// Command descriptive struct. Sub map[string]*Command
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. In a chain of sub commands, no duplicate flags should be defined.
// Flags is a map of flags that the command accepts. Flags map[string]Predictor
// The key is the flag name, and the value is it's predictions. // Args are extra arguments that the command accepts, those who are given without any flag
Flags Flags // before. In any chain of sub commands, only one of them should predict positional arguments.
// 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.
Args Predictor Args Predictor
} }
// Predict returns all possible predictions for args according to the command struct // Complete runs the completion of the described command.
func (c *Command) Predict(a Args) []string { func (c *Command) Complete(name string) {
options, _ := c.predict(a) Complete(name, c)
return options
} }
// Commands is the type of Sub member, it maps a command name to a command struct func (c *Command) SubCmdList() []string {
type Commands map[string]Command subs := make([]string, 0, len(c.Sub))
for sub := range c.Sub {
// Predict completion of sub command names names according to command line arguments subs = append(subs, sub)
func (c Commands) Predict(a Args) (prediction []string) {
for sub := range c {
prediction = append(prediction, sub)
} }
return return subs
} }
// Flags is the type Flags of the Flags member, it maps a flag name to the flag predictions. func (c *Command) SubCmdGet(cmd string) Completer {
type Flags map[string]Predictor if c.Sub[cmd] == nil {
return nil
// 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) return c.Sub[cmd]
}
func (c *Command) FlagList() []string {
flags := make([]string, 0, len(c.Flags))
for flag := range c.Flags {
flags = append(flags, flag)
} }
return return flags
} }
// predict options func (c *Command) FlagGet(flag string) Predictor {
// only is set to true if no more options are allowed to be returned return PredictFunc(func(prefix string) (options []string) {
// those are in cases of special flag that has specific completion arguments, f := c.Flags[flag]
// and other flags or sub commands can't come after it. if f == nil {
func (c *Command) predict(a Args) (options []string, only bool) { return nil
// 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
} }
return f.Predict(prefix)
// We matched so stop searching. Continuing to search can accidentally })
// match a subcommand with current set of commands, see issue #46. }
break
} func (c *Command) ArgsGet() Predictor {
} return PredictFunc(func(prefix string) (options []string) {
if c.Args == nil {
// if last completed word is a global flag that we need to complete return nil
if predictor, ok := c.GlobalFlags[a.LastCompleted]; ok && predictor != nil { }
Log("Predicting according to global flag %s", a.LastCompleted) return c.Args.Predict(prefix)
return predictor.Predict(a), true })
}
options = append(options, c.GlobalFlags.Predict(a)...)
// if a sub command was entered, we won't add the parent command
// completions and we return here.
if subCommandFound {
return
}
// if last completed word is a command flag that we need to complete
if predictor, ok := c.Flags[a.LastCompleted]; ok && predictor != nil {
Log("Predicting according to flag %s", a.LastCompleted)
return predictor.Predict(a), true
}
options = append(options, c.Sub.Predict(a)...)
options = append(options, c.Flags.Predict(a)...)
if c.Args != nil {
options = append(options, c.Args.Predict(a)...)
}
return
} }

View File

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

245
compflag/compflag.go Normal file
View File

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

105
compflag/compflag_test.go Normal file
View File

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

View File

@ -1,104 +1,332 @@
package complete package complete
import ( import (
"flag"
"fmt" "fmt"
"io" "io"
"os" "os"
"strconv" "strconv"
"strings" "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 ( // Completer is an interface that a command line should implement in order to get bash completion.
envLine = "COMP_LINE" type Completer interface {
envPoint = "COMP_POINT" // SubCmdList should return the list of all sub commands of the current command.
envDebug = "COMP_DEBUG" 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 // Complete the command line arguments for the given command in the case that the program
type Complete struct { // was invoked with COMP_LINE and COMP_POINT environment variables. In that case it will also
Command Command // `os.Exit()`. The program name should be provided for installation purposes.
cmd.CLI func Complete(name string, cmd Completer) {
Out io.Writer var (
} line = getEnv("COMP_LINE")
point = getEnv("COMP_POINT")
// New creates a new complete command. doInstall = getEnv("COMP_INSTALL") == "1"
// name is the name of command we want to auto complete. doUninstall = getEnv("COMP_UNINSTALL") == "1"
// IMPORTANT: it must be the same name - if the auto complete yes = getEnv("COMP_YES") == "1"
// completes the 'go' command, name must be equal to "go". )
// command is the struct of the command completion. if doInstall || doUninstall {
func New(name string, command Command) *Complete { install.Run(name, doUninstall, yes, out, in)
return &Complete{ exit(0)
Command: command, return
CLI: cmd.CLI{Name: name},
Out: os.Stdout,
} }
}
// Run runs the completion and add installation flags beforehand.
// The flags are added to the main flag CommandLine variable.
func (c *Complete) Run() bool {
c.AddFlags(nil)
flag.Parse()
return c.Complete()
}
// Complete a command from completion line in environment variable,
// and print out the complete options.
// returns success if the completion ran or if the cli matched
// any of the given flags, false otherwise
// For installation: it assumes that flags were added and parsed before
// it was called.
func (c *Complete) Complete() bool {
line, point, ok := getEnv()
if !ok {
// make sure flags parsed,
// in case they were not added in the main program
return c.CLI.Run()
}
if point >= 0 && point < len(line) {
line = line[:point]
}
Log("Completing phrase: %s", line)
a := newArgs(line)
Log("Completing last field: %s", a.Last)
options := c.Command.Predict(a)
Log("Options: %s", options)
// filter only options that match the last argument
matches := []string{}
for _, option := range options {
if strings.HasPrefix(option, a.Last) {
matches = append(matches, option)
}
}
Log("Matches: %s", matches)
c.output(matches)
return true
}
func getEnv() (line string, point int, ok bool) {
line = os.Getenv(envLine)
if line == "" { if line == "" {
return return
} }
point, err := strconv.Atoi(os.Getenv(envPoint)) i, err := strconv.Atoi(point)
if err != nil { if err != nil {
// If failed parsing point for some reason, set it to point panic("COMP_POINT env should be integer, got: " + point)
// on the end of the line.
Log("Failed parsing point %s: %v", os.Getenv(envPoint), err)
point = len(line)
} }
return line, point, true
// Parse the command line up to the completion point.
args := arg.Parse(line[:i])
// The first word is the current command name.
args = args[1:]
// Run the completion algorithm.
options, err := completer{Completer: cmd, args: args}.complete()
if err != nil {
fmt.Fprintln(out, "\n"+err.Error())
} else {
for _, option := range options {
fmt.Fprintln(out, option)
}
}
exit(0)
} }
func (c *Complete) output(options []string) { type completer struct {
// stdout of program defines the complete options Completer
for _, option := range options { args []arg.Arg
fmt.Fprintln(c.Out, option) stack []Completer
}
// compete command with given before and after text.
// if the command has sub commands: try to complete only sub commands or help flags. Otherwise
// complete flags and positional arguments.
func (c completer) complete() ([]string, error) {
reset:
arg := arg.Arg{}
if len(c.args) > 0 {
arg = c.args[0]
}
switch {
case len(c.SubCmdList()) == 0:
// No sub commands, parse flags and positional arguments.
return c.suggestLeafCommandOptions(), nil
// case !arg.Completed && arg.IsFlag():
// Suggest help flags for command
// return []string{helpFlag(arg.Text)}, nil
case !arg.Completed:
// Currently typing a sub command.
return c.suggestSubCommands(arg.Text), nil
case c.SubCmdGet(arg.Text) != nil:
// Sub command completed, look into that sub command completion.
// Set the complete command to the requested sub command, and the before text to all the text
// after the command name and rerun the complete algorithm with the new sub command.
c.stack = append([]Completer{c.Completer}, c.stack...)
c.Completer = c.SubCmdGet(arg.Text)
c.args = c.args[1:]
goto reset
default:
// Sub command is unknown...
return nil, fmt.Errorf("unknown subcommand: %s", arg.Text)
} }
} }
func (c completer) suggestSubCommands(prefix string) []string {
if len(prefix) > 0 && prefix[0] == '-' {
return []string{helpFlag(prefix)}
}
subs := c.SubCmdList()
return suggest("", prefix, func(prefix string) []string {
var options []string
for _, sub := range subs {
if strings.HasPrefix(sub, prefix) {
options = append(options, sub)
}
}
return options
})
}
func (c completer) suggestLeafCommandOptions() (options []string) {
arg, before := arg.Arg{}, arg.Arg{}
if len(c.args) > 0 {
arg = c.args[len(c.args)-1]
}
if len(c.args) > 1 {
before = c.args[len(c.args)-2]
}
if !arg.Completed {
// Complete value being typed.
if arg.HasValue {
// Complete value of current flag.
if arg.HasFlag {
return c.suggestFlagValue(arg.Flag, arg.Value)
}
// Complete value of flag in a previous argument.
if before.HasFlag && !before.HasValue {
return c.suggestFlagValue(before.Flag, arg.Value)
}
}
// A value with no flag. Suggest positional argument.
if !arg.HasValue {
options = c.suggestFlag(arg.Dashes, arg.Flag)
}
if !arg.HasFlag {
options = append(options, c.suggestArgsValue(arg.Value)...)
}
// Suggest flag according to prefix.
return options
}
// Has a value that was already completed. Suggest all flags and positional arguments.
if arg.HasValue {
options = c.suggestFlag(arg.Dashes, "")
if !arg.HasFlag {
options = append(options, c.suggestArgsValue("")...)
}
return options
}
// A flag without a value. Suggest a value or suggest any flag.
options = c.suggestFlagValue(arg.Flag, "")
if len(options) > 0 {
return options
}
return c.suggestFlag("", "")
}
func (c completer) suggestFlag(dashes, prefix string) []string {
if dashes == "" {
dashes = "-"
}
return suggest(dashes, prefix, func(prefix string) []string {
var options []string
c.iterateStack(func(cmd Completer) {
// Suggest all flags with the given prefix.
for _, name := range cmd.FlagList() {
if strings.HasPrefix(name, prefix) {
options = append(options, dashes+name)
}
}
})
return options
})
}
func (c completer) suggestFlagValue(flagName, prefix string) []string {
var options []string
c.iterateStack(func(cmd Completer) {
if len(options) == 0 {
if p := cmd.FlagGet(flagName); p != nil {
options = p.Predict(prefix)
}
}
})
return filterByPrefix(prefix, options...)
}
func (c completer) suggestArgsValue(prefix string) []string {
var options []string
c.iterateStack(func(cmd Completer) {
if len(options) == 0 {
if p := cmd.ArgsGet(); p != nil {
options = p.Predict(prefix)
}
}
})
return filterByPrefix(prefix, options...)
}
func (c completer) iterateStack(f func(Completer)) {
for _, cmd := range append([]Completer{c.Completer}, c.stack...) {
f(cmd)
}
}
func suggest(dashes, prefix string, collect func(prefix string) []string) []string {
options := collect(prefix)
// If nothing was suggested, suggest all flags.
if len(options) == 0 {
prefix = ""
options = collect(prefix)
}
// Add help flag if needed.
help := helpFlag(dashes + prefix)
if len(options) == 0 || strings.HasPrefix(help, dashes+prefix) {
options = append(options, help)
}
return options
}
func filterByPrefix(prefix string, options ...string) []string {
var filtered []string
for _, option := range options {
if fixed, ok := hasPrefix(option, prefix); ok {
filtered = append(filtered, fixed)
}
}
if len(filtered) > 0 {
return filtered
}
return options
}
// hasPrefix checks if s has the give prefix. It disregards quotes and escaped spaces, and return
// s in the form of the given prefix.
func hasPrefix(s, prefix string) (string, bool) {
var (
token tokener.Tokener
si, pi int
)
for ; pi < len(prefix); pi++ {
token.Visit(prefix[pi])
lastQuote := !token.Escaped() && (prefix[pi] == '"' || prefix[pi] == '\'')
if lastQuote {
continue
}
if si == len(s) {
break
}
if s[si] == ' ' && !token.Quoted() && token.Escaped() {
s = s[:si] + "\\" + s[si:]
}
if s[si] != prefix[pi] {
return "", false
}
si++
}
if pi < len(prefix) {
return "", false
}
for ; si < len(s); si++ {
token.Visit(s[si])
}
return token.Closed(), true
}
// helpFlag returns either "-h", "-help" or "--help".
func helpFlag(prefix string) string {
if prefix == "" || prefix == "-" || prefix == "-h" {
return "-h"
}
if strings.HasPrefix(prefix, "--") {
return "--help"
}
return "-help"
}

View File

@ -1,414 +1,246 @@
package complete package complete
import ( import (
"bytes" "io/ioutil"
"fmt"
"os" "os"
"sort"
"strconv"
"strings"
"testing" "testing"
"github.com/posener/complete/internal/arg"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestCompleter_Complete(t *testing.T) { var testCmd = &Command{
initTests() Flags: map[string]Predictor{"cmd-flag": nil},
Sub: map[string]*Command{
"flags": &Command{
Flags: map[string]Predictor{
"values": set{"a", "a a", "b"},
"something": set{""},
"nothing": nil,
},
},
"sub1": &Command{
Flags: map[string]Predictor{"flag1": nil},
Sub: map[string]*Command{
"sub11": &Command{
Flags: map[string]Predictor{"flag11": nil},
},
"sub12": &Command{},
},
Args: set{"arg1", "arg2"},
},
"sub2": &Command{},
"args": &Command{
Args: set{"a", "a a", "b"},
},
},
}
c := Command{ func TestCompleter(t *testing.T) {
Sub: Commands{ t.Parallel()
"sub1": {
Flags: Flags{
"-flag1": PredictAnything,
"-flag2": PredictNothing,
},
Sub: Commands{
"sub11": {},
},
},
"sub2": {
Flags: Flags{
"-flag2": PredictNothing,
"-flag3": PredictSet("opt1", "opt2", "opt12"),
},
Args: PredictFiles("*.md"),
},
"sub3": {
Sub: Commands{
"sub3": {},
},
},
},
Flags: Flags{
"-o": PredictFiles("*.txt"),
},
GlobalFlags: Flags{
"-h": PredictNothing,
"-global1": PredictAnything,
},
}
cmp := New("cmd", c)
tests := []struct { tests := []struct {
line string args string
point int // -1 indicates len(line)
want []string want []string
}{ }{
{ // Check empty flag name matching.
line: "cmd ",
point: -1, {args: "flags ", want: []string{"-values", "-nothing", "-something", "-cmd-flag", "-h"}},
want: []string{"sub1", "sub2", "sub3"}, {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.
line: "cmd -", {args: "flags -x", want: []string{"-values", "-nothing", "-something", "-cmd-flag", "-h"}},
point: -1, // Check prefix matching for chain of sub commands.
want: []string{"-h", "-global1", "-o"}, {args: "sub1 sub11 -fl", want: []string{"-flag11", "-flag1"}},
}, {args: "sub1 sub11 --fl", want: []string{"--flag11", "--flag1"}},
{
line: "cmd -h ", // Test sub command completion.
point: -1,
want: []string{"sub1", "sub2", "sub3"}, {args: "", want: []string{"flags", "sub1", "sub2", "args", "-h"}},
}, {args: " ", want: []string{"flags", "sub1", "sub2", "args", "-h"}},
{ {args: "f", want: []string{"flags"}},
line: "cmd -global1 ", // global1 is known follow flag {args: "sub", want: []string{"sub1", "sub2"}},
point: -1, {args: "sub1", want: []string{"sub1"}},
want: []string{}, {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"}},
line: "cmd sub",
point: -1, // Suggest flag value.
want: []string{"sub1", "sub2", "sub3"},
}, // 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.
line: "cmd sub1", {args: "flags -something ", want: []string{""}},
point: -1, {args: "flags -something foo", want: []string{""}},
want: []string{"sub1"}, // 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.
line: "cmd sub2", {args: "flags -nothing=", want: []string{}},
point: -1, // The flag value was not started, suggest all relevant values.
want: []string{"sub2"}, {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"}},
line: "cmd sub1 ", {args: "flags -values a\\ ", want: []string{"a\\ a"}},
point: -1, {args: "flags -values a\\ a", want: []string{"a\\ a"}},
want: []string{"sub11"}, {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\""}},
line: "cmd sub3 ", {args: "flags -values \"a a", want: []string{"\"a a\""}},
point: -1, {args: "flags -values \"a a\"", want: []string{"\"a a\""}},
want: []string{"sub3"}, {args: "flags -values \"a a\" ", want: []string{"-values", "-nothing", "-something", "-cmd-flag", "-h"}},
},
{ {args: "flags -values=", want: []string{"a", "a\\ a", "b"}},
line: "cmd sub1 -", {args: "flags -values=a", want: []string{"a", "a\\ a"}},
point: -1, {args: "flags -values=a\\", want: []string{"a\\ a"}},
want: []string{"-flag1", "-flag2", "-h", "-global1"}, {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"}},
line: "cmd sub2 ", {args: "flags -values=\"a", want: []string{"\"a\"", "\"a a\""}},
point: -1, {args: "flags -values=\"a ", want: []string{"\"a a\""}},
want: []string{"./", "dir/", "outer/", "readme.md"}, {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"}},
line: "cmd sub2 ./",
point: -1, // Complete positional arguments
want: []string{"./", "./readme.md", "./dir/", "./outer/"},
}, {args: "args ", want: []string{"-cmd-flag", "-h", "a", "a\\ a", "b"}},
{ {args: "args a", want: []string{"a", "a\\ a"}},
line: "cmd sub2 re", {args: "args a\\", want: []string{"a\\ a"}},
point: -1, {args: "args a\\ ", want: []string{"a\\ a"}},
want: []string{"readme.md"}, {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\""}},
line: "cmd sub2 ./re", {args: "args \"a ", want: []string{"\"a a\""}},
point: -1, {args: "args \"a a", want: []string{"\"a a\""}},
want: []string{"./readme.md"}, {args: "args \"a a\"", want: []string{"\"a a\""}},
}, {args: "args \"a a\" ", want: []string{"-cmd-flag", "-h", "a", "a\\ a", "b"}},
{
line: "cmd sub2 -flag2 ", // Complete positional arguments from a parent command
point: -1, {args: "sub1 sub12 arg", want: []string{"arg1", "arg2"}},
want: []string{"./", "dir/", "outer/", "readme.md"},
}, // Test help
{
line: "cmd sub1 -fl", {args: "-", want: []string{"-h"}},
point: -1, {args: " -", want: []string{"-h"}},
want: []string{"-flag1", "-flag2"}, {args: "--", want: []string{"--help"}},
}, {args: "-he", want: []string{"-help"}},
{ {args: "-x", want: []string{"-help"}},
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"},
},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(fmt.Sprintf("%s@%d", tt.line, tt.point), func(t *testing.T) { t.Run(tt.args, func(t *testing.T) {
got := runComplete(cmp, tt.line, tt.point) Test(t, testCmd, tt.args, tt.want)
})
}
}
sort.Strings(tt.want) func TestCompleter_error(t *testing.T) {
sort.Strings(got) t.Parallel()
if !equalSlices(got, tt.want) { tests := []struct {
t.Errorf("failed '%s'\ngot: %s\nwant: %s", t.Name(), got, tt.want) args string
err string
}{
// Sub command already fully typed but unknown.
{args: "x ", err: "unknown subcommand: x"},
}
for _, tt := range tests {
t.Run(tt.args, func(t *testing.T) {
_, err := completer{Completer: testCmd, args: arg.Parse(tt.args)}.complete()
require.Error(t, err)
assert.Equal(t, tt.err, err.Error())
})
}
}
func TestComplete(t *testing.T) {
defer func() {
getEnv = os.Getenv
exit = os.Exit
out = os.Stdout
}()
tests := []struct {
line, point string
shouldExit bool
shouldPanic bool
install string
uninstall string
}{
{shouldExit: true, line: "cmd", point: "1"},
{shouldExit: false, line: "", point: ""},
{shouldPanic: true, line: "cmd", point: ""},
{shouldPanic: true, line: "cmd", point: "a"},
{shouldPanic: true, line: "cmd", point: "4"},
{shouldExit: true, install: "1"},
{shouldExit: false, install: "a"},
{shouldExit: true, uninstall: "1"},
{shouldExit: false, uninstall: "a"},
}
for _, tt := range tests {
t.Run(tt.line+"@"+tt.point, func(t *testing.T) {
getEnv = func(env string) string {
switch env {
case "COMP_LINE":
return tt.line
case "COMP_POINT":
return tt.point
case "COMP_INSTALL":
return tt.install
case "COMP_UNINSTALL":
return tt.uninstall
case "COMP_YES":
return "0"
default:
panic(env)
}
}
isExit := false
exit = func(int) {
isExit = true
}
out = ioutil.Discard
if tt.shouldPanic {
assert.Panics(t, func() { testCmd.Complete("") })
} else {
testCmd.Complete("")
assert.Equal(t, tt.shouldExit, isExit)
} }
}) })
} }
} }
func TestCompleter_Complete_SharedPrefix(t *testing.T) { type set []string
initTests()
c := Command{ func (s set) Predict(_ string) []string {
Sub: Commands{ return s
"status": { }
Flags: Flags{
"-f3": PredictNothing,
},
},
"job": {
Sub: Commands{
"status": {
Flags: Flags{
"-f4": PredictNothing,
},
},
},
},
},
Flags: Flags{
"-o": PredictFiles("*.txt"),
},
GlobalFlags: Flags{
"-h": PredictNothing,
"-global1": PredictAnything,
},
}
cmp := New("cmd", c) func TestHasPrefix(t *testing.T) {
t.Parallel()
tests := []struct { tests := []struct {
line string s string
point int // -1 indicates len(line) prefix string
want []string want string
wantOK bool
}{ }{
{ {s: "ab", prefix: `b`, want: ``, wantOK: false},
line: "cmd ", {s: "", prefix: `b`, want: ``, wantOK: false},
point: -1, {s: "ab", prefix: `a`, want: `ab`, wantOK: true},
want: []string{"status", "job"}, {s: "ab", prefix: `"'b`, want: ``, wantOK: false},
}, {s: "ab", prefix: `"'a`, want: `"'ab'"`, wantOK: true},
{ {s: "ab", prefix: `'"a`, want: `'"ab"'`, wantOK: true},
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"},
},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.line, func(t *testing.T) { t.Run(tt.s+"/"+tt.prefix, func(t *testing.T) {
got := runComplete(cmp, tt.line, tt.point) got, gotOK := hasPrefix(tt.s, tt.prefix)
assert.Equal(t, tt.want, got)
sort.Strings(tt.want) assert.Equal(t, tt.wantOK, gotOK)
sort.Strings(got)
if !equalSlices(got, tt.want) {
t.Errorf("failed '%s'\ngot = %s\nwant: %s", t.Name(), got, tt.want)
}
}) })
} }
} }
// runComplete runs the complete login for test purposes
// it gets the complete struct and command line arguments and returns
// the complete options
func runComplete(c *Complete, line string, point int) (completions []string) {
if point == -1 {
point = len(line)
}
os.Setenv(envLine, line)
os.Setenv(envPoint, strconv.Itoa(point))
b := bytes.NewBuffer(nil)
c.Out = b
c.Complete()
completions = parseOutput(b.String())
return
}
func parseOutput(output string) []string {
lines := strings.Split(output, "\n")
options := []string{}
for _, l := range lines {
if l != "" {
options = append(options, l)
}
}
return options
}
func equalSlices(a, b []string) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}

164
doc.go
View File

@ -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 Writing bash completion scripts is a hard work, usually done in the bash scripting language.
to create bash completion scripts for any command, and also an easy way to install/uninstall This package provides:
the completion of the command.
* A library for bash completion for Go programs.
* A tool for writing bash completion script in the Go language. For any Go or non Go program.
* Bash completion for the `go` command line (See ./gocomplete).
* Library for bash-completion enabled flags (See ./compflag).
* Enables an easy way to install/uninstall the completion of the command.
The library and tools are extensible such that any program can add its one logic, completion types
or methologies.
Go Command Bash Completion Go Command Bash Completion
In ./cmd/gocomplete there is an example for bash completion for the `go` command line. ./gocomplete is the script for bash completion for the `go` command line. This is an example
that uses the `complete` package on the `go` command - the `complete` package can also be used to
implement any completions, see #usage.
This is an example that uses the `complete` package on the `go` command - the `complete` package Install:
can also be used to implement any completions, see #usage.
Install
1. Type in your shell: 1. Type in your shell:
go get -u github.com/posener/complete/gocomplete go get -u github.com/posener/complete/gocomplete
gocomplete -install COMP_INSTALL=1 gocomplete
2. Restart your shell 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 packages names or `.go` files when necessary.
- Complete test names after `-run` flag. - Complete test names after `-run` flag.
Complete package Complete Package
Supported shells: Supported shells:
@ -39,72 +50,83 @@ Supported shells:
Usage Usage
Assuming you have program called `run` and you want to have bash completion Add bash completion capabilities to any Go program. See ./example/command.
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, import (
with a `func main()` and so, that will make the completion of the `run` "flag"
program. Once the `runcomplete` will be in a binary form, we could "github.com/posener/complete"
`runcomplete -install` and that will add to our shell all the bash completion "github.com/posener/complete/predict"
options for `run`. )
So here it is: var (
// Add variables to the program.
import "github.com/posener/complete" name = flag.String("name", "", "")
something = flag.String("something", "", "")
nothing = flag.String("nothing", "", "")
)
func main() { func main() {
// Create the complete command.
// create a Command object, that represents the command we want // Here we define completion values for each flag.
// to complete. cmd := &complete.Command{
run := complete.Command{ Flags: map[string]complete.Predictor{
"name": predict.Set{"foo", "bar", "foo bar"},
// Sub defines a list of sub commands of the program, "something": predict.Something,
// this is recursive, since every command is of type command also. "nothing": predict.Nothing,
Sub: complete.Commands{
// add a build sub command
"build": complete.Command {
// define flags of the build sub command
Flags: complete.Flags{
// build sub command has a flag '-cpus', which
// expects number of cpus after it. in that case
// anything could complete this flag.
"-cpus": complete.PredictAnything,
},
},
},
// define flags of the 'run' main command
Flags: complete.Flags{
// a flag -o, which expects a file ending with .out after
// it, the tab completion will auto complete for files matching
// the given pattern.
"-o": complete.PredictFiles("*.out"),
},
// define global flags of the 'run' main command
// those will show up also when a sub command was entered in the
// command line
GlobalFlags: complete.Flags{
// a flag '-h' which does not expects anything after it
"-h": complete.PredictNothing,
}, },
} }
// Run the completion - provide it with the binary name.
// run the command completion, as part of the main() function. cmd.Complete("my-program")
// this triggers the autocompletion when needed. // Parse the flags.
// name must be exactly as the binary that we want to complete. flag.Parse()
complete.New("run", run).Run() // Program logic...
} }
Self completing program This package also enables to complete flags defined by the standard library `flag` package.
To use this feature, simply call `complete.CommandLine` before `flag.Parse`. (See ./example/stdlib).
In case that the program that we want to complete is written in go we import (
can make it self completing. "flag"
Here is an example: ./example/self/main.go . + "github.com/posener/complete"
)
var (
// Define flags here...
foo = flag.Bool("foo", false, "")
)
func main() {
// Call command line completion before parsing the flags - provide it with the binary name.
+ complete.CommandLine("my-program")
flag.Parse()
}
If flag value completion is desired, it can be done by providing the standard library `flag.Var`
function a `flag.Value` that also implements the `complete.Predictor` interface. For standard
flag with values, it is possible to use the `github.com/posener/complete/compflag` package.
(See ./example/compflag).
import (
"flag"
+ "github.com/posener/complete"
+ "github.com/posener/complete/compflag"
)
var (
// Define flags here...
- foo = flag.Bool("foo", false, "")
+ foo = compflag.Bool("foo", false, "")
)
func main() {
// Call command line completion before parsing the flags.
+ complete.CommandLine("my-program")
flag.Parse()
}
Instead of calling both `complete.CommandLine` and `flag.Parse`, one can call just `compflag.Parse`
which does them both.
Testing
For command line bash completion testing use the `complete.Test` function.
*/ */
package complete package complete

45
example/command/main.go Normal file
View File

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

31
example/compflag/main.go Normal file
View File

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

View File

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

35
example/stdlib/main.go Normal file
View File

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

44
flags.go Normal file
View File

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

57
flags_test.go Normal file
View File

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

File diff suppressed because it is too large Load Diff

View File

@ -4,16 +4,15 @@ import (
"go/ast" "go/ast"
"go/parser" "go/parser"
"go/token" "go/token"
"log"
"regexp" "regexp"
"github.com/posener/complete"
) )
func functionsInFile(path string, regexp *regexp.Regexp) (tests []string) { func functionsInFile(path string, regexp *regexp.Regexp) (tests []string) {
fset := token.NewFileSet() fset := token.NewFileSet()
f, err := parser.ParseFile(fset, path, nil, 0) f, err := parser.ParseFile(fset, path, nil, 0)
if err != nil { if err != nil {
complete.Log("Failed parsing %s: %s", path, err) log.Printf("Failed parsing %s: %s", path, err)
return nil return nil
} }
for _, d := range f.Decls { for _, d := range f.Decls {

View File

@ -3,18 +3,19 @@ package main
import ( import (
"go/build" "go/build"
"io/ioutil" "io/ioutil"
"log"
"os" "os"
"os/user" "os/user"
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/posener/complete" "github.com/posener/complete/predict"
) )
// predictPackages completes packages in the directory pointed by a.Last // predictPackages completes packages in the directory pointed by a.Last
// and packages that are one level below that package. // and packages that are one level below that package.
func predictPackages(a complete.Args) (prediction []string) { func predictPackages(prefix string) (prediction []string) {
prediction = []string{a.Last} prediction = []string{prefix}
lastPrediction := "" lastPrediction := ""
for len(prediction) == 1 && (lastPrediction == "" || lastPrediction != prediction[0]) { for len(prediction) == 1 && (lastPrediction == "" || lastPrediction != prediction[0]) {
// if only one prediction, predict files within this prediction, // 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 // level deeper and give the user the 'pkg' and all the nested packages within
// that package. // that package.
lastPrediction = prediction[0] lastPrediction = prediction[0]
a.Last = prediction[0] prefix = prediction[0]
prediction = predictLocalAndSystem(a) prediction = predictLocalAndSystem(prefix)
} }
return return
} }
func predictLocalAndSystem(a complete.Args) []string { func predictLocalAndSystem(prefix string) []string {
localDirs := complete.PredictFilesSet(listPackages(a.Directory())).Predict(a) localDirs := predict.FilesSet(listPackages(directory(prefix))).Predict(prefix)
// System directories are not actual file names, for example: 'github.com/posener/complete' could // 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 // 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. // is the reason to use the PredictSet and not the PredictDirs in this case.
s := systemDirs(a.Last) s := systemDirs(prefix)
sysDirs := complete.PredictSet(s...).Predict(a) sysDirs := predict.Set(s).Predict(prefix)
return append(localDirs, sysDirs...) return append(localDirs, sysDirs...)
} }
@ -45,7 +46,7 @@ func listPackages(dir string) (directories []string) {
// add subdirectories // add subdirectories
files, err := ioutil.ReadDir(dir) files, err := ioutil.ReadDir(dir)
if err != nil { if err != nil {
complete.Log("failed reading directory %s: %s", dir, err) log.Printf("failed reading directory %s: %s", dir, err)
return return
} }
@ -62,7 +63,7 @@ func listPackages(dir string) (directories []string) {
for _, p := range paths { for _, p := range paths {
pkg, err := build.ImportDir(p, 0) pkg, err := build.ImportDir(p, 0)
if err != nil { if err != nil {
complete.Log("failed importing directory %s: %s", p, err) log.Printf("failed importing directory %s: %s", p, err)
continue continue
} }
directories = append(directories, pkg.Dir) directories = append(directories, pkg.Dir)
@ -124,3 +125,53 @@ func findGopath() []string {
entries := strings.Split(gopath, listsep) entries := strings.Split(gopath, listsep)
return entries 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
}

View File

@ -20,7 +20,7 @@ var (
// for test names use prefix of 'Test' or 'Example', and for benchmark // for test names use prefix of 'Test' or 'Example', and for benchmark
// test names use 'Benchmark' // test names use 'Benchmark'
func funcPredict(funcRegexp *regexp.Regexp) complete.Predictor { 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) return funcNames(funcRegexp)
}) })
} }

View File

@ -14,7 +14,7 @@ func TestPredictions(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
predictor complete.Predictor predictor complete.Predictor
last string prefix string
want []string want []string
}{ }{
{ {
@ -31,8 +31,7 @@ func TestPredictions(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
a := complete.Args{Last: tt.last} got := tt.predictor.Predict(tt.prefix)
got := tt.predictor.Predict(a)
if !equal(got, tt.want) { if !equal(got, tt.want) {
t.Errorf("Failed %s: got: %q, want: %q", t.Name(), 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() { func Example() {
os.Setenv("COMP_LINE", "go ru") os.Setenv("COMP_LINE", "go ru")
os.Setenv("COMP_POINT", "5")
main() main()
// output: run // output: run
} }
func equal(s1, s2 []string) bool { func equal(s1, s2 []string) bool {

124
internal/arg/arg.go Normal file
View File

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

122
internal/arg/arg_test.go Normal file
View File

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

View File

@ -2,14 +2,42 @@ package install
import ( import (
"errors" "errors"
"fmt"
"io"
"os" "os"
"os/user" "os/user"
"path/filepath" "path/filepath"
"runtime" "runtime"
"strings"
"github.com/hashicorp/go-multierror" "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 { type installer interface {
IsInstalled(cmd, bin string) bool IsInstalled(cmd, bin string) bool
Install(cmd, bin string) error Install(cmd, bin string) error

View File

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

22
log.go
View File

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

View File

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

View File

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

View File

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

175
predict/files.go Normal file
View File

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

233
predict/files_test.go Normal file
View File

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

34
predict/predict.go Normal file
View File

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

61
predict/predict_test.go Normal file
View File

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

View File

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

View File

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

View File

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

29
testing.go Normal file
View File

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