Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ var (
debug bool // Global flag for debug mode
disableAuth bool // Global flag to disable authentication checks
clientID string // Global flag for client ID (used when auth is disabled)
outputFormat string // Global flag for output format (table, json, yaml)
outputFormat string // Global flag for output format (table, json, yaml, csv)
)

var rootCmd = &cobra.Command{
Expand Down Expand Up @@ -42,7 +42,7 @@ func init() {
rootCmd.PersistentFlags().StringVar(&clientID, "client-id", "", "Client ID to use (required when --disable-auth is enabled)")

// Add global output format flag
rootCmd.PersistentFlags().StringVar(&outputFormat, "output", "table", "Output format (table, json, yaml)")
rootCmd.PersistentFlags().StringVar(&outputFormat, "output", "table", "Output format (table, json, yaml, csv)")

// disable completion option
rootCmd.CompletionOptions.DisableDefaultCmd = true
Expand Down
137 changes: 137 additions & 0 deletions internal/formatter/formatter.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package formatter

import (
"bytes"
"encoding/csv"
"encoding/json"
"fmt"
"sort"
"strings"

"gopkg.in/yaml.v2"
Expand All @@ -15,6 +18,7 @@ const (
FormatTable OutputFormat = "table"
FormatJSON OutputFormat = "json"
FormatYAML OutputFormat = "yaml"
FormatCSV OutputFormat = "csv"
)

// Formatter handles output formatting for different formats
Expand All @@ -33,6 +37,8 @@ func New(format string) *Formatter {
f.format = FormatJSON
case "yaml", "yml":
f.format = FormatYAML
case "csv":
f.format = FormatCSV
case "table", "":
f.format = FormatTable
}
Expand All @@ -47,6 +53,8 @@ func (f *Formatter) Format(data interface{}) (string, error) {
return f.formatJSON(data)
case FormatYAML:
return f.formatYAML(data)
case FormatCSV:
return f.formatCSV(data)
case FormatTable:
// For table format, data should already be formatted as string
if str, ok := data.(string); ok {
Expand Down Expand Up @@ -76,6 +84,130 @@ func (f *Formatter) formatYAML(data interface{}) (string, error) {
return string(yamlBytes), nil
}

// formatCSV formats data as CSV (flattens structs/maps to one or more rows)
func (f *Formatter) formatCSV(data interface{}) (string, error) {
rows, err := dataToCSVRows(data)
if err != nil {
return "", fmt.Errorf("failed to convert to CSV: %v", err)
}
if len(rows) == 0 {
return "", nil
}
var buf bytes.Buffer
w := csv.NewWriter(&buf)
if err := w.WriteAll(rows); err != nil {
return "", fmt.Errorf("failed to write CSV: %v", err)
}
return strings.TrimSpace(buf.String()), nil
}

// dataToCSVRows converts interface{} to [][]string (header + data rows)
func dataToCSVRows(data interface{}) ([][]string, error) {
jsonBytes, err := json.Marshal(data)
if err != nil {
return nil, err
}
var raw interface{}
if err := json.Unmarshal(jsonBytes, &raw); err != nil {
return nil, err
}
switch v := raw.(type) {
case []interface{}:
if len(v) == 0 {
return nil, nil
}
allKeys := make(map[string]bool)
var maps []map[string]string
for _, item := range v {
m, err := flattenToMap(item)
if err != nil {
return nil, err
}
for k := range m {
allKeys[k] = true
}
maps = append(maps, m)
}
headers := sortedKeysFromSet(allKeys)
rows := [][]string{headers}
for _, m := range maps {
row := make([]string, len(headers))
for i, h := range headers {
row[i] = m[h]
}
rows = append(rows, row)
}
return rows, nil
case map[string]interface{}:
m := flattenMap("", v)
headers := sortedKeys(m)
row := make([]string, len(headers))
for i, h := range headers {
row[i] = m[h]
}
return [][]string{headers, row}, nil
default:
return nil, fmt.Errorf("unsupported type for CSV: %T", data)
}
}

func flattenToMap(data interface{}) (map[string]string, error) {
jsonBytes, err := json.Marshal(data)
if err != nil {
return nil, err
}
var raw map[string]interface{}
if err := json.Unmarshal(jsonBytes, &raw); err != nil {
return nil, err
}
return flattenMap("", raw), nil
}

func sortedKeysFromSet(m map[string]bool) []string {
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
return keys
}

func flattenMap(prefix string, m map[string]interface{}) map[string]string {
out := make(map[string]string)
for k, v := range m {
key := k
if prefix != "" {
key = prefix + "." + k
}
switch val := v.(type) {
case map[string]interface{}:
for k2, v2 := range flattenMap(key, val) {
out[k2] = v2
}
case []interface{}:
parts := make([]string, len(val))
for i, el := range val {
parts[i] = fmt.Sprint(el)
}
out[key] = strings.Join(parts, ",")
case nil:
out[key] = ""
default:
out[key] = fmt.Sprint(val)
}
}
return out
}

func sortedKeys(m map[string]string) []string {
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
return keys
}

// IsTable returns true if the format is table
func (f *Formatter) IsTable() bool {
return f.format == FormatTable
Expand All @@ -90,3 +222,8 @@ func (f *Formatter) IsJSON() bool {
func (f *Formatter) IsYAML() bool {
return f.format == FormatYAML
}

// IsCSV returns true if the format is CSV
func (f *Formatter) IsCSV() bool {
return f.format == FormatCSV
}
Loading