Initial commit
This commit is contained in:
commit
04e78e42ab
|
@ -0,0 +1,58 @@
|
|||
package complete
|
||||
|
||||
type Command struct {
|
||||
Sub map[string]Command
|
||||
Flags map[string]FlagOptions
|
||||
}
|
||||
|
||||
// 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) {
|
||||
|
||||
// remove the first argument, which is the command name
|
||||
args = args[1:]
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
sub, options, only := c.searchSub(args)
|
||||
if only {
|
||||
return
|
||||
}
|
||||
|
||||
// if no subcommand was entered in any of the args, add the
|
||||
// subcommands as complete options.
|
||||
if sub == "" {
|
||||
options = append(options, c.subCommands()...)
|
||||
}
|
||||
|
||||
// add global available complete options
|
||||
for flag := range c.Flags {
|
||||
options = append(options, flag)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Command) searchSub(args []string) (sub string, all []string, only bool) {
|
||||
for i, arg := range args {
|
||||
if cmd, ok := c.Sub[arg]; ok {
|
||||
sub = arg
|
||||
all, only = cmd.options(args[i:])
|
||||
return
|
||||
}
|
||||
}
|
||||
return "", nil, false
|
||||
}
|
||||
|
||||
func (c *Command) subCommands() []string {
|
||||
subs := make([]string, 0, len(c.Sub))
|
||||
for sub := range c.Sub {
|
||||
subs = append(subs, sub)
|
||||
}
|
||||
return subs
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
package complete
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
envComplete = "COMP_LINE"
|
||||
envDebug = "COMP_DEBUG"
|
||||
)
|
||||
|
||||
type Completer struct {
|
||||
Command
|
||||
log func(format string, args ...interface{})
|
||||
}
|
||||
|
||||
func New(c Command) *Completer {
|
||||
return &Completer{
|
||||
Command: c,
|
||||
log: logger(),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Completer) Complete() {
|
||||
args := getLine()
|
||||
c.log("Completing args: %s", args)
|
||||
|
||||
options := c.complete(args)
|
||||
|
||||
c.log("Completion: %s", options)
|
||||
output(options)
|
||||
}
|
||||
|
||||
func (c *Completer) complete(args []string) []string {
|
||||
all, _ := c.options(args[:len(args)-1])
|
||||
return c.chooseRelevant(last(args), all)
|
||||
}
|
||||
|
||||
func (c *Completer) chooseRelevant(last string, list []string) (opts []string) {
|
||||
if last == "" {
|
||||
return list
|
||||
}
|
||||
for _, sub := range list {
|
||||
if strings.HasPrefix(sub, last) {
|
||||
opts = append(opts, sub)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func getLine() []string {
|
||||
line := os.Getenv(envComplete)
|
||||
if line == "" {
|
||||
panic("should be run as a complete script")
|
||||
}
|
||||
return strings.Split(line, " ")
|
||||
}
|
||||
|
||||
func last(args []string) (last string) {
|
||||
if len(args) > 0 {
|
||||
last = args[len(args)-1]
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func output(options []string) {
|
||||
// stdout of program defines the complete options
|
||||
for _, option := range options {
|
||||
fmt.Println(option)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,149 @@
|
|||
package complete
|
||||
|
||||
import (
|
||||
"os"
|
||||
"sort"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCompleter_Complete(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
os.Setenv(envDebug, "1")
|
||||
|
||||
c := Completer{
|
||||
Command: Command{
|
||||
Sub: map[string]Command{
|
||||
"sub1": {
|
||||
Flags: map[string]FlagOptions{
|
||||
"-flag1": FlagUnknownFollow,
|
||||
"-flag2": FlagNoFollow,
|
||||
},
|
||||
},
|
||||
"sub2": {
|
||||
Flags: map[string]FlagOptions{
|
||||
"-flag2": FlagNoFollow,
|
||||
"-flag3": FlagNoFollow,
|
||||
},
|
||||
},
|
||||
},
|
||||
Flags: map[string]FlagOptions{
|
||||
"-h": FlagNoFollow,
|
||||
"-global1": FlagUnknownFollow,
|
||||
},
|
||||
},
|
||||
log: t.Logf,
|
||||
}
|
||||
|
||||
allGlobals := []string{}
|
||||
for sub := range c.Sub {
|
||||
allGlobals = append(allGlobals, sub)
|
||||
}
|
||||
for flag := range c.Flags {
|
||||
allGlobals = append(allGlobals, flag)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
args string
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
args: "",
|
||||
want: allGlobals,
|
||||
},
|
||||
{
|
||||
args: "-",
|
||||
want: []string{"-h", "-global1"},
|
||||
},
|
||||
{
|
||||
args: "-h ",
|
||||
want: allGlobals,
|
||||
},
|
||||
{
|
||||
args: "-global1 ", // global1 is known follow flag
|
||||
want: []string{},
|
||||
},
|
||||
{
|
||||
args: "sub",
|
||||
want: []string{"sub1", "sub2"},
|
||||
},
|
||||
{
|
||||
args: "sub1",
|
||||
want: []string{"sub1"},
|
||||
},
|
||||
{
|
||||
args: "sub2",
|
||||
want: []string{"sub2"},
|
||||
},
|
||||
{
|
||||
args: "sub1 ",
|
||||
want: []string{"-flag1", "-flag2", "-h", "-global1"},
|
||||
},
|
||||
{
|
||||
args: "sub2 ",
|
||||
want: []string{"-flag2", "-flag3", "-h", "-global1"},
|
||||
},
|
||||
{
|
||||
args: "sub1 -fl",
|
||||
want: []string{"-flag1", "-flag2"},
|
||||
},
|
||||
{
|
||||
args: "sub1 -flag1",
|
||||
want: []string{"-flag1"},
|
||||
},
|
||||
{
|
||||
args: "sub1 -flag1 ",
|
||||
want: []string{}, // flag1 is unknown follow flag
|
||||
},
|
||||
{
|
||||
args: "sub1 -flag2 ",
|
||||
want: []string{"-flag1", "-flag2", "-h", "-global1"},
|
||||
},
|
||||
{
|
||||
args: "-no-such-flag",
|
||||
want: []string{},
|
||||
},
|
||||
{
|
||||
args: "-no-such-flag ",
|
||||
want: allGlobals,
|
||||
},
|
||||
{
|
||||
args: "no-such-command",
|
||||
want: []string{},
|
||||
},
|
||||
{
|
||||
args: "no-such-command ",
|
||||
want: allGlobals,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.args, func(t *testing.T) {
|
||||
|
||||
tt.args = "cmd " + tt.args
|
||||
os.Setenv(envComplete, tt.args)
|
||||
args := getLine()
|
||||
|
||||
got := c.complete(args)
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
package complete
|
||||
|
||||
type FlagOptions struct {
|
||||
HasFollow bool
|
||||
FollowsOptions []string
|
||||
}
|
||||
|
||||
var (
|
||||
FlagNoFollow = FlagOptions{}
|
||||
FlagUnknownFollow = FlagOptions{HasFollow: true}
|
||||
)
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package complete
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
)
|
||||
|
||||
|
||||
func logger() func(format string, args ...interface{}) {
|
||||
var logfile io.Writer = ioutil.Discard
|
||||
if os.Getenv(envDebug) != "" {
|
||||
logfile = os.Stderr
|
||||
}
|
||||
return log.New(logfile, "complete ", log.Flags()).Printf
|
||||
}
|
Loading…
Reference in New Issue