Merge pull request #18 from posener/zsh

zsh support
This commit is contained in:
Eyal Posener 2017-05-13 11:10:46 +03:00 committed by GitHub
commit 758253551e
7 changed files with 233 additions and 176 deletions

View File

@ -1,153 +1,32 @@
package install package install
import ( import "fmt"
"bufio"
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"os/user"
"path/filepath"
)
type bash struct{} // (un)install in bash
// basically adds/remove from .bashrc:
func (bash) Install(cmd, bin string) error { //
bashRCFileName, err := bashRCFileName() // complete -C </path/to/completion/command> <command>
if err != nil { type bash struct {
return err rc string
}
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 (bash) Uninstall(cmd, bin string) error { func (b bash) Install(cmd, bin string) error {
bashRC, err := bashRCFileName() completeCmd := b.cmd(cmd, bin)
if err != nil { if lineInFile(b.rc, completeCmd) {
return err return fmt.Errorf("already installed in %s", b.rc)
} }
backup := bashRC + ".bck" return appendToFile(b.rc, completeCmd)
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 { func (b bash) Uninstall(cmd, bin string) error {
completeCmd := b.cmd(cmd, bin)
if !lineInFile(b.rc, completeCmd) {
return fmt.Errorf("does not installed in %s", b.rc)
}
return removeFromFile(b.rc, completeCmd)
}
func (bash) cmd(cmd, bin string) string {
return fmt.Sprintf("complete -C %s %s", bin, cmd) 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]
}
}
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
}

View File

@ -2,9 +2,11 @@ package install
import ( import (
"errors" "errors"
"fmt"
"os" "os"
"os/user"
"path/filepath" "path/filepath"
"github.com/hashicorp/go-multierror"
) )
type installer interface { type installer interface {
@ -15,46 +17,55 @@ type installer interface {
// Install complete command given: // Install complete command given:
// cmd: is the command name // cmd: is the command name
func Install(cmd string) error { func Install(cmd string) error {
shell := shellType() is := installers()
if shell == "" { if len(is) == 0 {
return errors.New("must install through a terminatl") return errors.New("Did not found any shells to install")
}
i := getInstaller(shell)
if i == nil {
return fmt.Errorf("shell %s not supported", shell)
} }
bin, err := getBinaryPath() bin, err := getBinaryPath()
if err != nil { if err != nil {
return err return err
} }
return i.Install(cmd, bin)
for _, i := range is {
errI := i.Install(cmd, bin)
if errI != nil {
multierror.Append(err, errI)
}
}
return err
} }
// Uninstall complete command given: // Uninstall complete command given:
// cmd: is the command name // cmd: is the command name
func Uninstall(cmd string) error { func Uninstall(cmd string) error {
shell := shellType() is := installers()
if shell == "" { if len(is) == 0 {
return errors.New("must uninstall through a terminatl") return errors.New("Did not found any shells to uninstall")
}
i := getInstaller(shell)
if i == nil {
return fmt.Errorf("shell %s not supported", shell)
} }
bin, err := getBinaryPath() bin, err := getBinaryPath()
if err != nil { if err != nil {
return err return err
} }
return i.Uninstall(cmd, bin)
for _, i := range is {
errI := i.Uninstall(cmd, bin)
if errI != nil {
multierror.Append(err, errI)
}
}
return err
} }
func getInstaller(shell string) installer { func installers() (i []installer) {
switch shell { if f := rcFile(".bashrc"); f != "" {
case "bash": i = append(i, bash{f})
return bash{}
default:
return nil
} }
if f := rcFile(".zshrc"); f != "" {
i = append(i, zsh{f})
}
return
} }
func getBinaryPath() (string, error) { func getBinaryPath() (string, error) {
@ -65,7 +76,10 @@ func getBinaryPath() (string, error) {
return filepath.Abs(bin) return filepath.Abs(bin)
} }
func shellType() string { func rcFile(name string) string {
shell := os.Getenv("SHELL") u, err := user.Current()
return filepath.Base(shell) if err != nil {
return ""
}
return filepath.Join(u.HomeDir, name)
} }

118
cmd/install/utils.go Normal file
View File

@ -0,0 +1,118 @@
package install
import (
"bufio"
"fmt"
"io"
"io/ioutil"
"os"
)
func lineInFile(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]
}
}
func appendToFile(name string, content string) error {
f, err := os.OpenFile(name, os.O_RDWR|os.O_APPEND, 0)
if err != nil {
return err
}
defer f.Close()
_, err = f.WriteString(fmt.Sprintf("\n%s\n", content))
return err
}
func removeFromFile(name string, content string) error {
backup := name + ".bck"
err := copyFile(name, backup)
if err != nil {
return err
}
temp, err := removeContentToTempFile(name, content)
if err != nil {
return err
}
err = copyFile(temp, name)
if err != nil {
return err
}
return os.Remove(backup)
}
func removeContentToTempFile(name, content string) (string, error) {
rf, err := os.Open(name)
if err != nil {
return "", err
}
defer rf.Close()
wf, err := ioutil.TempFile("/tmp", "complete-")
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 == content {
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
}

39
cmd/install/zsh.go Normal file
View File

@ -0,0 +1,39 @@
package install
import "fmt"
// (un)install in zsh
// basically adds/remove from .zshrc:
//
// autoload -U +X bashcompinit && bashcompinit"
// complete -C </path/to/completion/command> <command>
type zsh struct {
rc string
}
func (z zsh) Install(cmd, bin string) error {
completeCmd := z.cmd(cmd, bin)
if lineInFile(z.rc, completeCmd) {
return fmt.Errorf("already installed in %s", z.rc)
}
bashCompInit := "autoload -U +X bashcompinit && bashcompinit"
if !lineInFile(z.rc, bashCompInit) {
completeCmd = bashCompInit + "\n" + completeCmd
}
return appendToFile(z.rc, completeCmd)
}
func (z zsh) Uninstall(cmd, bin string) error {
completeCmd := z.cmd(cmd, bin)
if !lineInFile(z.rc, completeCmd) {
return fmt.Errorf("does not installed in %s", z.rc)
}
return removeFromFile(z.rc, completeCmd)
}
func (zsh) cmd(cmd, bin string) string {
return fmt.Sprintf("complete -C %s %s", bin, cmd)
}

View File

@ -4,11 +4,11 @@ package main
import "github.com/posener/complete" import "github.com/posener/complete"
var ( var (
ellipsis = complete.PredictSet("./...") ellipsis = complete.PredictSet("./...")
anyPackage = predictPackages("") anyPackage = predictPackages("")
goFiles = complete.PredictFiles("*.go") goFiles = complete.PredictFiles("*.go")
anyFile = complete.PredictFiles("*") anyFile = complete.PredictFiles("*")
anyGo = complete.PredictOr(goFiles, anyPackage, ellipsis) anyGo = complete.PredictOr(goFiles, anyPackage, ellipsis)
) )
func main() { func main() {

View File

@ -36,7 +36,14 @@ Uninstall by `gocomplete -uninstall`
- Complete packages names or `.go` files when necessary. - Complete packages names or `.go` files when necessary.
- Complete test names after `-run` flag. - Complete test names after `-run` flag.
## Usage ## complete package
Supported shells:
[x] bash
[x] zsh
### Usage
Assuming you have program called `run` and you want to have bash completion Assuming you have program called `run` and you want to have bash completion
for it, meaning, if you type `run` then space, then press the `Tab` key, for it, meaning, if you type `run` then space, then press the `Tab` key,
@ -95,7 +102,7 @@ func main() {
} }
``` ```
## Self completing program ### Self completing program
In case that the program that we want to complete is written in go we 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) can make it self completing. Here is an [example](./example/self/main.go)