Add easy way to install the bash completion
This commit is contained in:
parent
d33bac720b
commit
4f47fe9246
|
@ -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"
|
||||||
|
}
|
|
@ -5,6 +5,7 @@ type Commands map[string]Command
|
||||||
type Flags map[string]Predicate
|
type Flags map[string]Predicate
|
||||||
|
|
||||||
type Command struct {
|
type Command struct {
|
||||||
|
Name string
|
||||||
Sub Commands
|
Sub Commands
|
||||||
Flags Flags
|
Flags Flags
|
||||||
Args Predicate
|
Args Predicate
|
||||||
|
|
|
@ -164,6 +164,7 @@ func main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
gogo := complete.Command{
|
gogo := complete.Command{
|
||||||
|
Name: "go",
|
||||||
Sub: complete.Commands{
|
Sub: complete.Commands{
|
||||||
"build": build,
|
"build": build,
|
||||||
"install": build, // install and build have the same flags
|
"install": build, // install and build have the same flags
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
21
readme.md
21
readme.md
|
@ -5,20 +5,25 @@
|
||||||
|
|
||||||
WIP
|
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
|
go get github.com/posener/complete/gocomplete
|
||||||
echo "complete -C ~/.bash_completion/go go" >> ~/.bashrc
|
gocomplete -install
|
||||||
```
|
```
|
||||||
|
|
||||||
Or, install in the root directory:
|
### Uninstall
|
||||||
|
|
||||||
```
|
```
|
||||||
sudo go build -o /etc/bash_completion.d/go ./gocomplete
|
gocomplete -uninstall
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
12
run.go
12
run.go
|
@ -14,7 +14,11 @@ const (
|
||||||
// Run get a command, get the typed arguments from environment
|
// Run get a command, get the typed arguments from environment
|
||||||
// variable, and print out the complete options
|
// variable, and print out the complete options
|
||||||
func Run(c Command) {
|
func Run(c Command) {
|
||||||
args := getLine()
|
args, ok := getLine()
|
||||||
|
if !ok {
|
||||||
|
runCommandLine(c.Name)
|
||||||
|
return
|
||||||
|
}
|
||||||
Log("Completing args: %s", args)
|
Log("Completing args: %s", args)
|
||||||
|
|
||||||
options := complete(c, args)
|
options := complete(c, args)
|
||||||
|
@ -38,12 +42,12 @@ func complete(c Command, args []string) (matching []string) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func getLine() []string {
|
func getLine() ([]string, bool) {
|
||||||
line := os.Getenv(envComplete)
|
line := os.Getenv(envComplete)
|
||||||
if line == "" {
|
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) {
|
func last(args []string) (last string) {
|
||||||
|
|
|
@ -177,7 +177,7 @@ func TestCompleter_Complete(t *testing.T) {
|
||||||
|
|
||||||
tt.args = "cmd " + tt.args
|
tt.args = "cmd " + tt.args
|
||||||
os.Setenv(envComplete, tt.args)
|
os.Setenv(envComplete, tt.args)
|
||||||
args := getLine()
|
args, _ := getLine()
|
||||||
|
|
||||||
got := complete(c, args)
|
got := complete(c, args)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue