From 999950794f375f07b41feeb3a2dc0b92df38bbf1 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Thu, 31 Jul 2025 13:53:03 +0530 Subject: [PATCH 1/3] output: allow filtering for non-list apis Currently, filtering is not working when the response object is not returning an array. This PR makes changes to allow filtering when response object is an array or a map. For async apis, filter arg is passed to the query request allowing filtering to work with the final response. Signed-off-by: Abhishek Kumar --- cmd/network.go | 11 ++++++++++- cmd/output.go | 25 +++++++++++++++++++++++-- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/cmd/network.go b/cmd/network.go index 0dae39f3..fc54ceb9 100644 --- a/cmd/network.go +++ b/cmd/network.go @@ -157,7 +157,16 @@ func pollAsyncJob(r *Request, jobID string) (map[string]interface{}, error) { return nil, errors.New("async API job query timed out") case <-ticker.C: - queryResult, queryError := NewAPIRequest(r, "queryAsyncJobResult", []string{"jobid=" + jobID}, false) + args := []string{"jobid=" + jobID} + if r.Args != nil { + for _, arg := range r.Args { + if strings.HasPrefix(strings.ToLower(arg), "filter=") { + args = append(args, arg) + break + } + } + } + queryResult, queryError := NewAPIRequest(r, "queryAsyncJobResult", args, false) if queryError != nil { return queryResult, queryError } diff --git a/cmd/output.go b/cmd/output.go index 08004cf6..6d1dee48 100644 --- a/cmd/output.go +++ b/cmd/output.go @@ -206,22 +206,43 @@ func printCsv(response map[string]interface{}, filter []string) { enc.Flush() } +func getItemsFromValue(v interface{}) ([]interface{}, bool) { + valueType := reflect.TypeOf(v) + if valueType.Kind() == reflect.Slice { + sliceItems, ok := v.([]interface{}) + if !ok { + return nil, false + } + return sliceItems, true + } else if valueType.Kind() == reflect.Map { + mapItem, ok := v.(map[string]interface{}) + if !ok { + return nil, false + } + return []interface{}{mapItem}, true + } + return nil, false +} + func filterResponse(response map[string]interface{}, filter []string, outputType string) map[string]interface{} { - if filter == nil || len(filter) == 0 { + config.Debug("Filtering response with filter:", filter, "and output type:", outputType) + if len(filter) == 0 { return response } filteredResponse := make(map[string]interface{}) for k, v := range response { valueType := reflect.TypeOf(v) if valueType.Kind() == reflect.Slice || valueType.Kind() == reflect.Map { - items, ok := v.([]interface{}) + items, ok := getItemsFromValue(v) if !ok { + config.Debug("Skipping non-slice/map value for key:", k) continue } var filteredRows []interface{} for _, item := range items { row, ok := item.(map[string]interface{}) if !ok || len(row) < 1 { + config.Debug("Skipping non-map item for key:", k) continue } filteredRow := make(map[string]interface{}) From a6ee865302b99009ee5822e5b49fd4406dc85010 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Thu, 31 Jul 2025 16:46:35 +0530 Subject: [PATCH 2/3] add fix for #152 Signed-off-by: Abhishek Kumar --- cmd/output.go | 39 +++++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/cmd/output.go b/cmd/output.go index 6d1dee48..3af70fa6 100644 --- a/cmd/output.go +++ b/cmd/output.go @@ -66,24 +66,27 @@ func printText(response map[string]interface{}) { format := "text" for k, v := range response { valueType := reflect.TypeOf(v) - if valueType.Kind() == reflect.Slice { - fmt.Printf("%v:\n", k) - for idx, item := range v.([]interface{}) { - if idx > 0 { - fmt.Println("================================================================================") - } - row, isMap := item.(map[string]interface{}) - if isMap { - for field, value := range row { - fmt.Printf("%s = %v\n", field, jsonify(value, format)) + if valueType.Kind() == reflect.Slice || valueType.Kind() == reflect.Map { + items, ok := getItemsFromValue(v) + if ok { + fmt.Printf("%v:\n", k) + for idx, item := range items { + if idx > 0 { + fmt.Println("================================================================================") + } + row, isMap := item.(map[string]interface{}) + if isMap { + for field, value := range row { + fmt.Printf("%s = %v\n", field, jsonify(value, format)) + } + } else { + fmt.Printf("%v\n", item) } - } else { - fmt.Printf("%v\n", item) } + return } - } else { - fmt.Printf("%v = %v\n", k, jsonify(v, format)) } + fmt.Printf("%v = %v\n", k, jsonify(v, format)) } } @@ -92,8 +95,8 @@ func printTable(response map[string]interface{}, filter []string) { table := tablewriter.NewWriter(os.Stdout) for k, v := range response { valueType := reflect.TypeOf(v) - if valueType.Kind() == reflect.Slice { - items, ok := v.([]interface{}) + if valueType.Kind() == reflect.Slice || valueType.Kind() == reflect.Map { + items, ok := getItemsFromValue(v) if !ok { continue } @@ -134,7 +137,7 @@ func printColumn(response map[string]interface{}, filter []string) { for _, v := range response { valueType := reflect.TypeOf(v) if valueType.Kind() == reflect.Slice || valueType.Kind() == reflect.Map { - items, ok := v.([]interface{}) + items, ok := getItemsFromValue(v) if !ok { continue } @@ -173,7 +176,7 @@ func printCsv(response map[string]interface{}, filter []string) { for _, v := range response { valueType := reflect.TypeOf(v) if valueType.Kind() == reflect.Slice || valueType.Kind() == reflect.Map { - items, ok := v.([]interface{}) + items, ok := getItemsFromValue(v) if !ok { continue } From aaf45393ad9be5cf82f0ba1e594a37b34380aa36 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Wed, 6 Aug 2025 10:39:13 +0530 Subject: [PATCH 3/3] merge Signed-off-by: Abhishek Kumar --- cli/completer.go | 17 +++++++++ cmd/api.go | 15 ++++++-- cmd/network.go | 2 +- cmd/output.go | 94 ++++++++++++++++++++++++++++-------------------- config/cache.go | 7 ++++ 5 files changed, 93 insertions(+), 42 deletions(-) diff --git a/cli/completer.go b/cli/completer.go index be1ba215..53de5fbf 100644 --- a/cli/completer.go +++ b/cli/completer.go @@ -357,6 +357,7 @@ func (t *autoCompleter) Do(line []rune, pos int) (options [][]rune, offset int) } return } + if arg.Type == config.FAKE && arg.Name == "filter=" { offset = 0 filterInputs := strings.Split(strings.Replace(argInput, ",", ",|", -1), "|") @@ -373,6 +374,22 @@ func (t *autoCompleter) Do(line []rune, pos int) (options [][]rune, offset int) return } + if arg.Type == config.FAKE && arg.Name == "exclude=" { + offset = 0 + excludeFilterInputs := strings.Split(strings.Replace(argInput, ",", ",|", -1), "|") + lastExcludeFilterInput := lastString(excludeFilterInputs) + for _, key := range apiFound.ResponseKeys { + if inArray(key, excludeFilterInputs) { + continue + } + if strings.HasPrefix(key, lastExcludeFilterInput) { + options = append(options, []rune(key[len(lastExcludeFilterInput):])) + offset = len(lastExcludeFilterInput) + } + } + return + } + autocompleteAPI := findAutocompleteAPI(arg, apiFound, apiMap) if autocompleteAPI == nil { return nil, 0 diff --git a/cmd/api.go b/cmd/api.go index 058c6fbe..872a328c 100644 --- a/cmd/api.go +++ b/cmd/api.go @@ -82,7 +82,7 @@ func init() { if strings.HasSuffix(err.Error(), "context canceled") { return nil } else if response != nil { - printResult(r.Config.Core.Output, response, nil) + printResult(r.Config.Core.Output, response, nil, nil) } return err } @@ -98,8 +98,19 @@ func init() { } } + var excludeKeys []string + for _, arg := range apiArgs { + if strings.HasPrefix(arg, "exclude=") { + for _, excludeKey := range strings.Split(strings.Split(arg, "=")[1], ",") { + if len(strings.TrimSpace(excludeKey)) > 0 { + excludeKeys = append(excludeKeys, strings.TrimSpace(excludeKey)) + } + } + } + } + if len(response) > 0 { - printResult(r.Config.Core.Output, response, filterKeys) + printResult(r.Config.Core.Output, response, filterKeys, excludeKeys) } return nil diff --git a/cmd/network.go b/cmd/network.go index fc54ceb9..05daf619 100644 --- a/cmd/network.go +++ b/cmd/network.go @@ -216,7 +216,7 @@ func NewAPIRequest(r *Request, api string, args []string, isAsync bool) (map[str expiresKey := "expires" params.Add("response", "json") params.Add("signatureversion", signatureversion) - params.Add(expiresKey, time.Now().UTC().Add(15 * time.Minute).Format(time.RFC3339)) + params.Add(expiresKey, time.Now().UTC().Add(15*time.Minute).Format(time.RFC3339)) var encodedParams string var err error diff --git a/cmd/output.go b/cmd/output.go index 3af70fa6..d817d04b 100644 --- a/cmd/output.go +++ b/cmd/output.go @@ -62,6 +62,24 @@ func printJSON(response map[string]interface{}) { enc.Encode(response) } +func getItemsFromValue(v interface{}) ([]interface{}, bool) { + valueType := reflect.TypeOf(v) + if valueType.Kind() == reflect.Slice { + sliceItems, ok := v.([]interface{}) + if !ok { + return nil, false + } + return sliceItems, true + } else if valueType.Kind() == reflect.Map { + mapItem, ok := v.(map[string]interface{}) + if !ok { + return nil, false + } + return []interface{}{mapItem}, true + } + return nil, false +} + func printText(response map[string]interface{}) { format := "text" for k, v := range response { @@ -209,72 +227,70 @@ func printCsv(response map[string]interface{}, filter []string) { enc.Flush() } -func getItemsFromValue(v interface{}) ([]interface{}, bool) { - valueType := reflect.TypeOf(v) - if valueType.Kind() == reflect.Slice { - sliceItems, ok := v.([]interface{}) - if !ok { - return nil, false - } - return sliceItems, true - } else if valueType.Kind() == reflect.Map { - mapItem, ok := v.(map[string]interface{}) - if !ok { - return nil, false - } - return []interface{}{mapItem}, true +func filterResponse(response map[string]interface{}, filter []string, excludeFilter []string, outputType string) map[string]interface{} { + if len(filter) == 0 && len(excludeFilter) == 0 { + return response } - return nil, false -} -func filterResponse(response map[string]interface{}, filter []string, outputType string) map[string]interface{} { - config.Debug("Filtering response with filter:", filter, "and output type:", outputType) - if len(filter) == 0 { - return response + excludeSet := make(map[string]struct{}, len(excludeFilter)) + for _, key := range excludeFilter { + excludeSet[key] = struct{}{} + } + + filterSet := make(map[string]struct{}, len(filter)) + for _, key := range filter { + filterSet[key] = struct{}{} } + filteredResponse := make(map[string]interface{}) - for k, v := range response { - valueType := reflect.TypeOf(v) + + for key, value := range response { + valueType := reflect.TypeOf(value) if valueType.Kind() == reflect.Slice || valueType.Kind() == reflect.Map { - items, ok := getItemsFromValue(v) + items, ok := getItemsFromValue(value) if !ok { - config.Debug("Skipping non-slice/map value for key:", k) continue } var filteredRows []interface{} for _, item := range items { row, ok := item.(map[string]interface{}) - if !ok || len(row) < 1 { - config.Debug("Skipping non-map item for key:", k) + if !ok || len(row) == 0 { continue } + filteredRow := make(map[string]interface{}) - for _, filterKey := range filter { - for field := range row { - if filterKey == field { - filteredRow[field] = row[field] + + if len(filter) > 0 { + // Include only keys that exist in filterSet + for filterKey := range filterSet { + if val, exists := row[filterKey]; exists { + filteredRow[filterKey] = val + } else if outputType == config.COLUMN || outputType == config.CSV || outputType == config.TABLE { + filteredRow[filterKey] = "" // Ensure all filter keys exist in row } } - if outputType == config.COLUMN || outputType == config.CSV || outputType == config.TABLE { - if _, ok := filteredRow[filterKey]; !ok { - filteredRow[filterKey] = "" + } else { + // Exclude keys from excludeFilter + for field, val := range row { + if _, excluded := excludeSet[field]; !excluded { + filteredRow[field] = val } } } + filteredRows = append(filteredRows, filteredRow) } - filteredResponse[k] = filteredRows + filteredResponse[key] = filteredRows } else { - filteredResponse[k] = v - continue + filteredResponse[key] = value } - } + return filteredResponse } -func printResult(outputType string, response map[string]interface{}, filter []string) { - response = filterResponse(response, filter, outputType) +func printResult(outputType string, response map[string]interface{}, filter []string, excludeFilter []string) { + response = filterResponse(response, filter, excludeFilter, outputType) switch outputType { case config.JSON: printJSON(response) diff --git a/config/cache.go b/config/cache.go index 096582d5..13596dd3 100644 --- a/config/cache.go +++ b/config/cache.go @@ -151,6 +151,13 @@ func (c *Config) UpdateCache(response map[string]interface{}) interface{} { Description: "cloudmonkey specific response key filtering", }) + // Add exclude arg + apiArgs = append(apiArgs, &APIArg{ + Name: "exclude=", + Type: FAKE, + Description: "cloudmonkey specific response key to exlude when filtering", + }) + sort.Slice(apiArgs, func(i, j int) bool { return apiArgs[i].Name < apiArgs[j].Name })