This commit is contained in:
parent
05b68ffc81
commit
8724aaf183
|
@ -1,6 +1,7 @@
|
|||
language: go
|
||||
go:
|
||||
- tip
|
||||
- 1.13.x
|
||||
- 1.12.x
|
||||
- 1.11.x
|
||||
- 1.10.x
|
||||
|
|
114
args.go
114
args.go
|
@ -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]
|
||||
}
|
213
args_test.go
213
args_test.go
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
128
cmd/cmd.go
128
cmd/cmd.go
|
@ -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"
|
||||
}
|
||||
}
|
139
command.go
139
command.go
|
@ -1,111 +1,62 @@
|
|||
package complete
|
||||
|
||||
// Command represents a command line
|
||||
// It holds the data that enables auto completion of command line
|
||||
// Command can also be a sub command.
|
||||
// Command is an object that can be used to create complete options for a go executable that does
|
||||
// not have a good binding to the `Completer` interface, or to use a Go program as complete binary
|
||||
// for another executable (see ./gocomplete as an example.)
|
||||
type Command struct {
|
||||
// Sub is map of sub commands of the current command
|
||||
// The key refer to the sub command name, and the value is it's
|
||||
// Command descriptive struct.
|
||||
Sub Commands
|
||||
|
||||
// Flags is a map of flags that the command accepts.
|
||||
// The key is the flag name, and the value is it's predictions.
|
||||
Flags Flags
|
||||
|
||||
// GlobalFlags is a map of flags that the command accepts.
|
||||
// Global flags that can appear also after a sub command.
|
||||
GlobalFlags Flags
|
||||
|
||||
// Args are extra arguments that the command accepts, those who are
|
||||
// given without any flag before.
|
||||
// Sub is map of sub commands of the current command. The key refer to the sub command name, and
|
||||
// the value is it's command descriptive struct.
|
||||
Sub map[string]*Command
|
||||
// Flags is a map of flags that the command accepts. The key is the flag name, and the value is
|
||||
// it's predictions. In a chain of sub commands, no duplicate flags should be defined.
|
||||
Flags map[string]Predictor
|
||||
// Args are extra arguments that the command accepts, those who are given without any flag
|
||||
// before. In any chain of sub commands, only one of them should predict positional arguments.
|
||||
Args Predictor
|
||||
}
|
||||
|
||||
// Predict returns all possible predictions for args according to the command struct
|
||||
func (c *Command) Predict(a Args) []string {
|
||||
options, _ := c.predict(a)
|
||||
return options
|
||||
// Complete runs the completion of the described command.
|
||||
func (c *Command) Complete(name string) {
|
||||
Complete(name, c)
|
||||
}
|
||||
|
||||
// Commands is the type of Sub member, it maps a command name to a command struct
|
||||
type Commands map[string]Command
|
||||
|
||||
// Predict completion of sub command names names according to command line arguments
|
||||
func (c Commands) Predict(a Args) (prediction []string) {
|
||||
for sub := range c {
|
||||
prediction = append(prediction, sub)
|
||||
func (c *Command) SubCmdList() []string {
|
||||
subs := make([]string, 0, len(c.Sub))
|
||||
for sub := range c.Sub {
|
||||
subs = append(subs, sub)
|
||||
}
|
||||
return
|
||||
return subs
|
||||
}
|
||||
|
||||
// Flags is the type Flags of the Flags member, it maps a flag name to the flag predictions.
|
||||
type Flags map[string]Predictor
|
||||
func (c *Command) SubCmdGet(cmd string) Completer {
|
||||
if c.Sub[cmd] == nil {
|
||||
return nil
|
||||
}
|
||||
return c.Sub[cmd]
|
||||
}
|
||||
func (c *Command) FlagList() []string {
|
||||
flags := make([]string, 0, len(c.Flags))
|
||||
for flag := range c.Flags {
|
||||
flags = append(flags, flag)
|
||||
}
|
||||
return flags
|
||||
}
|
||||
|
||||
// Predict completion of flags names according to command line arguments
|
||||
func (f Flags) Predict(a Args) (prediction []string) {
|
||||
for flag := range f {
|
||||
// If the flag starts with a hyphen, we avoid emitting the prediction
|
||||
// unless the last typed arg contains a hyphen as well.
|
||||
flagHyphenStart := len(flag) != 0 && flag[0] == '-'
|
||||
lastHyphenStart := len(a.Last) != 0 && a.Last[0] == '-'
|
||||
if flagHyphenStart && !lastHyphenStart {
|
||||
continue
|
||||
func (c *Command) FlagGet(flag string) Predictor {
|
||||
return PredictFunc(func(prefix string) (options []string) {
|
||||
f := c.Flags[flag]
|
||||
if f == nil {
|
||||
return nil
|
||||
}
|
||||
prediction = append(prediction, flag)
|
||||
}
|
||||
return
|
||||
return f.Predict(prefix)
|
||||
})
|
||||
}
|
||||
|
||||
// predict options
|
||||
// only is set to true if no more options are allowed to be returned
|
||||
// those are in cases of special flag that has specific completion arguments,
|
||||
// and other flags or sub commands can't come after it.
|
||||
func (c *Command) predict(a Args) (options []string, only bool) {
|
||||
|
||||
// search sub commands for predictions first
|
||||
subCommandFound := false
|
||||
for i, arg := range a.Completed {
|
||||
if cmd, ok := c.Sub[arg]; ok {
|
||||
subCommandFound = true
|
||||
|
||||
// recursive call for sub command
|
||||
options, only = cmd.predict(a.from(i))
|
||||
if only {
|
||||
return
|
||||
}
|
||||
|
||||
// We matched so stop searching. Continuing to search can accidentally
|
||||
// match a subcommand with current set of commands, see issue #46.
|
||||
break
|
||||
func (c *Command) ArgsGet() Predictor {
|
||||
return PredictFunc(func(prefix string) (options []string) {
|
||||
if c.Args == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// if last completed word is a global flag that we need to complete
|
||||
if predictor, ok := c.GlobalFlags[a.LastCompleted]; ok && predictor != nil {
|
||||
Log("Predicting according to global flag %s", a.LastCompleted)
|
||||
return predictor.Predict(a), true
|
||||
}
|
||||
|
||||
options = append(options, c.GlobalFlags.Predict(a)...)
|
||||
|
||||
// if a sub command was entered, we won't add the parent command
|
||||
// completions and we return here.
|
||||
if subCommandFound {
|
||||
return
|
||||
}
|
||||
|
||||
// if last completed word is a command flag that we need to complete
|
||||
if predictor, ok := c.Flags[a.LastCompleted]; ok && predictor != nil {
|
||||
Log("Predicting according to flag %s", a.LastCompleted)
|
||||
return predictor.Predict(a), true
|
||||
}
|
||||
|
||||
options = append(options, c.Sub.Predict(a)...)
|
||||
options = append(options, c.Flags.Predict(a)...)
|
||||
if c.Args != nil {
|
||||
options = append(options, c.Args.Predict(a)...)
|
||||
}
|
||||
|
||||
return
|
||||
return c.Args.Predict(prefix)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
|
@ -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{""}
|
||||
}
|
|
@ -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"})
|
||||
})
|
||||
}
|
390
complete.go
390
complete.go
|
@ -1,104 +1,332 @@
|
|||
package complete
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/posener/complete/cmd"
|
||||
"github.com/posener/complete/internal/arg"
|
||||
"github.com/posener/complete/internal/install"
|
||||
"github.com/posener/complete/internal/tokener"
|
||||
)
|
||||
|
||||
const (
|
||||
envLine = "COMP_LINE"
|
||||
envPoint = "COMP_POINT"
|
||||
envDebug = "COMP_DEBUG"
|
||||
// Completer is an interface that a command line should implement in order to get bash completion.
|
||||
type Completer interface {
|
||||
// SubCmdList should return the list of all sub commands of the current command.
|
||||
SubCmdList() []string
|
||||
// SubCmdGet should return a sub command of the current command for the given sub command name.
|
||||
SubCmdGet(cmd string) Completer
|
||||
// FlagList should return a list of all the flag names of the current command. The flag names
|
||||
// should not have the dash prefix.
|
||||
FlagList() []string
|
||||
// FlagGet should return completion options for a given flag. It is invoked with the flag name
|
||||
// without the dash prefix. The flag is not promised to be in the command flags. In that case,
|
||||
// this method should return a nil predictor.
|
||||
FlagGet(flag string) Predictor
|
||||
// ArgsGet should return predictor for positional arguments of the command line.
|
||||
ArgsGet() Predictor
|
||||
}
|
||||
|
||||
// Predictor can predict completion options.
|
||||
type Predictor interface {
|
||||
// Predict returns prediction options for a given prefix. The prefix is what currently is typed
|
||||
// as a hint for what to return, but the returned values can have any prefix. The returned
|
||||
// values will be filtered by the prefix when needed regardless. The prefix may be empty which
|
||||
// means that no value was typed.
|
||||
Predict(prefix string) []string
|
||||
}
|
||||
|
||||
// PredictFunc is a function that implements the Predictor interface.
|
||||
type PredictFunc func(prefix string) []string
|
||||
|
||||
func (p PredictFunc) Predict(prefix string) []string {
|
||||
if p == nil {
|
||||
return nil
|
||||
}
|
||||
return p(prefix)
|
||||
}
|
||||
|
||||
var (
|
||||
getEnv = os.Getenv
|
||||
exit = os.Exit
|
||||
out io.Writer = os.Stdout
|
||||
in io.Reader = os.Stdin
|
||||
)
|
||||
|
||||
// Complete structs define completion for a command with CLI options
|
||||
type Complete struct {
|
||||
Command Command
|
||||
cmd.CLI
|
||||
Out io.Writer
|
||||
}
|
||||
|
||||
// New creates a new complete command.
|
||||
// name is the name of command we want to auto complete.
|
||||
// IMPORTANT: it must be the same name - if the auto complete
|
||||
// completes the 'go' command, name must be equal to "go".
|
||||
// command is the struct of the command completion.
|
||||
func New(name string, command Command) *Complete {
|
||||
return &Complete{
|
||||
Command: command,
|
||||
CLI: cmd.CLI{Name: name},
|
||||
Out: os.Stdout,
|
||||
// Complete the command line arguments for the given command in the case that the program
|
||||
// was invoked with COMP_LINE and COMP_POINT environment variables. In that case it will also
|
||||
// `os.Exit()`. The program name should be provided for installation purposes.
|
||||
func Complete(name string, cmd Completer) {
|
||||
var (
|
||||
line = getEnv("COMP_LINE")
|
||||
point = getEnv("COMP_POINT")
|
||||
doInstall = getEnv("COMP_INSTALL") == "1"
|
||||
doUninstall = getEnv("COMP_UNINSTALL") == "1"
|
||||
yes = getEnv("COMP_YES") == "1"
|
||||
)
|
||||
if doInstall || doUninstall {
|
||||
install.Run(name, doUninstall, yes, out, in)
|
||||
exit(0)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Run runs the completion and add installation flags beforehand.
|
||||
// The flags are added to the main flag CommandLine variable.
|
||||
func (c *Complete) Run() bool {
|
||||
c.AddFlags(nil)
|
||||
flag.Parse()
|
||||
return c.Complete()
|
||||
}
|
||||
|
||||
// Complete a command from completion line in environment variable,
|
||||
// and print out the complete options.
|
||||
// returns success if the completion ran or if the cli matched
|
||||
// any of the given flags, false otherwise
|
||||
// For installation: it assumes that flags were added and parsed before
|
||||
// it was called.
|
||||
func (c *Complete) Complete() bool {
|
||||
line, point, ok := getEnv()
|
||||
if !ok {
|
||||
// make sure flags parsed,
|
||||
// in case they were not added in the main program
|
||||
return c.CLI.Run()
|
||||
}
|
||||
|
||||
if point >= 0 && point < len(line) {
|
||||
line = line[:point]
|
||||
}
|
||||
|
||||
Log("Completing phrase: %s", line)
|
||||
a := newArgs(line)
|
||||
Log("Completing last field: %s", a.Last)
|
||||
options := c.Command.Predict(a)
|
||||
Log("Options: %s", options)
|
||||
|
||||
// filter only options that match the last argument
|
||||
matches := []string{}
|
||||
for _, option := range options {
|
||||
if strings.HasPrefix(option, a.Last) {
|
||||
matches = append(matches, option)
|
||||
}
|
||||
}
|
||||
Log("Matches: %s", matches)
|
||||
c.output(matches)
|
||||
return true
|
||||
}
|
||||
|
||||
func getEnv() (line string, point int, ok bool) {
|
||||
line = os.Getenv(envLine)
|
||||
if line == "" {
|
||||
return
|
||||
}
|
||||
point, err := strconv.Atoi(os.Getenv(envPoint))
|
||||
i, err := strconv.Atoi(point)
|
||||
if err != nil {
|
||||
// If failed parsing point for some reason, set it to point
|
||||
// on the end of the line.
|
||||
Log("Failed parsing point %s: %v", os.Getenv(envPoint), err)
|
||||
point = len(line)
|
||||
panic("COMP_POINT env should be integer, got: " + point)
|
||||
}
|
||||
return line, point, true
|
||||
|
||||
// Parse the command line up to the completion point.
|
||||
args := arg.Parse(line[:i])
|
||||
|
||||
// The first word is the current command name.
|
||||
args = args[1:]
|
||||
|
||||
// Run the completion algorithm.
|
||||
options, err := completer{Completer: cmd, args: args}.complete()
|
||||
if err != nil {
|
||||
fmt.Fprintln(out, "\n"+err.Error())
|
||||
} else {
|
||||
for _, option := range options {
|
||||
fmt.Fprintln(out, option)
|
||||
}
|
||||
}
|
||||
exit(0)
|
||||
}
|
||||
|
||||
func (c *Complete) output(options []string) {
|
||||
// stdout of program defines the complete options
|
||||
for _, option := range options {
|
||||
fmt.Fprintln(c.Out, option)
|
||||
type completer struct {
|
||||
Completer
|
||||
args []arg.Arg
|
||||
stack []Completer
|
||||
}
|
||||
|
||||
// compete command with given before and after text.
|
||||
// if the command has sub commands: try to complete only sub commands or help flags. Otherwise
|
||||
// complete flags and positional arguments.
|
||||
func (c completer) complete() ([]string, error) {
|
||||
reset:
|
||||
arg := arg.Arg{}
|
||||
if len(c.args) > 0 {
|
||||
arg = c.args[0]
|
||||
}
|
||||
switch {
|
||||
case len(c.SubCmdList()) == 0:
|
||||
// No sub commands, parse flags and positional arguments.
|
||||
return c.suggestLeafCommandOptions(), nil
|
||||
|
||||
// case !arg.Completed && arg.IsFlag():
|
||||
// Suggest help flags for command
|
||||
// return []string{helpFlag(arg.Text)}, nil
|
||||
|
||||
case !arg.Completed:
|
||||
// Currently typing a sub command.
|
||||
return c.suggestSubCommands(arg.Text), nil
|
||||
|
||||
case c.SubCmdGet(arg.Text) != nil:
|
||||
// Sub command completed, look into that sub command completion.
|
||||
// Set the complete command to the requested sub command, and the before text to all the text
|
||||
// after the command name and rerun the complete algorithm with the new sub command.
|
||||
c.stack = append([]Completer{c.Completer}, c.stack...)
|
||||
c.Completer = c.SubCmdGet(arg.Text)
|
||||
c.args = c.args[1:]
|
||||
goto reset
|
||||
|
||||
default:
|
||||
|
||||
// Sub command is unknown...
|
||||
return nil, fmt.Errorf("unknown subcommand: %s", arg.Text)
|
||||
}
|
||||
}
|
||||
|
||||
func (c completer) suggestSubCommands(prefix string) []string {
|
||||
if len(prefix) > 0 && prefix[0] == '-' {
|
||||
return []string{helpFlag(prefix)}
|
||||
}
|
||||
subs := c.SubCmdList()
|
||||
return suggest("", prefix, func(prefix string) []string {
|
||||
var options []string
|
||||
for _, sub := range subs {
|
||||
if strings.HasPrefix(sub, prefix) {
|
||||
options = append(options, sub)
|
||||
}
|
||||
}
|
||||
return options
|
||||
})
|
||||
}
|
||||
|
||||
func (c completer) suggestLeafCommandOptions() (options []string) {
|
||||
arg, before := arg.Arg{}, arg.Arg{}
|
||||
if len(c.args) > 0 {
|
||||
arg = c.args[len(c.args)-1]
|
||||
}
|
||||
if len(c.args) > 1 {
|
||||
before = c.args[len(c.args)-2]
|
||||
}
|
||||
|
||||
if !arg.Completed {
|
||||
// Complete value being typed.
|
||||
if arg.HasValue {
|
||||
// Complete value of current flag.
|
||||
if arg.HasFlag {
|
||||
return c.suggestFlagValue(arg.Flag, arg.Value)
|
||||
}
|
||||
// Complete value of flag in a previous argument.
|
||||
if before.HasFlag && !before.HasValue {
|
||||
return c.suggestFlagValue(before.Flag, arg.Value)
|
||||
}
|
||||
}
|
||||
|
||||
// A value with no flag. Suggest positional argument.
|
||||
if !arg.HasValue {
|
||||
options = c.suggestFlag(arg.Dashes, arg.Flag)
|
||||
}
|
||||
if !arg.HasFlag {
|
||||
options = append(options, c.suggestArgsValue(arg.Value)...)
|
||||
}
|
||||
// Suggest flag according to prefix.
|
||||
return options
|
||||
}
|
||||
|
||||
// Has a value that was already completed. Suggest all flags and positional arguments.
|
||||
if arg.HasValue {
|
||||
options = c.suggestFlag(arg.Dashes, "")
|
||||
if !arg.HasFlag {
|
||||
options = append(options, c.suggestArgsValue("")...)
|
||||
}
|
||||
return options
|
||||
}
|
||||
// A flag without a value. Suggest a value or suggest any flag.
|
||||
options = c.suggestFlagValue(arg.Flag, "")
|
||||
if len(options) > 0 {
|
||||
return options
|
||||
}
|
||||
return c.suggestFlag("", "")
|
||||
}
|
||||
|
||||
func (c completer) suggestFlag(dashes, prefix string) []string {
|
||||
if dashes == "" {
|
||||
dashes = "-"
|
||||
}
|
||||
return suggest(dashes, prefix, func(prefix string) []string {
|
||||
var options []string
|
||||
c.iterateStack(func(cmd Completer) {
|
||||
// Suggest all flags with the given prefix.
|
||||
for _, name := range cmd.FlagList() {
|
||||
if strings.HasPrefix(name, prefix) {
|
||||
options = append(options, dashes+name)
|
||||
}
|
||||
}
|
||||
})
|
||||
return options
|
||||
})
|
||||
}
|
||||
|
||||
func (c completer) suggestFlagValue(flagName, prefix string) []string {
|
||||
var options []string
|
||||
c.iterateStack(func(cmd Completer) {
|
||||
if len(options) == 0 {
|
||||
if p := cmd.FlagGet(flagName); p != nil {
|
||||
options = p.Predict(prefix)
|
||||
}
|
||||
}
|
||||
})
|
||||
return filterByPrefix(prefix, options...)
|
||||
}
|
||||
|
||||
func (c completer) suggestArgsValue(prefix string) []string {
|
||||
var options []string
|
||||
c.iterateStack(func(cmd Completer) {
|
||||
if len(options) == 0 {
|
||||
if p := cmd.ArgsGet(); p != nil {
|
||||
options = p.Predict(prefix)
|
||||
}
|
||||
}
|
||||
})
|
||||
return filterByPrefix(prefix, options...)
|
||||
}
|
||||
|
||||
func (c completer) iterateStack(f func(Completer)) {
|
||||
for _, cmd := range append([]Completer{c.Completer}, c.stack...) {
|
||||
f(cmd)
|
||||
}
|
||||
}
|
||||
|
||||
func suggest(dashes, prefix string, collect func(prefix string) []string) []string {
|
||||
options := collect(prefix)
|
||||
// If nothing was suggested, suggest all flags.
|
||||
if len(options) == 0 {
|
||||
prefix = ""
|
||||
options = collect(prefix)
|
||||
}
|
||||
|
||||
// Add help flag if needed.
|
||||
help := helpFlag(dashes + prefix)
|
||||
if len(options) == 0 || strings.HasPrefix(help, dashes+prefix) {
|
||||
options = append(options, help)
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
func filterByPrefix(prefix string, options ...string) []string {
|
||||
var filtered []string
|
||||
for _, option := range options {
|
||||
if fixed, ok := hasPrefix(option, prefix); ok {
|
||||
filtered = append(filtered, fixed)
|
||||
}
|
||||
}
|
||||
if len(filtered) > 0 {
|
||||
return filtered
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
// hasPrefix checks if s has the give prefix. It disregards quotes and escaped spaces, and return
|
||||
// s in the form of the given prefix.
|
||||
func hasPrefix(s, prefix string) (string, bool) {
|
||||
var (
|
||||
token tokener.Tokener
|
||||
si, pi int
|
||||
)
|
||||
for ; pi < len(prefix); pi++ {
|
||||
token.Visit(prefix[pi])
|
||||
lastQuote := !token.Escaped() && (prefix[pi] == '"' || prefix[pi] == '\'')
|
||||
if lastQuote {
|
||||
continue
|
||||
}
|
||||
if si == len(s) {
|
||||
break
|
||||
}
|
||||
if s[si] == ' ' && !token.Quoted() && token.Escaped() {
|
||||
s = s[:si] + "\\" + s[si:]
|
||||
}
|
||||
if s[si] != prefix[pi] {
|
||||
return "", false
|
||||
}
|
||||
si++
|
||||
}
|
||||
|
||||
if pi < len(prefix) {
|
||||
return "", false
|
||||
}
|
||||
|
||||
for ; si < len(s); si++ {
|
||||
token.Visit(s[si])
|
||||
}
|
||||
|
||||
return token.Closed(), true
|
||||
}
|
||||
|
||||
// helpFlag returns either "-h", "-help" or "--help".
|
||||
func helpFlag(prefix string) string {
|
||||
if prefix == "" || prefix == "-" || prefix == "-h" {
|
||||
return "-h"
|
||||
}
|
||||
if strings.HasPrefix(prefix, "--") {
|
||||
return "--help"
|
||||
}
|
||||
return "-help"
|
||||
}
|
||||
|
|
596
complete_test.go
596
complete_test.go
|
@ -1,414 +1,246 @@
|
|||
package complete
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/posener/complete/internal/arg"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestCompleter_Complete(t *testing.T) {
|
||||
initTests()
|
||||
var testCmd = &Command{
|
||||
Flags: map[string]Predictor{"cmd-flag": nil},
|
||||
Sub: map[string]*Command{
|
||||
"flags": &Command{
|
||||
Flags: map[string]Predictor{
|
||||
"values": set{"a", "a a", "b"},
|
||||
"something": set{""},
|
||||
"nothing": nil,
|
||||
},
|
||||
},
|
||||
"sub1": &Command{
|
||||
Flags: map[string]Predictor{"flag1": nil},
|
||||
Sub: map[string]*Command{
|
||||
"sub11": &Command{
|
||||
Flags: map[string]Predictor{"flag11": nil},
|
||||
},
|
||||
"sub12": &Command{},
|
||||
},
|
||||
Args: set{"arg1", "arg2"},
|
||||
},
|
||||
"sub2": &Command{},
|
||||
"args": &Command{
|
||||
Args: set{"a", "a a", "b"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
c := Command{
|
||||
Sub: Commands{
|
||||
"sub1": {
|
||||
Flags: Flags{
|
||||
"-flag1": PredictAnything,
|
||||
"-flag2": PredictNothing,
|
||||
},
|
||||
Sub: Commands{
|
||||
"sub11": {},
|
||||
},
|
||||
},
|
||||
"sub2": {
|
||||
Flags: Flags{
|
||||
"-flag2": PredictNothing,
|
||||
"-flag3": PredictSet("opt1", "opt2", "opt12"),
|
||||
},
|
||||
Args: PredictFiles("*.md"),
|
||||
},
|
||||
"sub3": {
|
||||
Sub: Commands{
|
||||
"sub3": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
Flags: Flags{
|
||||
"-o": PredictFiles("*.txt"),
|
||||
},
|
||||
GlobalFlags: Flags{
|
||||
"-h": PredictNothing,
|
||||
"-global1": PredictAnything,
|
||||
},
|
||||
}
|
||||
cmp := New("cmd", c)
|
||||
func TestCompleter(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
line string
|
||||
point int // -1 indicates len(line)
|
||||
want []string
|
||||
args string
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
line: "cmd ",
|
||||
point: -1,
|
||||
want: []string{"sub1", "sub2", "sub3"},
|
||||
},
|
||||
{
|
||||
line: "cmd -",
|
||||
point: -1,
|
||||
want: []string{"-h", "-global1", "-o"},
|
||||
},
|
||||
{
|
||||
line: "cmd -h ",
|
||||
point: -1,
|
||||
want: []string{"sub1", "sub2", "sub3"},
|
||||
},
|
||||
{
|
||||
line: "cmd -global1 ", // global1 is known follow flag
|
||||
point: -1,
|
||||
want: []string{},
|
||||
},
|
||||
{
|
||||
line: "cmd sub",
|
||||
point: -1,
|
||||
want: []string{"sub1", "sub2", "sub3"},
|
||||
},
|
||||
{
|
||||
line: "cmd sub1",
|
||||
point: -1,
|
||||
want: []string{"sub1"},
|
||||
},
|
||||
{
|
||||
line: "cmd sub2",
|
||||
point: -1,
|
||||
want: []string{"sub2"},
|
||||
},
|
||||
{
|
||||
line: "cmd sub1 ",
|
||||
point: -1,
|
||||
want: []string{"sub11"},
|
||||
},
|
||||
{
|
||||
line: "cmd sub3 ",
|
||||
point: -1,
|
||||
want: []string{"sub3"},
|
||||
},
|
||||
{
|
||||
line: "cmd sub1 -",
|
||||
point: -1,
|
||||
want: []string{"-flag1", "-flag2", "-h", "-global1"},
|
||||
},
|
||||
{
|
||||
line: "cmd sub2 ",
|
||||
point: -1,
|
||||
want: []string{"./", "dir/", "outer/", "readme.md"},
|
||||
},
|
||||
{
|
||||
line: "cmd sub2 ./",
|
||||
point: -1,
|
||||
want: []string{"./", "./readme.md", "./dir/", "./outer/"},
|
||||
},
|
||||
{
|
||||
line: "cmd sub2 re",
|
||||
point: -1,
|
||||
want: []string{"readme.md"},
|
||||
},
|
||||
{
|
||||
line: "cmd sub2 ./re",
|
||||
point: -1,
|
||||
want: []string{"./readme.md"},
|
||||
},
|
||||
{
|
||||
line: "cmd sub2 -flag2 ",
|
||||
point: -1,
|
||||
want: []string{"./", "dir/", "outer/", "readme.md"},
|
||||
},
|
||||
{
|
||||
line: "cmd sub1 -fl",
|
||||
point: -1,
|
||||
want: []string{"-flag1", "-flag2"},
|
||||
},
|
||||
{
|
||||
line: "cmd sub1 -flag1",
|
||||
point: -1,
|
||||
want: []string{"-flag1"},
|
||||
},
|
||||
{
|
||||
line: "cmd sub1 -flag1 ",
|
||||
point: -1,
|
||||
want: []string{}, // flag1 is unknown follow flag
|
||||
},
|
||||
{
|
||||
line: "cmd sub1 -flag2 -",
|
||||
point: -1,
|
||||
want: []string{"-flag1", "-flag2", "-h", "-global1"},
|
||||
},
|
||||
{
|
||||
line: "cmd -no-such-flag",
|
||||
point: -1,
|
||||
want: []string{},
|
||||
},
|
||||
{
|
||||
line: "cmd -no-such-flag ",
|
||||
point: -1,
|
||||
want: []string{"sub1", "sub2", "sub3"},
|
||||
},
|
||||
{
|
||||
line: "cmd -no-such-flag -",
|
||||
point: -1,
|
||||
want: []string{"-h", "-global1", "-o"},
|
||||
},
|
||||
{
|
||||
line: "cmd no-such-command",
|
||||
point: -1,
|
||||
want: []string{},
|
||||
},
|
||||
{
|
||||
line: "cmd no-such-command ",
|
||||
point: -1,
|
||||
want: []string{"sub1", "sub2", "sub3"},
|
||||
},
|
||||
{
|
||||
line: "cmd -o ",
|
||||
point: -1,
|
||||
want: []string{"a.txt", "b.txt", "c.txt", ".dot.txt", "./", "dir/", "outer/"},
|
||||
},
|
||||
{
|
||||
line: "cmd -o ./no-su",
|
||||
point: -1,
|
||||
want: []string{},
|
||||
},
|
||||
{
|
||||
line: "cmd -o ./",
|
||||
point: -1,
|
||||
want: []string{"./a.txt", "./b.txt", "./c.txt", "./.dot.txt", "./", "./dir/", "./outer/"},
|
||||
},
|
||||
{
|
||||
line: "cmd -o=./",
|
||||
point: -1,
|
||||
want: []string{"./a.txt", "./b.txt", "./c.txt", "./.dot.txt", "./", "./dir/", "./outer/"},
|
||||
},
|
||||
{
|
||||
line: "cmd -o .",
|
||||
point: -1,
|
||||
want: []string{"./a.txt", "./b.txt", "./c.txt", "./.dot.txt", "./", "./dir/", "./outer/"},
|
||||
},
|
||||
{
|
||||
line: "cmd -o ./b",
|
||||
point: -1,
|
||||
want: []string{"./b.txt"},
|
||||
},
|
||||
{
|
||||
line: "cmd -o=./b",
|
||||
point: -1,
|
||||
want: []string{"./b.txt"},
|
||||
},
|
||||
{
|
||||
line: "cmd -o ./read",
|
||||
point: -1,
|
||||
want: []string{},
|
||||
},
|
||||
{
|
||||
line: "cmd -o=./read",
|
||||
point: -1,
|
||||
want: []string{},
|
||||
},
|
||||
{
|
||||
line: "cmd -o ./readme.md",
|
||||
point: -1,
|
||||
want: []string{},
|
||||
},
|
||||
{
|
||||
line: "cmd -o ./readme.md ",
|
||||
point: -1,
|
||||
want: []string{"sub1", "sub2", "sub3"},
|
||||
},
|
||||
{
|
||||
line: "cmd -o=./readme.md ",
|
||||
point: -1,
|
||||
want: []string{"sub1", "sub2", "sub3"},
|
||||
},
|
||||
{
|
||||
line: "cmd -o sub2 -flag3 ",
|
||||
point: -1,
|
||||
want: []string{"opt1", "opt2", "opt12"},
|
||||
},
|
||||
{
|
||||
line: "cmd -o sub2 -flag3 opt1",
|
||||
point: -1,
|
||||
want: []string{"opt1", "opt12"},
|
||||
},
|
||||
{
|
||||
line: "cmd -o sub2 -flag3 opt",
|
||||
point: -1,
|
||||
want: []string{"opt1", "opt2", "opt12"},
|
||||
},
|
||||
{
|
||||
line: "cmd -o ./b foo",
|
||||
// ^
|
||||
point: 10,
|
||||
want: []string{"./b.txt"},
|
||||
},
|
||||
{
|
||||
line: "cmd -o=./b foo",
|
||||
// ^
|
||||
point: 10,
|
||||
want: []string{"./b.txt"},
|
||||
},
|
||||
{
|
||||
line: "cmd -o sub2 -flag3 optfoo",
|
||||
// ^
|
||||
point: 22,
|
||||
want: []string{"opt1", "opt2", "opt12"},
|
||||
},
|
||||
{
|
||||
line: "cmd -o ",
|
||||
// ^
|
||||
point: 4,
|
||||
want: []string{"sub1", "sub2", "sub3"},
|
||||
},
|
||||
// Check empty flag name matching.
|
||||
|
||||
{args: "flags ", want: []string{"-values", "-nothing", "-something", "-cmd-flag", "-h"}},
|
||||
{args: "flags -", want: []string{"-values", "-nothing", "-something", "-cmd-flag", "-h"}},
|
||||
{args: "flags --", want: []string{"--values", "--nothing", "--something", "--cmd-flag", "--help"}},
|
||||
// If started a flag with no matching prefix, expect to see all possible flags.
|
||||
{args: "flags -x", want: []string{"-values", "-nothing", "-something", "-cmd-flag", "-h"}},
|
||||
// Check prefix matching for chain of sub commands.
|
||||
{args: "sub1 sub11 -fl", want: []string{"-flag11", "-flag1"}},
|
||||
{args: "sub1 sub11 --fl", want: []string{"--flag11", "--flag1"}},
|
||||
|
||||
// Test sub command completion.
|
||||
|
||||
{args: "", want: []string{"flags", "sub1", "sub2", "args", "-h"}},
|
||||
{args: " ", want: []string{"flags", "sub1", "sub2", "args", "-h"}},
|
||||
{args: "f", want: []string{"flags"}},
|
||||
{args: "sub", want: []string{"sub1", "sub2"}},
|
||||
{args: "sub1", want: []string{"sub1"}},
|
||||
{args: "sub1 ", want: []string{"sub11", "sub12", "-h"}},
|
||||
// Suggest all sub commands if prefix is not known.
|
||||
{args: "x", want: []string{"flags", "sub1", "sub2", "args", "-h"}},
|
||||
|
||||
// Suggest flag value.
|
||||
|
||||
// A flag that has an empty completion should return empty completion. It "completes
|
||||
// something"... But it doesn't know what, so we should not complete anything else.
|
||||
{args: "flags -something ", want: []string{""}},
|
||||
{args: "flags -something foo", want: []string{""}},
|
||||
// A flag that have nil completion should complete all other options.
|
||||
{args: "flags -nothing ", want: []string{"-values", "-nothing", "-something", "-cmd-flag", "-h"}},
|
||||
// Trying to provide a value to the nothing flag should revert the phrase back to nothing.
|
||||
{args: "flags -nothing=", want: []string{}},
|
||||
// The flag value was not started, suggest all relevant values.
|
||||
{args: "flags -values ", want: []string{"a", "a\\ a", "b"}},
|
||||
{args: "flags -values a", want: []string{"a", "a\\ a"}},
|
||||
{args: "flags -values a\\", want: []string{"a\\ a"}},
|
||||
{args: "flags -values a\\ ", want: []string{"a\\ a"}},
|
||||
{args: "flags -values a\\ a", want: []string{"a\\ a"}},
|
||||
{args: "flags -values a\\ a ", want: []string{"-values", "-nothing", "-something", "-cmd-flag", "-h"}},
|
||||
{args: "flags -values \"a", want: []string{"\"a\"", "\"a a\""}},
|
||||
{args: "flags -values \"a ", want: []string{"\"a a\""}},
|
||||
{args: "flags -values \"a a", want: []string{"\"a a\""}},
|
||||
{args: "flags -values \"a a\"", want: []string{"\"a a\""}},
|
||||
{args: "flags -values \"a a\" ", want: []string{"-values", "-nothing", "-something", "-cmd-flag", "-h"}},
|
||||
|
||||
{args: "flags -values=", want: []string{"a", "a\\ a", "b"}},
|
||||
{args: "flags -values=a", want: []string{"a", "a\\ a"}},
|
||||
{args: "flags -values=a\\", want: []string{"a\\ a"}},
|
||||
{args: "flags -values=a\\ ", want: []string{"a\\ a"}},
|
||||
{args: "flags -values=a\\ a", want: []string{"a\\ a"}},
|
||||
{args: "flags -values=a\\ a ", want: []string{"-values", "-nothing", "-something", "-cmd-flag", "-h"}},
|
||||
{args: "flags -values=\"a", want: []string{"\"a\"", "\"a a\""}},
|
||||
{args: "flags -values=\"a ", want: []string{"\"a a\""}},
|
||||
{args: "flags -values=\"a a", want: []string{"\"a a\""}},
|
||||
{args: "flags -values=\"a a\"", want: []string{"\"a a\""}},
|
||||
{args: "flags -values=\"a a\" ", want: []string{"-values", "-nothing", "-something", "-cmd-flag", "-h"}},
|
||||
|
||||
// Complete positional arguments
|
||||
|
||||
{args: "args ", want: []string{"-cmd-flag", "-h", "a", "a\\ a", "b"}},
|
||||
{args: "args a", want: []string{"a", "a\\ a"}},
|
||||
{args: "args a\\", want: []string{"a\\ a"}},
|
||||
{args: "args a\\ ", want: []string{"a\\ a"}},
|
||||
{args: "args a\\ a", want: []string{"a\\ a"}},
|
||||
{args: "args a\\ a ", want: []string{"-cmd-flag", "-h", "a", "a\\ a", "b"}},
|
||||
{args: "args \"a", want: []string{"\"a\"", "\"a a\""}},
|
||||
{args: "args \"a ", want: []string{"\"a a\""}},
|
||||
{args: "args \"a a", want: []string{"\"a a\""}},
|
||||
{args: "args \"a a\"", want: []string{"\"a a\""}},
|
||||
{args: "args \"a a\" ", want: []string{"-cmd-flag", "-h", "a", "a\\ a", "b"}},
|
||||
|
||||
// Complete positional arguments from a parent command
|
||||
{args: "sub1 sub12 arg", want: []string{"arg1", "arg2"}},
|
||||
|
||||
// Test help
|
||||
|
||||
{args: "-", want: []string{"-h"}},
|
||||
{args: " -", want: []string{"-h"}},
|
||||
{args: "--", want: []string{"--help"}},
|
||||
{args: "-he", want: []string{"-help"}},
|
||||
{args: "-x", want: []string{"-help"}},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(fmt.Sprintf("%s@%d", tt.line, tt.point), func(t *testing.T) {
|
||||
got := runComplete(cmp, tt.line, tt.point)
|
||||
t.Run(tt.args, func(t *testing.T) {
|
||||
Test(t, testCmd, tt.args, tt.want)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
sort.Strings(tt.want)
|
||||
sort.Strings(got)
|
||||
func TestCompleter_error(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if !equalSlices(got, tt.want) {
|
||||
t.Errorf("failed '%s'\ngot: %s\nwant: %s", t.Name(), got, tt.want)
|
||||
tests := []struct {
|
||||
args string
|
||||
err string
|
||||
}{
|
||||
// Sub command already fully typed but unknown.
|
||||
{args: "x ", err: "unknown subcommand: x"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.args, func(t *testing.T) {
|
||||
_, err := completer{Completer: testCmd, args: arg.Parse(tt.args)}.complete()
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, tt.err, err.Error())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestComplete(t *testing.T) {
|
||||
defer func() {
|
||||
getEnv = os.Getenv
|
||||
exit = os.Exit
|
||||
out = os.Stdout
|
||||
}()
|
||||
|
||||
tests := []struct {
|
||||
line, point string
|
||||
shouldExit bool
|
||||
shouldPanic bool
|
||||
install string
|
||||
uninstall string
|
||||
}{
|
||||
{shouldExit: true, line: "cmd", point: "1"},
|
||||
{shouldExit: false, line: "", point: ""},
|
||||
{shouldPanic: true, line: "cmd", point: ""},
|
||||
{shouldPanic: true, line: "cmd", point: "a"},
|
||||
{shouldPanic: true, line: "cmd", point: "4"},
|
||||
|
||||
{shouldExit: true, install: "1"},
|
||||
{shouldExit: false, install: "a"},
|
||||
{shouldExit: true, uninstall: "1"},
|
||||
{shouldExit: false, uninstall: "a"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.line+"@"+tt.point, func(t *testing.T) {
|
||||
getEnv = func(env string) string {
|
||||
switch env {
|
||||
case "COMP_LINE":
|
||||
return tt.line
|
||||
case "COMP_POINT":
|
||||
return tt.point
|
||||
case "COMP_INSTALL":
|
||||
return tt.install
|
||||
case "COMP_UNINSTALL":
|
||||
return tt.uninstall
|
||||
case "COMP_YES":
|
||||
return "0"
|
||||
default:
|
||||
panic(env)
|
||||
}
|
||||
}
|
||||
isExit := false
|
||||
exit = func(int) {
|
||||
isExit = true
|
||||
}
|
||||
out = ioutil.Discard
|
||||
if tt.shouldPanic {
|
||||
assert.Panics(t, func() { testCmd.Complete("") })
|
||||
} else {
|
||||
testCmd.Complete("")
|
||||
assert.Equal(t, tt.shouldExit, isExit)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompleter_Complete_SharedPrefix(t *testing.T) {
|
||||
initTests()
|
||||
type set []string
|
||||
|
||||
c := Command{
|
||||
Sub: Commands{
|
||||
"status": {
|
||||
Flags: Flags{
|
||||
"-f3": PredictNothing,
|
||||
},
|
||||
},
|
||||
"job": {
|
||||
Sub: Commands{
|
||||
"status": {
|
||||
Flags: Flags{
|
||||
"-f4": PredictNothing,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Flags: Flags{
|
||||
"-o": PredictFiles("*.txt"),
|
||||
},
|
||||
GlobalFlags: Flags{
|
||||
"-h": PredictNothing,
|
||||
"-global1": PredictAnything,
|
||||
},
|
||||
}
|
||||
func (s set) Predict(_ string) []string {
|
||||
return s
|
||||
}
|
||||
|
||||
cmp := New("cmd", c)
|
||||
func TestHasPrefix(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
line string
|
||||
point int // -1 indicates len(line)
|
||||
want []string
|
||||
s string
|
||||
prefix string
|
||||
want string
|
||||
wantOK bool
|
||||
}{
|
||||
{
|
||||
line: "cmd ",
|
||||
point: -1,
|
||||
want: []string{"status", "job"},
|
||||
},
|
||||
{
|
||||
line: "cmd -",
|
||||
point: -1,
|
||||
want: []string{"-h", "-global1", "-o"},
|
||||
},
|
||||
{
|
||||
line: "cmd j",
|
||||
point: -1,
|
||||
want: []string{"job"},
|
||||
},
|
||||
{
|
||||
line: "cmd job ",
|
||||
point: -1,
|
||||
want: []string{"status"},
|
||||
},
|
||||
{
|
||||
line: "cmd job -",
|
||||
point: -1,
|
||||
want: []string{"-h", "-global1"},
|
||||
},
|
||||
{
|
||||
line: "cmd job status ",
|
||||
point: -1,
|
||||
want: []string{},
|
||||
},
|
||||
{
|
||||
line: "cmd job status -",
|
||||
point: -1,
|
||||
want: []string{"-f4", "-h", "-global1"},
|
||||
},
|
||||
{s: "ab", prefix: `b`, want: ``, wantOK: false},
|
||||
{s: "", prefix: `b`, want: ``, wantOK: false},
|
||||
{s: "ab", prefix: `a`, want: `ab`, wantOK: true},
|
||||
{s: "ab", prefix: `"'b`, want: ``, wantOK: false},
|
||||
{s: "ab", prefix: `"'a`, want: `"'ab'"`, wantOK: true},
|
||||
{s: "ab", prefix: `'"a`, want: `'"ab"'`, wantOK: true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.line, func(t *testing.T) {
|
||||
got := runComplete(cmp, tt.line, tt.point)
|
||||
|
||||
sort.Strings(tt.want)
|
||||
sort.Strings(got)
|
||||
|
||||
if !equalSlices(got, tt.want) {
|
||||
t.Errorf("failed '%s'\ngot = %s\nwant: %s", t.Name(), got, tt.want)
|
||||
}
|
||||
t.Run(tt.s+"/"+tt.prefix, func(t *testing.T) {
|
||||
got, gotOK := hasPrefix(tt.s, tt.prefix)
|
||||
assert.Equal(t, tt.want, got)
|
||||
assert.Equal(t, tt.wantOK, gotOK)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// runComplete runs the complete login for test purposes
|
||||
// it gets the complete struct and command line arguments and returns
|
||||
// the complete options
|
||||
func runComplete(c *Complete, line string, point int) (completions []string) {
|
||||
if point == -1 {
|
||||
point = len(line)
|
||||
}
|
||||
os.Setenv(envLine, line)
|
||||
os.Setenv(envPoint, strconv.Itoa(point))
|
||||
b := bytes.NewBuffer(nil)
|
||||
c.Out = b
|
||||
c.Complete()
|
||||
completions = parseOutput(b.String())
|
||||
return
|
||||
}
|
||||
|
||||
func parseOutput(output string) []string {
|
||||
lines := strings.Split(output, "\n")
|
||||
options := []string{}
|
||||
for _, l := range lines {
|
||||
if l != "" {
|
||||
options = append(options, l)
|
||||
}
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
func equalSlices(a, b []string) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for i := range a {
|
||||
if a[i] != b[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
|
164
doc.go
164
doc.go
|
@ -1,35 +1,46 @@
|
|||
/*
|
||||
Package complete provides a tool for bash writing bash completion in go, and bash completion for the go command line.
|
||||
Package complete is everything for bash completion and Go.
|
||||
|
||||
Writing bash completion scripts is a hard work. This package provides an easy way
|
||||
to create bash completion scripts for any command, and also an easy way to install/uninstall
|
||||
the completion of the command.
|
||||
Writing bash completion scripts is a hard work, usually done in the bash scripting language.
|
||||
This package provides:
|
||||
|
||||
* A library for bash completion for Go programs.
|
||||
|
||||
* A tool for writing bash completion script in the Go language. For any Go or non Go program.
|
||||
|
||||
* Bash completion for the `go` command line (See ./gocomplete).
|
||||
|
||||
* Library for bash-completion enabled flags (See ./compflag).
|
||||
|
||||
* Enables an easy way to install/uninstall the completion of the command.
|
||||
|
||||
The library and tools are extensible such that any program can add its one logic, completion types
|
||||
or methologies.
|
||||
|
||||
Go Command Bash Completion
|
||||
|
||||
In ./cmd/gocomplete there is an example for bash completion for the `go` command line.
|
||||
./gocomplete is the script for bash completion for the `go` command line. This is an example
|
||||
that uses the `complete` package on the `go` command - the `complete` package can also be used to
|
||||
implement any completions, see #usage.
|
||||
|
||||
This is an example that uses the `complete` package on the `go` command - the `complete` package
|
||||
can also be used to implement any completions, see #usage.
|
||||
|
||||
Install
|
||||
Install:
|
||||
|
||||
1. Type in your shell:
|
||||
|
||||
go get -u github.com/posener/complete/gocomplete
|
||||
gocomplete -install
|
||||
COMP_INSTALL=1 gocomplete
|
||||
|
||||
2. Restart your shell
|
||||
|
||||
Uninstall by `gocomplete -uninstall`
|
||||
Uninstall by `COMP_UNINSTALL=1 gocomplete`
|
||||
|
||||
Features
|
||||
Features:
|
||||
|
||||
- Complete `go` command, including sub commands and all flags.
|
||||
- Complete `go` command, including sub commands and flags.
|
||||
- Complete packages names or `.go` files when necessary.
|
||||
- Complete test names after `-run` flag.
|
||||
|
||||
Complete package
|
||||
Complete Package
|
||||
|
||||
Supported shells:
|
||||
|
||||
|
@ -39,72 +50,83 @@ Supported shells:
|
|||
|
||||
Usage
|
||||
|
||||
Assuming you have program called `run` and you want to have bash completion
|
||||
for it, meaning, if you type `run` then space, then press the `Tab` key,
|
||||
the shell will suggest relevant complete options.
|
||||
Add bash completion capabilities to any Go program. See ./example/command.
|
||||
|
||||
In that case, we will create a program called `runcomplete`, a go program,
|
||||
with a `func main()` and so, that will make the completion of the `run`
|
||||
program. Once the `runcomplete` will be in a binary form, we could
|
||||
`runcomplete -install` and that will add to our shell all the bash completion
|
||||
options for `run`.
|
||||
import (
|
||||
"flag"
|
||||
"github.com/posener/complete"
|
||||
"github.com/posener/complete/predict"
|
||||
)
|
||||
|
||||
So here it is:
|
||||
|
||||
import "github.com/posener/complete"
|
||||
var (
|
||||
// Add variables to the program.
|
||||
name = flag.String("name", "", "")
|
||||
something = flag.String("something", "", "")
|
||||
nothing = flag.String("nothing", "", "")
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
// create a Command object, that represents the command we want
|
||||
// to complete.
|
||||
run := complete.Command{
|
||||
|
||||
// Sub defines a list of sub commands of the program,
|
||||
// this is recursive, since every command is of type command also.
|
||||
Sub: complete.Commands{
|
||||
|
||||
// add a build sub command
|
||||
"build": complete.Command {
|
||||
|
||||
// define flags of the build sub command
|
||||
Flags: complete.Flags{
|
||||
// build sub command has a flag '-cpus', which
|
||||
// expects number of cpus after it. in that case
|
||||
// anything could complete this flag.
|
||||
"-cpus": complete.PredictAnything,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// define flags of the 'run' main command
|
||||
Flags: complete.Flags{
|
||||
// a flag -o, which expects a file ending with .out after
|
||||
// it, the tab completion will auto complete for files matching
|
||||
// the given pattern.
|
||||
"-o": complete.PredictFiles("*.out"),
|
||||
},
|
||||
|
||||
// define global flags of the 'run' main command
|
||||
// those will show up also when a sub command was entered in the
|
||||
// command line
|
||||
GlobalFlags: complete.Flags{
|
||||
|
||||
// a flag '-h' which does not expects anything after it
|
||||
"-h": complete.PredictNothing,
|
||||
// Create the complete command.
|
||||
// Here we define completion values for each flag.
|
||||
cmd := &complete.Command{
|
||||
Flags: map[string]complete.Predictor{
|
||||
"name": predict.Set{"foo", "bar", "foo bar"},
|
||||
"something": predict.Something,
|
||||
"nothing": predict.Nothing,
|
||||
},
|
||||
}
|
||||
|
||||
// run the command completion, as part of the main() function.
|
||||
// this triggers the autocompletion when needed.
|
||||
// name must be exactly as the binary that we want to complete.
|
||||
complete.New("run", run).Run()
|
||||
// Run the completion - provide it with the binary name.
|
||||
cmd.Complete("my-program")
|
||||
// Parse the flags.
|
||||
flag.Parse()
|
||||
// Program logic...
|
||||
}
|
||||
|
||||
Self completing program
|
||||
This package also enables to complete flags defined by the standard library `flag` package.
|
||||
To use this feature, simply call `complete.CommandLine` before `flag.Parse`. (See ./example/stdlib).
|
||||
|
||||
In case that the program that we want to complete is written in go we
|
||||
can make it self completing.
|
||||
Here is an example: ./example/self/main.go .
|
||||
import (
|
||||
"flag"
|
||||
+ "github.com/posener/complete"
|
||||
)
|
||||
var (
|
||||
// Define flags here...
|
||||
foo = flag.Bool("foo", false, "")
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Call command line completion before parsing the flags - provide it with the binary name.
|
||||
+ complete.CommandLine("my-program")
|
||||
flag.Parse()
|
||||
}
|
||||
|
||||
If flag value completion is desired, it can be done by providing the standard library `flag.Var`
|
||||
function a `flag.Value` that also implements the `complete.Predictor` interface. For standard
|
||||
flag with values, it is possible to use the `github.com/posener/complete/compflag` package.
|
||||
(See ./example/compflag).
|
||||
|
||||
import (
|
||||
"flag"
|
||||
+ "github.com/posener/complete"
|
||||
+ "github.com/posener/complete/compflag"
|
||||
)
|
||||
var (
|
||||
// Define flags here...
|
||||
- foo = flag.Bool("foo", false, "")
|
||||
+ foo = compflag.Bool("foo", false, "")
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Call command line completion before parsing the flags.
|
||||
+ complete.CommandLine("my-program")
|
||||
flag.Parse()
|
||||
}
|
||||
|
||||
Instead of calling both `complete.CommandLine` and `flag.Parse`, one can call just `compflag.Parse`
|
||||
which does them both.
|
||||
|
||||
Testing
|
||||
|
||||
For command line bash completion testing use the `complete.Test` function.
|
||||
*/
|
||||
package complete
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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 }
|
|
@ -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
|
@ -4,16 +4,15 @@ import (
|
|||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"log"
|
||||
"regexp"
|
||||
|
||||
"github.com/posener/complete"
|
||||
)
|
||||
|
||||
func functionsInFile(path string, regexp *regexp.Regexp) (tests []string) {
|
||||
fset := token.NewFileSet()
|
||||
f, err := parser.ParseFile(fset, path, nil, 0)
|
||||
if err != nil {
|
||||
complete.Log("Failed parsing %s: %s", path, err)
|
||||
log.Printf("Failed parsing %s: %s", path, err)
|
||||
return nil
|
||||
}
|
||||
for _, d := range f.Decls {
|
||||
|
|
|
@ -3,18 +3,19 @@ package main
|
|||
import (
|
||||
"go/build"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/posener/complete"
|
||||
"github.com/posener/complete/predict"
|
||||
)
|
||||
|
||||
// predictPackages completes packages in the directory pointed by a.Last
|
||||
// and packages that are one level below that package.
|
||||
func predictPackages(a complete.Args) (prediction []string) {
|
||||
prediction = []string{a.Last}
|
||||
func predictPackages(prefix string) (prediction []string) {
|
||||
prediction = []string{prefix}
|
||||
lastPrediction := ""
|
||||
for len(prediction) == 1 && (lastPrediction == "" || lastPrediction != prediction[0]) {
|
||||
// if only one prediction, predict files within this prediction,
|
||||
|
@ -23,19 +24,19 @@ func predictPackages(a complete.Args) (prediction []string) {
|
|||
// level deeper and give the user the 'pkg' and all the nested packages within
|
||||
// that package.
|
||||
lastPrediction = prediction[0]
|
||||
a.Last = prediction[0]
|
||||
prediction = predictLocalAndSystem(a)
|
||||
prefix = prediction[0]
|
||||
prediction = predictLocalAndSystem(prefix)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func predictLocalAndSystem(a complete.Args) []string {
|
||||
localDirs := complete.PredictFilesSet(listPackages(a.Directory())).Predict(a)
|
||||
func predictLocalAndSystem(prefix string) []string {
|
||||
localDirs := predict.FilesSet(listPackages(directory(prefix))).Predict(prefix)
|
||||
// System directories are not actual file names, for example: 'github.com/posener/complete' could
|
||||
// be the argument, but the actual filename is in $GOPATH/src/github.com/posener/complete'. this
|
||||
// is the reason to use the PredictSet and not the PredictDirs in this case.
|
||||
s := systemDirs(a.Last)
|
||||
sysDirs := complete.PredictSet(s...).Predict(a)
|
||||
s := systemDirs(prefix)
|
||||
sysDirs := predict.Set(s).Predict(prefix)
|
||||
return append(localDirs, sysDirs...)
|
||||
}
|
||||
|
||||
|
@ -45,7 +46,7 @@ func listPackages(dir string) (directories []string) {
|
|||
// add subdirectories
|
||||
files, err := ioutil.ReadDir(dir)
|
||||
if err != nil {
|
||||
complete.Log("failed reading directory %s: %s", dir, err)
|
||||
log.Printf("failed reading directory %s: %s", dir, err)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -62,7 +63,7 @@ func listPackages(dir string) (directories []string) {
|
|||
for _, p := range paths {
|
||||
pkg, err := build.ImportDir(p, 0)
|
||||
if err != nil {
|
||||
complete.Log("failed importing directory %s: %s", p, err)
|
||||
log.Printf("failed importing directory %s: %s", p, err)
|
||||
continue
|
||||
}
|
||||
directories = append(directories, pkg.Dir)
|
||||
|
@ -124,3 +125,53 @@ func findGopath() []string {
|
|||
entries := strings.Split(gopath, listsep)
|
||||
return entries
|
||||
}
|
||||
|
||||
func directory(prefix string) string {
|
||||
if info, err := os.Stat(prefix); err == nil && info.IsDir() {
|
||||
return fixPathForm(prefix, prefix)
|
||||
}
|
||||
dir := filepath.Dir(prefix)
|
||||
if info, err := os.Stat(dir); err != nil || !info.IsDir() {
|
||||
return "./"
|
||||
}
|
||||
return fixPathForm(prefix, dir)
|
||||
}
|
||||
|
||||
// fixPathForm changes a file name to a relative name
|
||||
func fixPathForm(last string, file string) string {
|
||||
// get wording directory for relative name
|
||||
workDir, err := os.Getwd()
|
||||
if err != nil {
|
||||
return file
|
||||
}
|
||||
|
||||
abs, err := filepath.Abs(file)
|
||||
if err != nil {
|
||||
return file
|
||||
}
|
||||
|
||||
// if last is absolute, return path as absolute
|
||||
if filepath.IsAbs(last) {
|
||||
return fixDirPath(abs)
|
||||
}
|
||||
|
||||
rel, err := filepath.Rel(workDir, abs)
|
||||
if err != nil {
|
||||
return file
|
||||
}
|
||||
|
||||
// fix ./ prefix of path
|
||||
if rel != "." && strings.HasPrefix(last, ".") {
|
||||
rel = "./" + rel
|
||||
}
|
||||
|
||||
return fixDirPath(rel)
|
||||
}
|
||||
|
||||
func fixDirPath(path string) string {
|
||||
info, err := os.Stat(path)
|
||||
if err == nil && info.IsDir() && !strings.HasSuffix(path, "/") {
|
||||
path += "/"
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ var (
|
|||
// for test names use prefix of 'Test' or 'Example', and for benchmark
|
||||
// test names use 'Benchmark'
|
||||
func funcPredict(funcRegexp *regexp.Regexp) complete.Predictor {
|
||||
return complete.PredictFunc(func(a complete.Args) []string {
|
||||
return complete.PredictFunc(func(prefix string) []string {
|
||||
return funcNames(funcRegexp)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ func TestPredictions(t *testing.T) {
|
|||
tests := []struct {
|
||||
name string
|
||||
predictor complete.Predictor
|
||||
last string
|
||||
prefix string
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
|
@ -31,8 +31,7 @@ func TestPredictions(t *testing.T) {
|
|||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
a := complete.Args{Last: tt.last}
|
||||
got := tt.predictor.Predict(a)
|
||||
got := tt.predictor.Predict(tt.prefix)
|
||||
if !equal(got, tt.want) {
|
||||
t.Errorf("Failed %s: got: %q, want: %q", t.Name(), got, tt.want)
|
||||
}
|
||||
|
@ -44,9 +43,9 @@ func BenchmarkFake(b *testing.B) {}
|
|||
|
||||
func Example() {
|
||||
os.Setenv("COMP_LINE", "go ru")
|
||||
os.Setenv("COMP_POINT", "5")
|
||||
main()
|
||||
// output: run
|
||||
|
||||
}
|
||||
|
||||
func equal(s1, s2 []string) bool {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -2,14 +2,42 @@ package install
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/go-multierror"
|
||||
)
|
||||
|
||||
func Run(name string, uninstall, yes bool, out io.Writer, in io.Reader) {
|
||||
action := "install"
|
||||
if uninstall {
|
||||
action = "uninstall"
|
||||
}
|
||||
if !yes {
|
||||
fmt.Fprintf(out, "%s completion for %s? ", action, name)
|
||||
var answer string
|
||||
fmt.Fscanln(in, &answer)
|
||||
switch strings.ToLower(answer) {
|
||||
case "y", "yes":
|
||||
default:
|
||||
fmt.Fprintf(out, "Cancelling...")
|
||||
return
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(out, action+"ing...")
|
||||
|
||||
if uninstall {
|
||||
Uninstall(name)
|
||||
} else {
|
||||
Install(name)
|
||||
}
|
||||
}
|
||||
|
||||
type installer interface {
|
||||
IsInstalled(cmd, bin string) bool
|
||||
Install(cmd, bin string) error
|
|
@ -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
22
log.go
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
41
predict.go
41
predict.go
|
@ -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 })
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
})
|
||||
}
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
174
predict_files.go
174
predict_files.go
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
271
predict_test.go
271
predict_test.go
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue