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

View File

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

View File

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

55
flag.go
View File

@ -1,8 +1,20 @@
package complete package complete
import (
"os"
"path/filepath"
)
type FlagOptions struct { type FlagOptions struct {
HasFollow bool HasFollow bool
FollowsOptions []string FollowsOptions func() []Option
}
func (f *FlagOptions) follows() []Option {
if f.FollowsOptions == nil {
return nil
}
return f.FollowsOptions()
} }
var ( var (
@ -10,3 +22,44 @@ var (
FlagUnknownFollow = FlagOptions{HasFollow: true} 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" "os"
) )
var logger = getLogger()
func logger() func(format string, args ...interface{}) { func getLogger() func(format string, args ...interface{}) {
var logfile io.Writer = ioutil.Discard var logfile io.Writer = ioutil.Discard
if os.Getenv(envDebug) != "" { if os.Getenv(envDebug) != "" {
logfile = os.Stderr 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)
}