Merge pull request #10 from posener/alongside

Enable completion and executable be the same command
This commit is contained in:
Eyal Posener 2017-05-10 20:14:34 +03:00 committed by GitHub
commit 1c743d8c0b
7 changed files with 257 additions and 149 deletions

View File

@ -11,81 +11,122 @@ import (
"github.com/posener/complete/cmd/install"
)
// 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 Run(cmd string) {
c := parseFlags(cmd)
err := c.validate()
if err != nil {
os.Stderr.WriteString(err.Error() + "\n")
os.Exit(1)
}
if !c.yes && !prompt(c.action(), cmd) {
fmt.Println("Cancelling...")
os.Exit(2)
}
fmt.Println(c.action() + "ing...")
if c.install {
err = install.Install(cmd)
} else {
err = install.Uninstall(cmd)
}
if err != nil {
fmt.Printf("%s failed! %s\n", c.action(), err)
os.Exit(3)
}
fmt.Println("Done!")
}
// CLI for command line
type CLI struct {
Name string
// prompt use for approval
func prompt(action, cmd string) bool {
fmt.Printf("%s completion for %s? ", action, cmd)
var answer string
fmt.Scanln(&answer)
switch strings.ToLower(answer) {
case "y", "yes":
return true
default:
return false
}
}
// config for command line
type config struct {
install bool
uninstall bool
yes bool
}
// create a config from command line arguments
func parseFlags(cmd string) config {
var c config
flag.BoolVar(&c.install, "install", false,
fmt.Sprintf("Install completion for %s command", cmd))
flag.BoolVar(&c.uninstall, "uninstall", false,
fmt.Sprintf("Uninstall completion for %s command", cmd))
flag.BoolVar(&c.yes, "y", false, "Don't prompt user for typing 'yes'")
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 {
// add flags and parse them in case they were not added and parsed
// by the main program
f.AddFlags(nil, "", "")
flag.Parse()
return c
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
}
// validate the config
func (c config) validate() error {
if c.install && c.uninstall {
return errors.New("Install and uninstall are exclusive")
// prompt use for approval
// exit if approval was not given
func (f *CLI) prompt() {
defer fmt.Println(f.action() + "ing...")
if f.yes {
return
}
if !c.install && !c.uninstall {
return errors.New("Must specify -install or -uninstall")
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, installName, uninstallName string) {
if flags == nil {
flags = flag.CommandLine
}
if installName == "" {
installName = defaultInstallName
}
if uninstallName == "" {
uninstallName = defaultUninstallName
}
if flags.Lookup(installName) == nil {
flags.BoolVar(&f.install, installName, false,
fmt.Sprintf("Install completion for %s command", f.Name))
}
if flags.Lookup(uninstallName) == nil {
flags.BoolVar(&f.uninstall, 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'")
}
}
// 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 config values.
func (c config) action() string {
if c.install {
// 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"
}
return "Uninstall"
}

View File

@ -18,23 +18,42 @@ const (
envDebug = "COMP_DEBUG"
)
// Run get a command, get the typed arguments from environment
// variable, and print out the complete options
// Complete structs define completion for a command with CLI options
type Complete struct {
Command Command
cmd.CLI
}
// 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".
func Run(name string, c Command) {
// command is the struct of the command completion.
func New(name string, command Command) *Complete {
return &Complete{
Command: command,
CLI: cmd.CLI{Name: name},
}
}
// Run get a command, get the typed arguments from 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
func (c *Complete) Run() bool {
args, ok := getLine()
if !ok {
cmd.Run(name)
return
// make sure flags parsed,
// in case they were not added in the main program
return c.CLI.Run()
}
Log("Completing args: %s", args)
options := complete(c, args)
options := complete(c.Command, args)
Log("Completion: %s", options)
output(options)
return true
}
// complete get a command an command line arguments and returns

51
example/self/main.go Normal file
View File

@ -0,0 +1,51 @@
// 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.AddFlags(nil, "complete", "uncomplete")
// 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.Run() {
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)
}

View File

@ -185,5 +185,5 @@ func main() {
},
}
complete.Run("go", gogo)
complete.New("go", gogo).Run()
}

View File

@ -15,100 +15,92 @@ func TestMatch(t *testing.T) {
panic(err)
}
tests := []struct {
m Matcher
type matcherTest struct {
prefix string
want bool
}
tests := []struct {
m Matcher
tests []matcherTest
}{
{
m: Prefix("abcd"),
prefix: "",
want: true,
m: Prefix("abcd"),
tests: []matcherTest{
{prefix: "", want: true},
{prefix: "ab", want: true},
{prefix: "ac", want: false},
},
},
{
m: Prefix("abcd"),
prefix: "ab",
want: true,
m: Prefix(""),
tests: []matcherTest{
{prefix: "ac", want: false},
{prefix: "", want: true},
},
},
{
m: Prefix("abcd"),
prefix: "ac",
want: false,
m: File("file.txt"),
tests: []matcherTest{
{prefix: "", want: true},
{prefix: "f", want: true},
{prefix: "./f", want: true},
{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},
},
},
{
m: Prefix(""),
prefix: "ac",
want: false,
m: File("./file.txt"),
tests: []matcherTest{
{prefix: "", want: true},
{prefix: "f", want: true},
{prefix: "./f", want: true},
{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},
},
},
{
m: Prefix(""),
prefix: "",
want: true,
},
{
m: File("file.txt"),
prefix: "",
want: true,
},
{
m: File("./file.txt"),
prefix: "",
want: true,
},
{
m: File("./file.txt"),
prefix: "f",
want: true,
},
{
m: File("./file.txt"),
prefix: "file.",
want: true,
},
{
m: File("./file.txt"),
prefix: "./f",
want: true,
},
{
m: File("./file.txt"),
prefix: "other.txt",
want: false,
},
{
m: File("./file.txt"),
prefix: "/file.txt",
want: false,
},
{
m: File("/file.txt"),
prefix: "file.txt",
want: false,
},
{
m: File("/file.txt"),
prefix: "./file.txt",
want: false,
},
{
m: File("/file.txt"),
prefix: "/file.txt",
want: true,
},
{
m: File("/file.txt"),
prefix: "/fil",
want: true,
m: File("/file.txt"),
tests: []matcherTest{
{prefix: "", want: false},
{prefix: "f", want: false},
{prefix: "./f", 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},
},
},
}
for _, tt := range tests {
name := tt.m.String() + "/" + tt.prefix
t.Run(name, func(t *testing.T) {
got := tt.m.Match(tt.prefix)
if got != tt.want {
t.Errorf("Failed %s: got = %t, want: %t", name, got, tt.want)
}
})
for _, ttt := range tt.tests {
name := "matcher:" + tt.m.String() + "/prefix:" + ttt.prefix
t.Run(name, func(t *testing.T) {
got := tt.m.Match(ttt.prefix)
if got != ttt.want {
t.Errorf("Failed %s: got = %t, want: %t", name, got, ttt.want)
}
})
}
}
}

View File

@ -85,7 +85,12 @@ func main() {
// 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.Run("run", run)
// name must be exactly as the binary that we want to complete.
complete.New("run", run).Run()
}
```
## Self completing program
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)