Skip to content

Commit

Permalink
update to 0.80 support auto completion for zsh/bash
Browse files Browse the repository at this point in the history
  • Loading branch information
ailan-gl committed Mar 27, 2018
1 parent d6054a8 commit 4c6b531
Show file tree
Hide file tree
Showing 10 changed files with 543 additions and 23 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export VERSION=0.70
export VERSION=0.80

all: build
publish: build build_mac build_linux build_windows
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,12 @@ The following are supported authentication methods:
| RamRoleArn | Use the AssumeRole to access Alibaba Cloud services |
| EcsRamRole | Use the EcsRamRole to access ECS resources |


#### Enable bash/zsh auto completion

- Use `aliyun auto-completion` command to enable auto completion in zsh/bash
- Use `aliyun auto-completion --uninstall` command to disable auto completion.

## Use Alibaba Cloud CLI

The Alibaba Cloud OpenAPI has two styles, RPC style and RESTful style. Most of the Alibaba Cloud products use the RPC style. The way of calling an API varies depending on the API style.
Expand Down
5 changes: 5 additions & 0 deletions README_zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,11 @@ ecs | EcsRamRole:EcsTest | Valid | cn-beijing | en
| RamRoleArn | 使用RAM子账号的AssumeRole方式访问 |
| EcsRamRole | 在ECS实例上通过EcsRamRole实现免密验证 |

#### 启用zsh/bash自动补全

- 使用`aliyun auto-completion`命令开启自动补全,目前支持zsh/bash
- 使用`aliyun auto-completion --uninstall`命令关闭自动补全

## 使用阿里云CLI

阿里云云产品的OpenAPI有RPC和RESTful两种风格,大部分产品使用的是RPC风格。不同风格的API的调用方法也不同。
Expand Down
42 changes: 40 additions & 2 deletions cli/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package cli
import (
"fmt"
"github.com/aliyun/aliyun-cli/i18n"
"strings"
)

type Command struct {
Expand Down Expand Up @@ -42,7 +43,7 @@ type Command struct {
Help func(ctx *Context, args []string) error

// auto compete
AutoComplete func(ctx *Context) []string
AutoComplete func(ctx *Context, args[]string) []string

suggestDistance int
parent *Command
Expand All @@ -68,7 +69,7 @@ func (c *Command) Execute(args []string) {
ctx.completion = ParseCompletion()

//
// if
// if completion
if ctx.completion != nil {
args = ctx.completion.GetArgs()
}
Expand Down Expand Up @@ -114,6 +115,31 @@ func (c *Command) GetUsageWithParent() string {
return usage
}


func (c *Command) ExecuteComplete(ctx *Context, args []string) {
if strings.HasPrefix(ctx.completion.Current, "-") {
for _, f := range ctx.flags.Flags() {
if f.Hidden {
continue
}
if !strings.HasPrefix(f.Name, "--" + ctx.completion.Current) {
continue
}
fmt.Printf("--%s\n", f.Name)
}
} else {
for _, sc := range c.subCommands {
if sc.Hidden {
continue
}
if !strings.HasPrefix(sc.Name, ctx.completion.Current) {
continue
}
fmt.Printf("%s\n", sc.Name)
}
}
}

func (c *Command) executeInner(ctx *Context, args []string) error {
//
// fmt.Printf(">>> Execute Command: %s args=%v\n", c.Name, args)
Expand Down Expand Up @@ -184,6 +210,18 @@ func (c *Command) executeInner(ctx *Context, args []string) error {
}
}

if ctx.completion != nil {
if c.AutoComplete != nil {
ss := c.AutoComplete(ctx, callArgs)
for _, s := range ss {
fmt.Printf("%s\n", s)
}
} else {
c.ExecuteComplete(ctx, callArgs)
}
return nil
}

if ctx.help {
c.executeHelp(ctx, callArgs)
return nil
Expand Down
97 changes: 88 additions & 9 deletions cli/completion.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,30 +6,109 @@ package cli
import (
"os"
"strconv"
"fmt"
"strings"
)

type Completion struct {
Words []string
Line string
Point int
Current string
Args []string
line string
point int
}

func ParseCompletion() *Completion {
line := os.Getenv("COMP_LINE")
if line == "" {
return nil
}
p, _ := strconv.Atoi(os.Getenv("COMP_POINT"))

if p >= len(line) {
p = len(line)
}

args := parseLineForCompletion(line, p)
current := ""

if strings.HasSuffix(line, " ") {
if len(args) == 1 {
args = []string{}
} else {
args = args[1:]
}
} else {
if len(args) > 1 {
current = args[len(args) - 1]
args = args[1:len(args)-1]
} else {
panic(fmt.Errorf("unexcepted args %v for line '%s'", args, line))
}
}

point, _ := strconv.Atoi(os.Getenv("COMP_POINT"))
words := os.Getenv("COMP_WORDS")
return &Completion{
Words: strings.Split(words, " "),
Line: line,
Point: point,
Current: current,
Args: args,
line: line,
point: p,
}
}

func (c *Completion) GetCurrent() string {
return c.Current
}

func (c *Completion) GetArgs() []string {
return []string{}
return c.Args
}

func parseLineForCompletion(line string, point int) []string {
if point > len(line) {
panic(fmt.Errorf("%s[%d] out of range", line, point))

}
var quote rune
var backslash bool
var word []rune
cl := make([]string, 0)
for _, char := range line[:point] {
if backslash {
word = append(word, char)
backslash = false
continue
}
if char == '\\' {
word = append(word, char)
backslash = true
continue
}

switch quote {
case 0:
switch char {
case '\'', '"':
word = append(word, char)
quote = char
case ' ', '\t':
if word != nil {
cl = append(cl, string(word))
}
word = nil
default:
word = append(word, char)
}
case '\'':
word = append(word, char)
if char == '\'' {
quote = 0
}
case '"':
word = append(word, char)
if char == '"' {
quote = 0
}
}
}

return append(cl, string(word))
}
159 changes: 159 additions & 0 deletions cli/completion_installer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
package cli

import (
"fmt"
"github.com/aliyun/aliyun-cli/i18n"
)

func NewAutoCompleteCommand() *Command {
cmd := &Command{
Name: "auto-completion",
Short: i18n.T(
"enable auto completion",
"启用自动完成"),
Usage: "auto-completion [--uninstall]",
Run: func(ctx *Context, args []string) error {
//s, _ := os.Executable()
//fmt.Printf("%s \n", s)
//
//if f := rcFile(".zshrc"); f != "" {
// // i = append(i, zshInstaller{f})
// fmt.Printf("zshInstaller: %s\n", f)
//}
if ctx.flags.IsAssigned("uninstall") {
uninstallCompletion("aliyun")
} else {
installCompletion("aliyun")
}
return nil
},
}
cmd.Flags().Add(Flag{
Name: "uninstall",
Usage: i18n.T("uninstall auto completion", "卸载自动完成"),
})
return cmd
}

func installCompletion(cmd string) {
bin, err := getBinaryPath()
if err != nil {
Errorf("can't get binary path %s", err)
return
}

for _, i := range completionInstallers() {
err := i.Install(cmd, bin)
if err != nil {
Errorf("install completion failed for %s %s\n", bin, err)
}
}
}

func uninstallCompletion(cmd string) {
bin, err := getBinaryPath()
if err != nil {
Errorf("can't get binary path %s", err)
return
}

for _, i := range completionInstallers() {
err := i.Uninstall(cmd, bin)
if err != nil {
Errorf("uninstall %s failed\n", err)
}
}
}

func completionInstallers() (i []completionInstaller) {
for _, rc := range [...]string{".bashrc", ".bash_profile", ".bash_login", ".profile"} {
if f := rcFile(rc); f != "" {
i = append(i, bashInstaller{f})
break
}
}
if f := rcFile(".zshrc"); f != "" {
i = append(i, zshInstaller{f})
}
return
}

type completionInstaller interface {
GetName() string
Install(cmd string, bin string) error
Uninstall(cmd string, bin string) error
}

// (un)install in zshInstaller
// basically adds/remove from .zshrc:
//
// autoload -U +X bashcompinit && bashcompinit"
// complete -C </path/to/completion/command> <command>
type zshInstaller struct {
rc string
}

func (z zshInstaller) GetName() string {
return "zsh"
}

func (z zshInstaller) 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 zshInstaller) 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 (zshInstaller) cmd(cmd, bin string) string {
return fmt.Sprintf("complete -o nospace -F %s %s", bin, cmd)
}


// (un)install in bashInstaller
// basically adds/remove from .bashrc:
//
// complete -C </path/to/completion/command> <command>
type bashInstaller struct {
rc string
}

func (b bashInstaller) GetName() string {
return "bash"
}

func (b bashInstaller) Install(cmd, bin string) error {
completeCmd := b.cmd(cmd, bin)
if lineInFile(b.rc, completeCmd) {
return fmt.Errorf("already installed in %s", b.rc)
}
return appendToFile(b.rc, completeCmd)
}

func (b bashInstaller) 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 (bashInstaller) cmd(cmd, bin string) string {
return fmt.Sprintf("complete -C %s %s", bin, cmd)
}
Loading

0 comments on commit 4c6b531

Please sign in to comment.