You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cloudstack.apache.org by ro...@apache.org on 2018/04/12 23:13:45 UTC
[cloudstack-cloudmonkey] branch master updated: cli: implement
auto-completion for apis
This is an automated email from the ASF dual-hosted git repository.
rohit pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/cloudstack-cloudmonkey.git
The following commit(s) were added to refs/heads/master by this push:
new ff373cd cli: implement auto-completion for apis
ff373cd is described below
commit ff373cd11187234c77f2ff2bfc68de2d82828433
Author: Rohit Yadav <ro...@apache.org>
AuthorDate: Fri Apr 13 04:43:09 2018 +0530
cli: implement auto-completion for apis
Signed-off-by: Rohit Yadav <ro...@apache.org>
---
.gitignore | 2 +-
Makefile | 2 +-
cli/completer.go | 218 +++++++++++++++++++++++++++++++++++++++----------------
cli/selector.go | 47 ++++++++----
cmd/api.go | 29 ++++++++
cmd/network.go | 4 +-
config/cache.go | 69 ++++++++++++++----
config/config.go | 43 +++++++++++
config/util.go | 67 -----------------
9 files changed, 316 insertions(+), 165 deletions(-)
diff --git a/.gitignore b/.gitignore
index 0cbfde4..67cb950 100644
--- a/.gitignore
+++ b/.gitignore
@@ -21,5 +21,5 @@ dist
*.exe
*.test
*.out
-.gopath~
+.gopath
.idea
diff --git a/Makefile b/Makefile
index 41184d9..d32b6be 100644
--- a/Makefile
+++ b/Makefile
@@ -20,7 +20,7 @@ PACKAGE = cloudmonkey
DATE ?= $(shell date +%FT%T%z)
VERSION ?= $(shell git describe --tags --always --dirty --match=v* 2> /dev/null || \
cat $(CURDIR)/.version 2> /dev/null || echo v0)
-GOPATH = $(CURDIR)/.gopath~
+GOPATH = $(CURDIR)/.gopath
BIN = $(GOPATH)/bin
BASE = $(GOPATH)/src/$(PACKAGE)
PKGS = $(or $(PKG),$(shell cd $(BASE) && env GOPATH=$(GOPATH) $(GO) list ./... | grep -v "^$(PACKAGE)/vendor/"))
diff --git a/cli/completer.go b/cli/completer.go
index ba45391..1a4c0a2 100644
--- a/cli/completer.go
+++ b/cli/completer.go
@@ -35,7 +35,29 @@ type CliCompleter struct {
var completer *CliCompleter
-func TrimSpaceLeft(in []rune) []rune {
+func buildApiCacheMap(apiMap map[string][]*config.Api) map[string][]*config.Api {
+ for _, cmd := range cmd.AllCommands() {
+ verb := cmd.Name
+ if cmd.SubCommands != nil && len(cmd.SubCommands) > 0 {
+ for _, scmd := range cmd.SubCommands {
+ dummyApi := &config.Api{
+ Name: scmd,
+ Verb: verb,
+ }
+ apiMap[verb] = append(apiMap[verb], dummyApi)
+ }
+ } else {
+ dummyApi := &config.Api{
+ Name: "",
+ Verb: verb,
+ }
+ apiMap[verb] = append(apiMap[verb], dummyApi)
+ }
+ }
+ return apiMap
+}
+
+func trimSpaceLeft(in []rune) []rune {
firstIndex := len(in)
for i, r := range in {
if unicode.IsSpace(r) == false {
@@ -65,36 +87,9 @@ func doInternal(line []rune, pos int, lineLen int, argName []rune) (newLine [][]
return
}
-func (t *CliCompleter) Do(line []rune, pos int) (newLine [][]rune, offset int) {
-
- line = TrimSpaceLeft(line[:pos])
- lineLen := len(line)
+func (t *CliCompleter) Do(line []rune, pos int) (options [][]rune, offset int) {
- apiCache := t.Config.GetCache()
- apiMap := make(map[string][]*config.Api)
- for api := range apiCache {
- verb := apiCache[api].Verb
- apiMap[verb] = append(apiMap[verb], apiCache[api])
- }
-
- for _, cmd := range cmd.AllCommands() {
- verb := cmd.Name
- if cmd.SubCommands != nil && len(cmd.SubCommands) > 0 {
- for _, scmd := range cmd.SubCommands {
- dummyApi := &config.Api{
- Name: scmd,
- Verb: verb,
- }
- apiMap[verb] = append(apiMap[verb], dummyApi)
- }
- } else {
- dummyApi := &config.Api{
- Name: "",
- Verb: verb,
- }
- apiMap[verb] = append(apiMap[verb], dummyApi)
- }
- }
+ apiMap := buildApiCacheMap(t.Config.GetApiVerbMap())
var verbs []string
for verb := range apiMap {
@@ -105,56 +100,155 @@ func (t *CliCompleter) Do(line []rune, pos int) (newLine [][]rune, offset int) {
}
sort.Strings(verbs)
- var verbsFound []string
+ line = trimSpaceLeft(line[:pos])
+
+ // Auto-complete verb
+ var verbFound string
for _, verb := range verbs {
search := verb + " "
if !runes.HasPrefix(line, []rune(search)) {
- sLine, sOffset := doInternal(line, pos, lineLen, []rune(search))
- newLine = append(newLine, sLine...)
+ sLine, sOffset := doInternal(line, pos, len(line), []rune(search))
+ options = append(options, sLine...)
offset = sOffset
} else {
- verbsFound = append(verbsFound, verb)
+ verbFound = verb
+ break
}
}
+ if len(verbFound) == 0 {
+ return
+ }
- apiArg := false
- for _, verbFound := range verbsFound {
- search := verbFound + " "
+ // Auto-complete noun
+ var nounFound string
+ line = trimSpaceLeft(line[len(verbFound):])
+ for _, api := range apiMap[verbFound] {
+ search := api.Noun + " "
+ if !runes.HasPrefix(line, []rune(search)) {
+ sLine, sOffset := doInternal(line, pos, len(line), []rune(search))
+ options = append(options, sLine...)
+ offset = sOffset
+ } else {
+ nounFound = api.Noun
+ break
+ }
+ }
+ if len(nounFound) == 0 {
+ return
+ }
- nLine := TrimSpaceLeft(line[len(search):])
- offset = lineLen - len(verbFound) - 1
+ // Find API
+ var apiFound *config.Api
+ for _, api := range apiMap[verbFound] {
+ if api.Noun == nounFound {
+ apiFound = api
+ break
+ }
+ }
+ if apiFound == nil {
+ return
+ }
- for _, api := range apiMap[verbFound] {
- resource := strings.TrimPrefix(strings.ToLower(api.Name), verbFound)
- search = resource + " "
+ // Auto-complete api args
+ splitLine := strings.Split(string(line), " ")
+ line = trimSpaceLeft([]rune(splitLine[len(splitLine)-1]))
+ for _, arg := range apiFound.Args {
+ search := arg.Name + "="
+ if !runes.HasPrefix(line, []rune(search)) {
+ sLine, sOffset := doInternal(line, pos, len(line), []rune(search))
+ options = append(options, sLine...)
+ offset = sOffset
+ } else {
+ if arg.Type == "boolean" {
+ options = [][]rune{[]rune("true "), []rune("false ")}
+ offset = 0
+ return
+ }
+
+ var autocompleteApi *config.Api
+ var relatedNoun string
+ if arg.Name == "id" || arg.Name == "ids" {
+ relatedNoun = apiFound.Noun
+ if apiFound.Verb != "list" {
+ relatedNoun += "s"
+ }
+ } else if arg.Name == "account" {
+ relatedNoun = "accounts"
+ } else {
+ relatedNoun = strings.Replace(strings.Replace(arg.Name, "ids", "", -1), "id", "", -1) + "s"
+ }
+ for _, related := range apiMap["list"] {
+ if relatedNoun == related.Noun {
+ autocompleteApi = related
+ break
+ }
+ }
+
+ if autocompleteApi == nil {
+ return nil, 0
+ }
+
+ r := cmd.NewRequest(nil, config.NewConfig(), nil, nil)
+ autocompleteApiArgs := []string{"listall=true"}
+ if autocompleteApi.Noun == "templates" {
+ autocompleteApiArgs = append(autocompleteApiArgs, "templatefilter=all")
+ }
+ response, _ := cmd.NewAPIRequest(r, autocompleteApi.Name, autocompleteApiArgs)
- if runes.HasPrefix(nLine, []rune(search)) {
- // FIXME: handle params to API here with = stuff
- for _, arg := range api.Args {
- opt := arg.Name + "="
- newLine = append(newLine, []rune(opt))
+ var autocompleteOptions []SelectOption
+ for _, v := range response {
+ switch obj := v.(type) {
+ case []interface{}:
+ if obj == nil {
+ break
+ }
+ for _, item := range obj {
+ resource, ok := item.(map[string]interface{})
+ if !ok {
+ continue
+ }
+ opt := SelectOption{}
+ if resource["id"] != nil {
+ opt.Id = resource["id"].(string)
+ }
+ if resource["name"] != nil {
+ opt.Name = resource["name"].(string)
+ } else if resource["username"] != nil {
+ opt.Name = resource["username"].(string)
+ }
+ if resource["displaytext"] != nil {
+ opt.Detail = resource["displaytext"].(string)
+ }
+
+ autocompleteOptions = append(autocompleteOptions, opt)
+ }
+ break
}
- if string(nLine[len(nLine)-1]) == "=" {
- apiArg = true
+ }
+
+ var selected string
+ if len(autocompleteOptions) > 1 {
+ sort.Slice(autocompleteOptions, func(i, j int) bool {
+ return autocompleteOptions[i].Name < autocompleteOptions[j].Name
+ })
+ fmt.Println()
+ selectedOption := ShowSelector(autocompleteOptions)
+ if arg.Name == "account" {
+ selected = selectedOption.Name
+ } else {
+ selected = selectedOption.Id
}
- offset = lineLen - len(verbFound) - len(resource) - 1
} else {
- sLine, _ := doInternal(nLine, pos, len(nLine), []rune(search))
- newLine = append(newLine, sLine...)
+ if len(autocompleteOptions) == 1 {
+ selected = autocompleteOptions[0].Id
+ }
}
+ options = [][]rune{[]rune(selected + " ")}
+ offset = 0
}
}
- // FIXME: pass selector uuid options
- if apiArg {
- fmt.Println()
- option := ShowSelector()
- // show only one option in autocompletion
- newLine = [][]rune{[]rune(option)}
- offset = 0
- }
-
- return newLine, offset
+ return options, offset
}
func NewCompleter(cfg *config.Config) *CliCompleter {
diff --git a/cli/selector.go b/cli/selector.go
index 5cf35af..bb1696e 100644
--- a/cli/selector.go
+++ b/cli/selector.go
@@ -25,23 +25,38 @@ import (
"github.com/rhtyd/readline"
)
-type SelectOptions struct {
- Name string
+type SelectOption struct {
Id string
+ Name string
Detail string
}
-func ShowSelector() string {
- options := []SelectOptions{
- {Name: "Option1", Id: "some-uuid", Detail: "Some Detail"},
- {Name: "Option2", Id: "some-uuid", Detail: "Some Detail"},
- {Name: "Option3", Id: "some-uuid", Detail: "Some Detail"},
- {Name: "Option4", Id: "some-uuid", Detail: "Some Detail"},
- {Name: "Option5", Id: "some-uuid", Detail: "Some Detail"},
- {Name: "Option6", Id: "some-uuid", Detail: "Some Detail"},
- {Name: "Option7", Id: "some-uuid", Detail: "Some Detail"},
- {Name: "Option8", Id: "some-uuid", Detail: "Some Detail"},
+type Selector struct {
+ InUse bool
+}
+
+var selector Selector
+
+func init() {
+ selector = Selector{
+ InUse: false,
+ }
+}
+
+func (s Selector) lock() {
+ s.InUse = true
+}
+
+func (s Selector) unlock() {
+ s.InUse = false
+}
+
+func ShowSelector(options []SelectOption) SelectOption {
+ if selector.InUse {
+ return SelectOption{}
}
+ selector.lock()
+ defer selector.unlock()
templates := &promptui.SelectTemplates{
Label: "{{ . }}?",
@@ -50,9 +65,9 @@ func ShowSelector() string {
Selected: "Selected: {{ .Name | cyan }} ({{ .Id | red }})",
Details: `
--------- Current Selection ----------
-{{ "Name:" | faint }} {{ .Name }}
{{ "Id:" | faint }} {{ .Id }}
-{{ "Detail:" | faint }} {{ .Detail }}`,
+{{ "Name:" | faint }} {{ .Name }}
+{{ "Description:" | faint }} {{ .Detail }}`,
}
searcher := func(input string, index int) bool {
@@ -83,8 +98,8 @@ func ShowSelector() string {
if err != nil {
fmt.Printf("Prompt failed %v\n", err)
- return ""
+ return SelectOption{}
}
- return options[i].Id
+ return options[i]
}
diff --git a/cmd/api.go b/cmd/api.go
index 4be24e3..ff9d0ce 100644
--- a/cmd/api.go
+++ b/cmd/api.go
@@ -51,6 +51,35 @@ func init() {
return errors.New("unknown or unauthorized API: " + apiName)
}
+ if strings.Contains(strings.Join(apiArgs, " "), "-h") {
+ fmt.Println("=== Help docs ===")
+ fmt.Println(api.Name, ":", api.Description)
+ fmt.Println("Async:", api.Async)
+ fmt.Println("Required params:", strings.Join(api.RequiredArgs, ", "))
+ for _, arg := range api.Args {
+ fmt.Println(arg.Name, "(", arg.Type, ")", arg.Description)
+ }
+ return nil
+ }
+
+ var missingArgs []string
+ for _, required := range api.RequiredArgs {
+ provided := false
+ for _, arg := range apiArgs {
+ if strings.HasPrefix(arg, required+"=") {
+ provided = true
+ }
+ }
+ if !provided {
+ missingArgs = append(missingArgs, required)
+ }
+ }
+
+ if len(missingArgs) > 0 {
+ fmt.Println("Missing required arguments: ", strings.Join(missingArgs, ", "))
+ return nil
+ }
+
b, _ := NewAPIRequest(r, api.Name, apiArgs)
response, _ := json.MarshalIndent(b, "", " ")
diff --git a/cmd/network.go b/cmd/network.go
index 352afd9..6ff7d7c 100644
--- a/cmd/network.go
+++ b/cmd/network.go
@@ -57,8 +57,6 @@ func encodeRequestParams(params url.Values) string {
}
func NewAPIRequest(r *Request, api string, args []string) (map[string]interface{}, error) {
- fmt.Println("[debug] Running api:", api, args)
-
params := make(url.Values)
params.Add("command", api)
for _, arg := range args {
@@ -85,7 +83,7 @@ func NewAPIRequest(r *Request, api string, args []string) (map[string]interface{
apiUrl := fmt.Sprintf("%s?%s", r.Config.ActiveProfile.Url, encodedParams)
- fmt.Println("[debug] Requesting: ", apiUrl)
+ //fmt.Println("[debug] Requesting: ", apiUrl)
response, err := http.Get(apiUrl)
if err != nil {
fmt.Println("Error:", err)
diff --git a/config/cache.go b/config/cache.go
index 3814669..27b431f 100644
--- a/config/cache.go
+++ b/config/cache.go
@@ -21,31 +21,46 @@ import (
"encoding/json"
"fmt"
"io/ioutil"
+ "sort"
"strings"
"unicode"
)
type ApiArg struct {
Name string
+ Type string
+ Related []string
Description string
Required bool
Length int
- Type string
- Related []string
}
type Api struct {
Name string
- ResponseName string
- Description string
- Async bool
- Related []string
- Args []*ApiArg
- RequiredArgs []*ApiArg
Verb string
+ Noun string
+ Args []*ApiArg
+ RequiredArgs []string
+ Related []string
+ Async bool
+ Description string
+ ResponseName string
}
var apiCache map[string]*Api
+var apiVerbMap map[string][]*Api
+
+func (c *Config) GetApiVerbMap() map[string][]*Api {
+ if apiVerbMap != nil {
+ return apiVerbMap
+ }
+ apiSplitMap := make(map[string][]*Api)
+ for api := range apiCache {
+ verb := apiCache[api].Verb
+ apiSplitMap[verb] = append(apiSplitMap[verb], apiCache[api])
+ }
+ return apiSplitMap
+}
func (c *Config) GetCache() map[string]*Api {
if apiCache == nil {
@@ -73,6 +88,7 @@ func (c *Config) SaveCache(response map[string]interface{}) {
func (c *Config) UpdateCache(response map[string]interface{}) interface{} {
apiCache = make(map[string]*Api)
+ apiVerbMap = nil
count := response["count"]
apiList := response["api"].([]interface{})
@@ -85,6 +101,7 @@ func (c *Config) UpdateCache(response map[string]interface{}) interface{} {
}
apiName := api["name"].(string)
isAsync := api["isasync"].(bool)
+ description := api["description"].(string)
idx := 0
for _, chr := range apiName {
@@ -95,22 +112,44 @@ func (c *Config) UpdateCache(response map[string]interface{}) interface{} {
}
}
verb := apiName[:idx]
+ noun := strings.ToLower(apiName[idx:])
var apiArgs []*ApiArg
for _, argNode := range api["params"].([]interface{}) {
apiArg, _ := argNode.(map[string]interface{})
+ related := []string{}
+ if apiArg["related"] != nil {
+ related = strings.Split(apiArg["related"].(string), ",")
+ sort.Strings(related)
+ }
apiArgs = append(apiArgs, &ApiArg{
- Name: apiArg["name"].(string),
- Type: apiArg["type"].(string),
- Required: apiArg["required"].(bool),
+ Name: apiArg["name"].(string),
+ Type: apiArg["type"].(string),
+ Required: apiArg["required"].(bool),
+ Related: related,
+ Description: apiArg["description"].(string),
})
}
+ sort.Slice(apiArgs, func(i, j int) bool {
+ return apiArgs[i].Name < apiArgs[j].Name
+ })
+
+ var requiredArgs []string
+ for _, arg := range apiArgs {
+ if arg.Required {
+ requiredArgs = append(requiredArgs, arg.Name)
+ }
+ }
+
apiCache[strings.ToLower(apiName)] = &Api{
- Name: apiName,
- Async: isAsync,
- Args: apiArgs,
- Verb: verb,
+ Name: apiName,
+ Verb: verb,
+ Noun: noun,
+ Args: apiArgs,
+ RequiredArgs: requiredArgs,
+ Async: isAsync,
+ Description: description,
}
}
return count
diff --git a/config/config.go b/config/config.go
index 06c5393..0f2d71e 100644
--- a/config/config.go
+++ b/config/config.go
@@ -18,10 +18,24 @@
package config
import (
+ "fmt"
+ homedir "github.com/mitchellh/go-homedir"
"os"
"path"
)
+var name = "cloudmonkey"
+var version = "6.0.0-alpha1"
+
+func getDefaultConfigDir() string {
+ home, err := homedir.Dir()
+ if err != nil {
+ fmt.Println(err)
+ os.Exit(1)
+ }
+ return path.Join(home, ".cmk")
+}
+
type OutputFormat string
const (
@@ -98,3 +112,32 @@ func loadConfig() *Config {
return cfg
}
+
+func (c *Config) Name() string {
+ return name
+}
+
+func (c *Config) Version() string {
+ return version
+}
+
+func (c *Config) PrintHeader() {
+ fmt.Printf("Apache CloudStack 🐵 cloudmonkey %s.\n", version)
+ fmt.Printf("Type \"help\" for details, \"sync\" to update API cache or press tab to list commands.\n\n")
+}
+
+func (c *Config) GetPrompt() string {
+ return fmt.Sprintf("(%s) \033[34m🐵\033[0m > ", c.ActiveProfile.Name)
+}
+
+func (c *Config) UpdateGlobalConfig(key string, value string) {
+ c.UpdateConfig("", key, value)
+}
+
+func (c *Config) UpdateConfig(namespace string, key string, value string) {
+ fmt.Println("Updating for key", key, ", value=", value, ", in ns=", namespace)
+ if key == "profile" {
+ //FIXME
+ c.ActiveProfile.Name = value
+ }
+}
diff --git a/config/util.go b/config/util.go
deleted file mode 100644
index 7421e5b..0000000
--- a/config/util.go
+++ /dev/null
@@ -1,67 +0,0 @@
-// Licensed to the Apache Software Foundation (ASF) under one
-// or more contributor license agreements. See the NOTICE file
-// distributed with this work for additional information
-// regarding copyright ownership. The ASF licenses this file
-// to you under the Apache License, Version 2.0 (the
-// "License"); you may not use this file except in compliance
-// with the License. You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing,
-// software distributed under the License is distributed on an
-// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-// KIND, either express or implied. See the License for the
-// specific language governing permissions and limitations
-// under the License.
-
-package config
-
-import (
- "fmt"
- "os"
- "path"
-
- "github.com/mitchellh/go-homedir"
-)
-
-var name = "cloudmonkey"
-var version = "6.0.0-alpha1"
-
-func getDefaultConfigDir() string {
- home, err := homedir.Dir()
- if err != nil {
- fmt.Println(err)
- os.Exit(1)
- }
- return path.Join(home, ".cmk")
-}
-
-func (c *Config) Name() string {
- return name
-}
-
-func (c *Config) Version() string {
- return version
-}
-
-func (c *Config) PrintHeader() {
- fmt.Printf("Apache CloudStack 🐵 cloudmonkey %s.\n", version)
- fmt.Printf("Type \"help\" for details, \"sync\" to update API cache or press tab to list commands.\n\n")
-}
-
-func (c *Config) GetPrompt() string {
- return fmt.Sprintf("(%s) \033[34m🐵\033[0m > ", c.ActiveProfile.Name)
-}
-
-func (c *Config) UpdateGlobalConfig(key string, value string) {
- c.UpdateConfig("", key, value)
-}
-
-func (c *Config) UpdateConfig(namespace string, key string, value string) {
- fmt.Println("Updating for key", key, ", value=", value, ", in ns=", namespace)
- if key == "profile" {
- //FIXME
- c.ActiveProfile.Name = value
- }
-}
--
To stop receiving notification emails like this one, please contact
rohit@apache.org.