Add file completion flag

This commit is contained in:
Eyal Posener 2017-05-05 21:57:21 +03:00
parent 6311b602ab
commit 5e07cbd4c2
6 changed files with 128 additions and 29 deletions

View File

@ -11,7 +11,7 @@ type Command struct {
// options returns all available complete options for the given command
// args are all except the last command line arguments relevant to the command
func (c *Command) options(args []string) (options []string, only bool) {
func (c *Command) options(args []string) (options []Option, only bool) {
// remove the first argument, which is the command name
args = args[1:]
@ -19,7 +19,7 @@ func (c *Command) options(args []string) (options []string, only bool) {
// if prev has something that needs to follow it,
// it is the most relevant completion
if options, ok := c.Flags[last(args)]; ok && options.HasFollow {
return options.FollowsOptions, true
return options.follows(), true
}
sub, options, only := c.searchSub(args)
@ -35,13 +35,13 @@ func (c *Command) options(args []string) (options []string, only bool) {
// add global available complete options
for flag := range c.Flags {
options = append(options, flag)
options = append(options, Arg(flag))
}
return
}
func (c *Command) searchSub(args []string) (sub string, all []string, only bool) {
func (c *Command) searchSub(args []string) (sub string, all []Option, only bool) {
for i, arg := range args {
if cmd, ok := c.Sub[arg]; ok {
sub = arg
@ -52,11 +52,10 @@ func (c *Command) searchSub(args []string) (sub string, all []string, only bool)
return "", nil, false
}
func (c *Command) subCommands() []string {
subs := make([]string, 0, len(c.Sub))
func (c *Command) subCommands() []Option {
subs := make([]Option, 0, len(c.Sub))
for sub := range c.Sub {
subs = append(subs, sub)
subs = append(subs, Arg(sub))
}
return subs
}

View File

@ -13,23 +13,19 @@ const (
type Completer struct {
Command
log func(format string, args ...interface{})
}
func New(c Command) *Completer {
return &Completer{
Command: c,
log: logger(),
}
return &Completer{Command: c}
}
func (c *Completer) Complete() {
args := getLine()
c.log("Completing args: %s", args)
logger("Completing args: %s", args)
options := c.complete(args)
c.log("Completion: %s", options)
logger("Completion: %s", options)
output(options)
}
@ -38,13 +34,10 @@ func (c *Completer) complete(args []string) []string {
return c.chooseRelevant(last(args), all)
}
func (c *Completer) chooseRelevant(last string, list []string) (opts []string) {
if last == "" {
return list
}
func (c *Completer) chooseRelevant(last string, list []Option) (options []string) {
for _, sub := range list {
if strings.HasPrefix(sub, last) {
opts = append(opts, sub)
if sub.Matches(last) {
options = append(options, sub.String())
}
}
return

View File

@ -9,7 +9,9 @@ import (
func TestCompleter_Complete(t *testing.T) {
t.Parallel()
os.Setenv(envDebug, "1")
if testing.Verbose() {
os.Setenv(envDebug, "1")
}
c := Completer{
Command: Command{
@ -30,9 +32,9 @@ func TestCompleter_Complete(t *testing.T) {
Flags: map[string]FlagOptions{
"-h": FlagNoFollow,
"-global1": FlagUnknownFollow,
"-o": FlagFileFilter("./gocomplete/*.go"),
},
},
log: t.Logf,
}
allGlobals := []string{}
@ -53,7 +55,7 @@ func TestCompleter_Complete(t *testing.T) {
},
{
args: "-",
want: []string{"-h", "-global1"},
want: []string{"-h", "-global1", "-o"},
},
{
args: "-h ",
@ -77,11 +79,11 @@ func TestCompleter_Complete(t *testing.T) {
},
{
args: "sub1 ",
want: []string{"-flag1", "-flag2", "-h", "-global1"},
want: []string{"-flag1", "-flag2", "-h", "-global1", "-o"},
},
{
args: "sub2 ",
want: []string{"-flag2", "-flag3", "-h", "-global1"},
want: []string{"-flag2", "-flag3", "-h", "-global1", "-o"},
},
{
args: "sub1 -fl",
@ -97,7 +99,7 @@ func TestCompleter_Complete(t *testing.T) {
},
{
args: "sub1 -flag2 ",
want: []string{"-flag1", "-flag2", "-h", "-global1"},
want: []string{"-flag1", "-flag2", "-h", "-global1", "-o"},
},
{
args: "-no-such-flag",
@ -115,6 +117,18 @@ func TestCompleter_Complete(t *testing.T) {
args: "no-such-command ",
want: allGlobals,
},
{
args: "-o ",
want: []string{"./gocomplete/complete.go"},
},
{
args: "-o goco",
want: []string{"./gocomplete/complete.go"},
},
{
args: "-o ./goco",
want: []string{"./gocomplete/complete.go"},
},
}
for _, tt := range tests {

55
flag.go
View File

@ -1,8 +1,20 @@
package complete
import (
"os"
"path/filepath"
)
type FlagOptions struct {
HasFollow bool
FollowsOptions []string
FollowsOptions func() []Option
}
func (f *FlagOptions) follows() []Option {
if f.FollowsOptions == nil {
return nil
}
return f.FollowsOptions()
}
var (
@ -10,3 +22,44 @@ var (
FlagUnknownFollow = FlagOptions{HasFollow: true}
)
func FlagFileFilter(pattern string) FlagOptions {
return FlagOptions{
HasFollow: true,
FollowsOptions: glob(pattern),
}
}
func glob(pattern string) func() []Option {
return func() []Option {
files, err := filepath.Glob(pattern)
if err != nil {
logger("failed glob operation with pattern '%s': %s", pattern, err)
}
if !filepath.IsAbs(pattern) {
filesToRel(files)
}
options := make([]Option, len(files))
for i, f := range files {
options[i] = ArgFileName(f)
}
return options
}
}
func filesToRel(files []string) {
wd, err := os.Getwd()
if err != nil {
return
}
for i := range files {
abs, err := filepath.Abs(files[i])
if err != nil {
continue
}
rel, err := filepath.Rel(wd, abs)
if err != nil {
continue
}
files[i] = "./" + rel
}
return
}

3
log.go
View File

@ -7,8 +7,9 @@ import (
"os"
)
var logger = getLogger()
func logger() func(format string, args ...interface{}) {
func getLogger() func(format string, args ...interface{}) {
var logfile io.Writer = ioutil.Discard
if os.Getenv(envDebug) != "" {
logfile = os.Stderr

39
option.go Normal file
View File

@ -0,0 +1,39 @@
package complete
import (
"path/filepath"
"strings"
)
type Option interface {
String() string
Matches(prefix string) bool
}
type Arg string
func (a Arg) String() string {
return string(a)
}
func (a Arg) Matches(prefix string) bool {
return strings.HasPrefix(string(a), prefix)
}
type ArgFileName string
func (a ArgFileName) String() string {
return string(a)
}
func (a ArgFileName) Matches(prefix string) bool {
full, err := filepath.Abs(string(a))
if err != nil {
logger("failed getting abs path of %s: %s", a, err)
}
prefixFull, err := filepath.Abs(prefix)
if err != nil {
logger("failed getting abs path of %s: %s", prefix, err)
}
return strings.HasPrefix(full, prefixFull)
}