package cmd import ( "encoding/json" "fmt" "strings" "github.com/itchyny/gojq" "github.com/spf13/cobra" ) // addJSONFlags adds --json, --json-fields, and --jq flags to a command. // // Flag design (BREAKING CHANGE — the previous --json was a string with // NoOptDefVal=" " so `--json=fields` projected and `--json` alone meant // "everything". That sentinel produced a `--json string[=" "]` in --help // and left users guessing about the equals sign). Now: // // - --json : Bool. "Output the response as JSON." (all fields) // - --json-fields … : String. Comma-separated projection. // - --jq … : String. jq expression filter. // // --json and --json-fields are mutually exclusive — pick one. --jq composes // with either (or neither, in which case it implies "as JSON"). func addJSONFlags(cmd *cobra.Command, jsonDesc string) { f := cmd.Flags() f.Bool("json", false, jsonDesc) f.String("json-fields", "", "Output as JSON, projecting only these comma-separated fields") f.String("jq", "", "Filter JSON output using a jq expression") cmd.MarkFlagsMutuallyExclusive("json", "json-fields") } // wantJSON returns true if the user requested JSON output via --json, // --json-fields, or --jq. func wantJSON(cmd *cobra.Command) bool { if b, _ := cmd.Flags().GetBool("json"); b { return true } if f, _ := cmd.Flags().GetString("json-fields"); f != "" { return true } if jq, _ := cmd.Flags().GetString("jq"); jq != "" { return true } return false } // outputJSON writes a value as JSON, respecting --json-fields and --jq. // --json (the bool) is the "no projection, no filter" signal handled // implicitly: when neither --json-fields nor --jq is set, the whole value // is emitted. func outputJSON(cmd *cobra.Command, value any) error { fields, _ := cmd.Flags().GetString("json-fields") jqExpr, _ := cmd.Flags().GetString("jq") return writeJSONFiltered(value, fields, jqExpr) } // writeJSON writes a value as pretty-printed JSON to ios.Out. func writeJSON(value any) error { enc := json.NewEncoder(ios.Out) enc.SetIndent("", " ") return enc.Encode(value) } // writeJSONFiltered writes a value as JSON, optionally selecting specific fields // and/or applying a jq expression. If fields is empty and jqExpr is empty, it // writes the full value. func writeJSONFiltered(value any, fields string, jqExpr string) error { // If no filtering, just write the full JSON. if fields == "" && jqExpr == "" { return writeJSON(value) } // Convert value to a generic interface via JSON round-trip so we can // manipulate it with maps/slices. raw, err := json.Marshal(value) if err != nil { return fmt.Errorf("marshaling JSON: %w", err) } var data any if err := json.Unmarshal(raw, &data); err != nil { return fmt.Errorf("unmarshaling JSON: %w", err) } // Apply field selection if specified. if fields != "" { fieldList := strings.Split(fields, ",") for i, f := range fieldList { fieldList[i] = strings.TrimSpace(f) } data = selectFields(data, fieldList) } // Apply jq expression if specified. if jqExpr != "" { return applyJQ(data, jqExpr) } return writeJSON(data) } // selectFields filters a JSON value to only include the specified fields. // Works on both single objects and arrays of objects. func selectFields(data any, fields []string) any { switch v := data.(type) { case []any: result := make([]any, len(v)) for i, item := range v { result[i] = selectFields(item, fields) } return result case map[string]any: result := make(map[string]any) for _, field := range fields { if val, ok := v[field]; ok { result[field] = val } } return result default: return data } } // applyJQ applies a jq expression to data and writes each output value. func applyJQ(data any, expr string) error { query, err := gojq.Parse(expr) if err != nil { return fmt.Errorf("invalid jq expression: %w", err) } iter := query.Run(data) enc := json.NewEncoder(ios.Out) enc.SetIndent("", " ") for { v, ok := iter.Next() if !ok { break } if err, isErr := v.(error); isErr { return fmt.Errorf("jq error: %w", err) } // For string values, print raw (no JSON encoding) to match jq behavior. if s, ok := v.(string); ok { fmt.Fprintln(ios.Out, s) } else { if err := enc.Encode(v); err != nil { return err } } } return nil }