| // 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 cli |
| |
| import ( |
| "fmt" |
| "sort" |
| "strings" |
| "unicode" |
| |
| "github.com/apache/cloudstack-cloudmonkey/cmd" |
| "github.com/apache/cloudstack-cloudmonkey/config" |
| ) |
| |
| 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 command, opts := range cmd.SubCommands { |
| var args []*config.APIArg |
| options := opts |
| if command == "profile" { |
| options = config.GetProfiles() |
| } |
| for _, opt := range options { |
| args = append(args, &config.APIArg{ |
| Name: opt, |
| }) |
| } |
| apiMap[verb] = append(apiMap[verb], &config.API{ |
| Name: command, |
| Verb: verb, |
| Noun: command, |
| Args: args, |
| }) |
| } |
| } 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 { |
| firstIndex = i |
| break |
| } |
| } |
| return in[firstIndex:] |
| } |
| |
| func equal(a, b []rune) bool { |
| if len(a) != len(b) { |
| return false |
| } |
| for i := 0; i < len(a); i++ { |
| if a[i] != b[i] { |
| return false |
| } |
| } |
| return true |
| } |
| |
| func hasPrefix(r, prefix []rune) bool { |
| if len(r) < len(prefix) { |
| return false |
| } |
| return equal(r[:len(prefix)], prefix) |
| } |
| |
| func inArray(s string, array []string) bool { |
| for _, item := range array { |
| if s == item { |
| return true |
| } |
| } |
| return false |
| } |
| |
| func lastString(array []string) string { |
| return array[len(array)-1] |
| } |
| |
| type argOption struct { |
| Value string |
| Detail string |
| } |
| |
| func buildArgOptions(response map[string]interface{}, hasID bool) []argOption { |
| argOptions := []argOption{} |
| 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 |
| } |
| var id, name, detail string |
| if resource["id"] != nil { |
| id = resource["id"].(string) |
| } |
| if resource["name"] != nil { |
| name = resource["name"].(string) |
| } else if resource["username"] != nil { |
| name = resource["username"].(string) |
| } |
| if resource["displaytext"] != nil { |
| detail = resource["displaytext"].(string) |
| } |
| if len(detail) == 0 && resource["description"] != nil { |
| detail = resource["description"].(string) |
| } |
| if len(detail) == 0 && resource["ipaddress"] != nil { |
| detail = resource["ipaddress"].(string) |
| } |
| var opt argOption |
| if hasID { |
| opt.Value = id |
| opt.Detail = name |
| if len(name) == 0 { |
| opt.Detail = detail |
| } |
| } else { |
| opt.Value = name |
| opt.Detail = detail |
| if len(name) == 0 { |
| opt.Value = detail |
| } |
| } |
| argOptions = append(argOptions, opt) |
| } |
| break |
| } |
| } |
| return argOptions |
| } |
| |
| func doInternal(line []rune, pos int, lineLen int, argName []rune) (newLine [][]rune, offset int) { |
| offset = lineLen |
| if lineLen >= len(argName) { |
| if hasPrefix(line, argName) { |
| if lineLen == len(argName) { |
| newLine = append(newLine, []rune{' '}) |
| } else { |
| newLine = append(newLine, argName) |
| } |
| offset = offset - len(argName) - 1 |
| } |
| } else { |
| if hasPrefix(argName, line) { |
| newLine = append(newLine, argName[offset:]) |
| } |
| } |
| return |
| } |
| |
| func findAutocompleteAPI(arg *config.APIArg, apiFound *config.API, apiMap map[string][]*config.API) *config.API { |
| if arg.Type == "map" { |
| return nil |
| } |
| |
| var autocompleteAPI *config.API |
| argName := strings.Replace(arg.Name, "=", "", -1) |
| relatedNoun := argName |
| if argName == "id" || argName == "ids" { |
| // Heuristic: user is trying to autocomplete for id/ids arg for a list API |
| relatedNoun = apiFound.Noun |
| if apiFound.Verb != "list" { |
| relatedNoun += "s" |
| } |
| } else if argName == "account" { |
| // Heuristic: user is trying to autocomplete for accounts |
| relatedNoun = "accounts" |
| } else if argName == "ipaddressid" { |
| // Heuristic: user is trying to autocomplete for ip addresses |
| relatedNoun = "publicipaddresses" |
| } else { |
| // Heuristic: autocomplete for the arg for which a list<Arg without id/ids>s API exists |
| // For example, for zoneid arg, listZones API exists |
| cutIdx := len(argName) |
| if strings.HasSuffix(argName, "id") { |
| cutIdx -= 2 |
| } else if strings.HasSuffix(argName, "ids") { |
| cutIdx -= 3 |
| } else { |
| } |
| relatedNoun = argName[:cutIdx] + "s" |
| } |
| |
| config.Debug("Possible related noun for the arg: ", relatedNoun, " and type: ", arg.Type) |
| for _, listAPI := range apiMap["list"] { |
| if relatedNoun == listAPI.Noun { |
| autocompleteAPI = listAPI |
| break |
| } |
| } |
| |
| if autocompleteAPI != nil { |
| config.Debug("Autocomplete: API found using heuristics: ", autocompleteAPI.Name) |
| } |
| |
| if strings.HasSuffix(relatedNoun, "s") { |
| relatedNoun = relatedNoun[:len(relatedNoun)-1] |
| } |
| |
| // Heuristic: find any list API that contains the arg name |
| if autocompleteAPI == nil { |
| config.Debug("Finding possible API that have: ", argName, " related APIs: ", arg.Related) |
| possibleAPIs := []*config.API{} |
| for _, listAPI := range apiMap["list"] { |
| if strings.Contains(listAPI.Noun, argName) { |
| config.Debug("Found possible API: ", listAPI.Name) |
| possibleAPIs = append(possibleAPIs, listAPI) |
| } |
| } |
| if len(possibleAPIs) == 1 { |
| autocompleteAPI = possibleAPIs[0] |
| } |
| } |
| |
| return autocompleteAPI |
| } |
| |
| type autoCompleter struct { |
| Config *config.Config |
| } |
| |
| func (t *autoCompleter) Do(line []rune, pos int) (options [][]rune, offset int) { |
| apiMap := buildAPICacheMap(t.Config.GetAPIVerbMap()) |
| |
| var verbs []string |
| for verb := range apiMap { |
| verbs = append(verbs, verb) |
| sort.Slice(apiMap[verb], func(i, j int) bool { |
| return apiMap[verb][i].Name < apiMap[verb][j].Name |
| }) |
| } |
| sort.Strings(verbs) |
| |
| line = trimSpaceLeft(line[:pos]) |
| |
| // Auto-complete verb |
| var verbFound string |
| for _, verb := range verbs { |
| search := verb + " " |
| if !hasPrefix(line, []rune(search)) { |
| sLine, sOffset := doInternal(line, pos, len(line), []rune(search)) |
| options = append(options, sLine...) |
| offset = sOffset |
| } else { |
| verbFound = verb |
| break |
| } |
| } |
| if len(verbFound) == 0 { |
| return |
| } |
| |
| // Auto-complete noun |
| var nounFound string |
| line = trimSpaceLeft(line[len(verbFound):]) |
| for _, api := range apiMap[verbFound] { |
| search := api.Noun + " " |
| if !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 |
| } |
| |
| // Find API |
| var apiFound *config.API |
| for _, api := range apiMap[verbFound] { |
| if api.Noun == nounFound { |
| apiFound = api |
| break |
| } |
| } |
| if apiFound == nil { |
| return |
| } |
| |
| // Auto-complete API arg |
| splitLine := strings.Split(string(line), " ") |
| line = trimSpaceLeft([]rune(splitLine[len(splitLine)-1])) |
| for _, arg := range apiFound.Args { |
| search := arg.Name |
| if !hasPrefix(line, []rune(search)) { |
| sLine, sOffset := doInternal(line, pos, len(line), []rune(search)) |
| options = append(options, sLine...) |
| offset = sOffset |
| } else { |
| words := strings.Split(string(line), "=") |
| argInput := lastString(words) |
| if arg.Type == "boolean" { |
| for _, search := range []string{"true ", "false "} { |
| offset = 0 |
| if strings.HasPrefix(search, argInput) { |
| options = append(options, []rune(search[len(argInput):])) |
| offset = len(argInput) |
| } |
| } |
| return |
| } |
| if arg.Type == config.FAKE && arg.Name == "filter=" { |
| offset = 0 |
| filterInputs := strings.Split(strings.Replace(argInput, ",", ",|", -1), "|") |
| lastFilterInput := lastString(filterInputs) |
| for _, key := range apiFound.ResponseKeys { |
| if inArray(key, filterInputs) { |
| continue |
| } |
| if strings.HasPrefix(key, lastFilterInput) { |
| options = append(options, []rune(key[len(lastFilterInput):])) |
| offset = len(lastFilterInput) |
| } |
| } |
| return |
| } |
| |
| autocompleteAPI := findAutocompleteAPI(arg, apiFound, apiMap) |
| if autocompleteAPI == nil { |
| return nil, 0 |
| } |
| |
| autocompleteAPIArgs := []string{"listall=true"} |
| if autocompleteAPI.Noun == "templates" { |
| autocompleteAPIArgs = append(autocompleteAPIArgs, "templatefilter=executable") |
| } |
| |
| spinner := t.Config.StartSpinner("fetching options, please wait...") |
| request := cmd.NewRequest(nil, completer.Config, nil) |
| response, _ := cmd.NewAPIRequest(request, autocompleteAPI.Name, autocompleteAPIArgs, false) |
| t.Config.StopSpinner(spinner) |
| |
| hasID := strings.HasSuffix(arg.Name, "id=") || strings.HasSuffix(arg.Name, "ids=") |
| argOptions := buildArgOptions(response, hasID) |
| |
| filteredOptions := []argOption{} |
| if len(argOptions) > 0 { |
| sort.Slice(argOptions, func(i, j int) bool { |
| return argOptions[i].Value < argOptions[j].Value |
| }) |
| for _, item := range argOptions { |
| if strings.HasPrefix(item.Value, argInput) { |
| filteredOptions = append(filteredOptions, item) |
| } |
| } |
| } |
| offset = 0 |
| if len(filteredOptions) == 0 { |
| options = [][]rune{[]rune("")} |
| } |
| for _, item := range filteredOptions { |
| option := item.Value + " " |
| if len(filteredOptions) > 1 && len(item.Detail) > 0 { |
| option += fmt.Sprintf("(%v)", item.Detail) |
| } |
| if strings.HasPrefix(option, argInput) { |
| options = append(options, []rune(option[len(argInput):])) |
| offset = len(argInput) |
| } |
| } |
| return |
| } |
| } |
| |
| return options, offset |
| } |