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. // --json is an optional-value string flag: // - --json (no value) → output all fields as JSON // - --json title,state → output only those fields (gh-compatible) // // --json-fields is kept as a backwards-compatible alias. func addJSONFlags(cmd *cobra.Command, jsonDesc string) { f := cmd.Flags() f.String("json", "", jsonDesc) f.Lookup("json").NoOptDefVal = " " // space sentinel: flag present with no value f.String("json-fields", "", "Comma-separated list of JSON fields to include") f.String("jq", "", "Filter JSON output using a jq expression") } // wantJSON returns true if the user requested JSON output via --json, --json-fields, or --jq. func wantJSON(cmd *cobra.Command) bool { if j, _ := cmd.Flags().GetString("json"); j != "" { return true } if jq, _ := cmd.Flags().GetString("jq"); jq != "" { return true } if f, _ := cmd.Flags().GetString("json-fields"); f != "" { return true } return false } // outputJSON writes a value as JSON, respecting --json, --json-fields, and --jq flags. func outputJSON(cmd *cobra.Command, value any) error { jsonVal, _ := cmd.Flags().GetString("json") jsonFields, _ := cmd.Flags().GetString("json-fields") jqExpr, _ := cmd.Flags().GetString("jq") fields := "" jsonVal = strings.TrimSpace(jsonVal) if jsonVal != "" { fields = jsonVal } else if jsonFields != "" { fields = jsonFields } 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 }