@@ -9,35 +9,62 @@ import (
99 "fmt"
1010 "io"
1111 "os"
12- "reflect"
1312 "strings"
1413 "text/tabwriter"
1514
1615 "gopkg.in/yaml.v2"
1716)
1817
19- func printJSON [T any ](items []T , out io.Writer ) error {
20- bytes , err := json .MarshalIndent (items , "" , " " )
18+ func printOutput (format string , items []map [string ]any , cols []ColumnDef , out io.Writer ) error {
19+ if strings .HasPrefix (format , "custom-columns=" ) {
20+ spec := strings .TrimPrefix (format , "custom-columns=" )
21+ parsed := parseCustomColumns (spec )
22+ if len (parsed ) == 0 {
23+ return fmt .Errorf ("custom-columns format specified but no valid columns given" )
24+ }
25+ return printTable (items , parsed , out )
26+ }
27+
28+ if strings .HasPrefix (format , "custom-columns-file=" ) {
29+ path := strings .TrimPrefix (format , "custom-columns-file=" )
30+ fileCols , err := parseCustomColumnsFile (path )
31+ if err != nil {
32+ return err
33+ }
34+ return printTable (items , fileCols , out )
35+ }
36+
37+ switch format {
38+ case "json" :
39+ return printJSON (items , out )
40+ case "yaml" :
41+ return printYAML (items , out )
42+ default :
43+ return printTable (items , cols , out )
44+ }
45+ }
46+
47+ func printJSON (items []map [string ]any , out io.Writer ) error {
48+ data , err := json .MarshalIndent (items , "" , " " )
2149 if err != nil {
2250 return err
2351 }
24- _ , err = fmt .Fprintln (out , string (bytes ))
52+ _ , err = fmt .Fprintln (out , string (data ))
2553 return err
2654}
2755
28- func printYAML [ T any ] (items []T , out io.Writer ) error {
29- bytes , err := yaml .Marshal (items )
56+ func printYAML (items []map [ string ] any , out io.Writer ) error {
57+ data , err := yaml .Marshal (items )
3058 if err != nil {
3159 return err
3260 }
33- _ , err = fmt .Fprint (out , string (bytes ))
61+ _ , err = fmt .Fprint (out , string (data ))
3462 return err
3563}
3664
37- func printTable [ T any ] (items []T , cols []ColumnDef , out io.Writer ) error {
65+ func printTable (items []map [ string ] any , cols []ColumnDef , out io.Writer ) error {
3866 w := tabwriter .NewWriter (out , 0 , 0 , 4 , ' ' , 0 )
3967
40- // Print headers
4168 for i , col := range cols {
4269 if i > 0 {
4370 fmt .Fprint (w , "\t " )
@@ -46,16 +73,13 @@ func printTable[T any](items []T, cols []ColumnDef, out io.Writer) error {
4673 }
4774 fmt .Fprintln (w )
4875
49- // Print rows
5076 for _ , item := range items {
51- v := reflect .ValueOf (item )
5277 for i , col := range cols {
5378 if i > 0 {
5479 fmt .Fprint (w , "\t " )
5580 }
56- field := v .FieldByName (col .Field )
57- if field .IsValid () {
58- fmt .Fprintf (w , "%v" , field .Interface ())
81+ if val , ok := fieldValue (item , col .Field ); ok {
82+ fmt .Fprint (w , formatValue (val ))
5983 }
6084 }
6185 fmt .Fprintln (w )
@@ -64,99 +88,83 @@ func printTable[T any](items []T, cols []ColumnDef, out io.Writer) error {
6488 return w .Flush ()
6589}
6690
67- func mapJsonPathToField [T any ](path string ) string {
68- path = strings .TrimPrefix (path , "." )
69-
70- typ := reflect .TypeOf (new (T )).Elem ()
71- if typ .Kind () == reflect .Ptr {
72- typ = typ .Elem ()
91+ func formatValue (v any ) string {
92+ if f , ok := v .(float64 ); ok && f == float64 (int64 (f )) {
93+ return fmt .Sprintf ("%d" , int64 (f ))
7394 }
74-
75- if typ .Kind () != reflect .Struct {
76- return path // fallback
77- }
78-
79- for i := 0 ; i < typ .NumField (); i ++ {
80- field := typ .Field (i )
81- jsonTag := field .Tag .Get ("json" )
82- if jsonTag != "" {
83- name := strings .Split (jsonTag , "," )[0 ]
84- if name == path {
85- return field .Name
86- }
87- }
88- }
89-
90- return path // fallback if no tag matches
95+ return fmt .Sprintf ("%v" , v )
9196}
9297
93- func parseCustomColumns [ T any ] (spec string ) []ColumnDef {
98+ func parseCustomColumns (spec string ) []ColumnDef {
9499 var cols []ColumnDef
95- parts := strings .Split (spec , "," )
96- for _ , part := range parts {
100+ for _ , part := range strings .Split (spec , "," ) {
97101 kv := strings .SplitN (part , ":" , 2 )
98102 if len (kv ) == 2 {
99103 cols = append (cols , ColumnDef {
100104 Header : strings .TrimSpace (kv [0 ]),
101- Field : mapJsonPathToField [ T ]( strings . TrimSpace ( kv [1 ]) ),
105+ Field : normalizeFieldPath ( kv [1 ]),
102106 })
103107 }
104108 }
105109 return cols
106110}
107111
108- func printOutput [T any ](format string , items []T , cols []ColumnDef , out io.Writer ) error {
109- if strings .HasPrefix (format , "custom-columns=" ) {
110- spec := strings .TrimPrefix (format , "custom-columns=" )
111- newCols := parseCustomColumns [T ](spec )
112- if len (newCols ) == 0 {
113- return fmt .Errorf ("custom-columns format specified but no columns given" )
114- }
115- return printTable (items , newCols , out )
112+ func parseCustomColumnsFile (path string ) ([]ColumnDef , error ) {
113+ content , err := os .ReadFile (path )
114+ if err != nil {
115+ return nil , err
116116 }
117117
118- if strings .HasPrefix (format , "custom-columns-file=" ) {
119- filename := strings .TrimPrefix (format , "custom-columns-file=" )
120- content , err := os .ReadFile (filename )
121- if err != nil {
122- return err
118+ lines := strings .Split (string (content ), "\n " )
119+ var headers , paths []string
120+ for _ , line := range lines {
121+ line = strings .TrimSpace (line )
122+ if line == "" {
123+ continue
123124 }
124-
125- lines := strings .Split (string (content ), "\n " )
126- var headers , paths []string
127- for _ , line := range lines {
128- line = strings .TrimSpace (line )
129- if line == "" {
130- continue
131- }
132- fields := strings .Fields (line )
133- if headers == nil {
134- headers = fields
135- } else if paths == nil {
136- paths = fields
137- }
125+ if headers == nil {
126+ headers = strings .Fields (line )
127+ } else if paths == nil {
128+ paths = strings .Fields (line )
138129 }
130+ }
139131
140- if len (headers ) != len (paths ) || len (headers ) == 0 {
141- return fmt .Errorf ("invalid custom-columns file format" )
142- }
132+ if len (headers ) != len (paths ) || len (headers ) == 0 {
133+ return nil , fmt .Errorf ("invalid custom-columns file format" )
134+ }
143135
144- var specParts []string
145- for i := range headers {
146- specParts = append (specParts , headers [i ]+ ":" + paths [i ])
147- }
148- spec := strings .Join (specParts , "," )
149-
150- newCols := parseCustomColumns [T ](spec )
151- return printTable (items , newCols , out )
136+ var cols []ColumnDef
137+ for i := range headers {
138+ cols = append (cols , ColumnDef {
139+ Header : headers [i ],
140+ Field : normalizeFieldPath (paths [i ]),
141+ })
152142 }
143+ return cols , nil
144+ }
153145
154- switch format {
155- case "json" :
156- return printJSON (items , out )
157- case "yaml" :
158- return printYAML (items , out )
159- default :
160- return printTable (items , cols , out )
146+ func normalizeFieldPath (path string ) string {
147+ return strings .TrimPrefix (strings .TrimSpace (path ), "." )
148+ }
149+
150+ func fieldValue (item map [string ]any , path string ) (any , bool ) {
151+ if path == "" {
152+ return nil , false
153+ }
154+
155+ var current any = item
156+ for _ , part := range strings .Split (normalizeFieldPath (path ), "." ) {
157+ if part == "" {
158+ return nil , false
159+ }
160+ m , ok := current .(map [string ]any )
161+ if ! ok {
162+ return nil , false
163+ }
164+ current , ok = m [part ]
165+ if ! ok {
166+ return nil , false
167+ }
161168 }
169+ return current , true
162170}
0 commit comments