diff --git a/command.go b/command.go index db60c3b..f3321fb 100644 --- a/command.go +++ b/command.go @@ -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 } - diff --git a/complete.go b/complete.go index eedaa81..6345e63 100644 --- a/complete.go +++ b/complete.go @@ -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 diff --git a/complete_test.go b/complete_test.go index 55934bd..3485a99 100644 --- a/complete_test.go +++ b/complete_test.go @@ -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 { diff --git a/flag.go b/flag.go index 2673c7a..645cb83 100644 --- a/flag.go +++ b/flag.go @@ -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 +} diff --git a/log.go b/log.go index b3fdb8e..0b0a54a 100644 --- a/log.go +++ b/log.go @@ -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 diff --git a/option.go b/option.go new file mode 100644 index 0000000..9333e84 --- /dev/null +++ b/option.go @@ -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) +}