Add easy way to install the bash completion

This commit is contained in:
Eyal Posener 2017-05-06 18:55:54 +03:00
parent d33bac720b
commit 4f47fe9246
9 changed files with 337 additions and 13 deletions

87
cmd.go Normal file
View File

@ -0,0 +1,87 @@
package complete
import (
"errors"
"flag"
"fmt"
"os"
"strings"
"github.com/posener/complete/install"
)
func runCommandLine(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, c.root)
} else {
err = install.Uninstall(cmd, c.root)
}
if err != nil {
fmt.Printf("%s failed! %s\n", c.action(), err)
os.Exit(3)
}
fmt.Println("Done!")
}
func prompt(action, cmd string) bool {
fmt.Printf("%s bash completion for %s? ", action, cmd)
var answer string
fmt.Scanln(&answer)
switch strings.ToLower(answer) {
case "y", "yes":
return true
default:
return false
}
}
type config struct {
install bool
uninstall bool
root bool
yes bool
}
func parseFlags(cmd string) config {
var c config
flag.BoolVar(&c.install, "install", false,
fmt.Sprintf("Install bash completion for %s command", cmd))
flag.BoolVar(&c.uninstall, "uninstall", false,
fmt.Sprintf("Uninstall bash completion for %s command", cmd))
flag.BoolVar(&c.root, "root", false,
"(Un)Install as root:\n"+
" (Un)Install at /etc/bash_completion.d/ (user should have write permissions to that directory).\n"+
" If not set, a complete command will be added(removed) to ~/.bashrc")
flag.BoolVar(&c.yes, "y", false, "Don't prompt user for typing 'yes'")
flag.Parse()
return c
}
func (c config) validate() error {
if c.install && c.uninstall {
return errors.New("Install and uninstall are exclusive")
}
if !c.install && !c.uninstall {
return errors.New("Must specify -install or -uninstall")
}
return nil
}
func (c config) action() string {
if c.install {
return "Install"
}
return "Uninstall"
}

View File

@ -5,6 +5,7 @@ type Commands map[string]Command
type Flags map[string]Predicate
type Command struct {
Name string
Sub Commands
Flags Flags
Args Predicate

View File

@ -164,6 +164,7 @@ func main() {
}
gogo := complete.Command{
Name: "go",
Sub: complete.Commands{
"build": build,
"install": build, // install and build have the same flags

153
install/home.go Normal file
View File

@ -0,0 +1,153 @@
package install
import (
"bufio"
"errors"
"fmt"
"io"
"os"
"os/user"
"path/filepath"
"io/ioutil"
)
type home struct{}
func (home) Install(cmd, bin string) error {
bashRCFileName, err := bashRCFileName()
if err != nil {
return err
}
completeCmd := completeCmd(cmd, bin)
if isInFile(bashRCFileName, completeCmd) {
return errors.New("Already installed in ~/.bashrc")
}
bashRC, err := os.OpenFile(bashRCFileName, os.O_RDWR|os.O_APPEND, 0)
if err != nil {
return err
}
defer bashRC.Close()
_, err = bashRC.WriteString(fmt.Sprintf("\n%s\n", completeCmd))
return err
}
func (home) Uninstall(cmd, bin string) error {
bashRC, err := bashRCFileName()
if err != nil {
return err
}
backup := bashRC + ".bck"
err = copyFile(bashRC, backup)
if err != nil {
return err
}
completeCmd := completeCmd(cmd, bin)
if !isInFile(bashRC, completeCmd) {
return errors.New("Does not installed in ~/.bashrc")
}
temp, err := uninstallToTemp(bashRC, completeCmd)
if err != nil {
return err
}
err = copyFile(temp, bashRC)
if err != nil {
return err
}
return os.Remove(backup)
}
func completeCmd(cmd, bin string) string {
return fmt.Sprintf("complete -C %s %s", bin, cmd)
}
func bashRCFileName() (string, error) {
u, err := user.Current()
if err != nil {
return "", err
}
return filepath.Join(u.HomeDir, ".bashrc"), nil
}
func isInFile(name string, lookFor string) bool {
f, err := os.Open(name)
if err != nil {
return false
}
defer f.Close()
r := bufio.NewReader(f)
prefix := []byte{}
for {
line, isPrefix, err := r.ReadLine()
if err == io.EOF {
return false
}
if err != nil {
return false
}
if isPrefix {
prefix = append(prefix, line...)
continue
}
line = append(prefix, line...)
if string(line) == lookFor {
return true
}
prefix = prefix[:0]
}
return false
}
func uninstallToTemp(bashRCFileName, completeCmd string) (string, error) {
rf, err := os.Open(bashRCFileName)
if err != nil {
return "", err
}
defer rf.Close()
wf, err := ioutil.TempFile("/tmp", "bashrc-")
if err != nil {
return "", err
}
defer wf.Close()
r := bufio.NewReader(rf)
prefix := []byte{}
for {
line, isPrefix, err := r.ReadLine()
if err == io.EOF {
break
}
if err != nil {
return "", err
}
if isPrefix {
prefix = append(prefix, line...)
continue
}
line = append(prefix, line...)
str := string(line)
if str == completeCmd {
continue
}
wf.WriteString(str + "\n")
prefix = prefix[:0]
}
return wf.Name(), nil
}
func copyFile(src string, dst string) error {
in, err := os.Open(src)
if err != nil {
return err
}
defer in.Close()
out, err := os.Create(dst)
if err != nil {
return err
}
defer out.Close()
_, err = io.Copy(out, in)
return err
}

43
install/install.go Normal file
View File

@ -0,0 +1,43 @@
package install
import (
"os"
"path/filepath"
)
type installer interface {
Install(cmd, bin string) error
Uninstall(cmd, bin string) error
}
func Install(cmd string, asRoot bool) error {
bin, err := getBinaryPath()
if err != nil {
return err
}
return getInstaller(asRoot).Install(cmd, bin)
}
func Uninstall(cmd string, asRoot bool) error {
bin, err := getBinaryPath()
if err != nil {
return err
}
return getInstaller(asRoot).Uninstall(cmd, bin)
}
func getInstaller(asRoot bool) installer {
if asRoot {
return root{}
} else {
return home{}
}
}
func getBinaryPath() (string, error) {
bin, err := os.Executable()
if err != nil {
return "", err
}
return filepath.Abs(bin)
}

30
install/root.go Normal file
View File

@ -0,0 +1,30 @@
package install
import "os"
type root struct{}
func (r root) Install(cmd string, bin string) error {
completeLink := getBashCompletionDLink(cmd)
err := r.Uninstall(cmd, bin)
if err != nil {
return err
}
return os.Symlink(bin, completeLink)
}
func (root) Uninstall(cmd string, bin string) error {
completeLink := getBashCompletionDLink(cmd)
if _, err := os.Stat(completeLink); err == nil {
err := os.Remove(completeLink)
if err != nil {
return err
}
}
return nil
}
func getBashCompletionDLink(cmd string) string {
return "/etc/bash_completion.d/"+cmd
}

View File

@ -5,20 +5,25 @@
WIP
a tool for bash writing bash completion in go.
A tool for bash writing bash completion in go.
## example: `go` command bash completion
Writing bash completion scripts is a hard work. This package provides an easy way
to create bash completion scripts for any command, and also an easy way to install/uninstall
the completion of the command.
Install in you home directory:
## go command bash completion
In [gocomplete](./gocomplete) there is an example for bash completion for the `go` command line.
### Install
```
go build -o ~/.bash_completion/go ./gocomplete
echo "complete -C ~/.bash_completion/go go" >> ~/.bashrc
go get github.com/posener/complete/gocomplete
gocomplete -install
```
Or, install in the root directory:
### Uninstall
```
sudo go build -o /etc/bash_completion.d/go ./gocomplete
gocomplete -uninstall
```

12
run.go
View File

@ -14,7 +14,11 @@ const (
// Run get a command, get the typed arguments from environment
// variable, and print out the complete options
func Run(c Command) {
args := getLine()
args, ok := getLine()
if !ok {
runCommandLine(c.Name)
return
}
Log("Completing args: %s", args)
options := complete(c, args)
@ -38,12 +42,12 @@ func complete(c Command, args []string) (matching []string) {
return
}
func getLine() []string {
func getLine() ([]string, bool) {
line := os.Getenv(envComplete)
if line == "" {
panic("should be run as a complete script")
return nil, false
}
return strings.Split(line, " ")
return strings.Split(line, " "), true
}
func last(args []string) (last string) {

View File

@ -177,7 +177,7 @@ func TestCompleter_Complete(t *testing.T) {
tt.args = "cmd " + tt.args
os.Setenv(envComplete, tt.args)
args := getLine()
args, _ := getLine()
got := complete(c, args)