-
Notifications
You must be signed in to change notification settings - Fork 4
/
formatting.go
330 lines (288 loc) · 10.7 KB
/
formatting.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
package common
import (
"fmt"
"io"
"os"
"reflect"
"strings"
"github.com/goccy/go-yaml"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/jedib0t/go-pretty/v6/text"
"github.com/spf13/cobra"
"github.com/thediveo/enumflag"
)
// OutputFormat encodes the available formats that can be used to display structured data
type OutputFormat enumflag.Flag
// Supported output formats
const (
FormatTable OutputFormat = iota
FormatJSON
FormatYAML
FormatCSV
FormatText
)
// OutputFormatFunc is a handler used to customize how a tabular field is formatted.
// The formatting utility calls the handler with the following arguments:
// - object: the golang struct that represents the table row being formatted
// - colume: the table column for which this handler is called
// - field: the object that needs formatting
//
// The handler must return a textual representation of the `field` object.
type OutputFormatFunc func(object interface{}, column string, field interface{}) string
// OutputFormatters is a map of output formatters, indexed by column name
type OutputFormatters map[string]OutputFormatFunc
// OutputFormatIDs maps format values to their textual representations
var OutputFormatIDs = map[OutputFormat][]string{
FormatTable: {"table"},
FormatJSON: {"json"},
FormatYAML: {"yaml"},
FormatCSV: {"csv"},
FormatText: {"text"},
}
// FormattingOptions contains output formatting parameters
type FormattingOptions struct {
// Output format
Format OutputFormat
// List of field specifiers controlling how information is converted from structured data into tabular format.
// Each value can be formatted using the following syntax:
//
// <column-name>[:<field-name>[.<subfield-name>[...]]]
//
// The <column-name> token represents the name used for the header. If a field is not specified, it is also
// interpreted as a field name and its value is not case-sensitive as far as it concerns matching the field names
// in the structure information being formatting.
//
// The <field-name> and subsequent <subfield-name> tokens are used to identify the exact (sub)field in the hierarchically
// structured information that the table column maps to. Their values are not case-sensitive.
Fields []string
// List of column names and their associated sorting mode, in sorting order.
SortBy []table.SortBy
// Custom formatting functions
Formatters OutputFormatters
}
// NewFormattingOptions initializes formatting options for a cobra command. It accepts the following arguments:
// - fields: list of field specifiers controlling how information is converted from structured data into tabular format
// (see FormattingOptions/Fields).
// - sortFields: list of sort specifiers. Each specifier should indicate the column name and sort mode. The order is significant
// and will determine the order in which columns will be sorted.
// - formatters: map of custom formatters. Use this to attach custom formatting handlers to columns that are not
// handled properly by the default formatting.
func NewFormattingOptions(fields []string, sortFields []table.SortBy, formatters OutputFormatters) (o *FormattingOptions) {
o = &FormattingOptions{}
if sortFields != nil {
o.SortBy = sortFields
}
if fields != nil {
o.Fields = fields
}
if formatters != nil {
o.Formatters = formatters
}
return
}
// NewSingleValueFormattingOptions initializes formatting options for a cobra command. Use this method instead of NewTableFormattingOptions
// if your command doesn't need to format list of objects in a table layout.
func NewSingleValueFormattingOptions() (o *FormattingOptions) {
return &FormattingOptions{}
}
func (o *FormattingOptions) addFormattingFlags(cmd *cobra.Command, defaultFormat OutputFormat) {
mapping := make(map[OutputFormat][]string)
formats := make([]string, 0)
for i, f := range OutputFormatIDs {
// remove table formats (table and CSV) if not using a table format
if defaultFormat != FormatTable && (i == FormatTable || i == FormatCSV) {
continue
}
// add text format only when explicitily using text format
if defaultFormat != FormatText && i == FormatText {
continue
}
formats = append(formats, f[0])
mapping[i] = f
}
o.Format = defaultFormat
cmd.Flags().Var(
enumflag.New(&o.Format, "format", mapping, enumflag.EnumCaseInsensitive),
"format",
fmt.Sprintf("specify the output format. Possible values are: %s", strings.Join(formats, ", ")),
)
cmd.Use = fmt.Sprintf("%s [--format {%s}]", cmd.Use, strings.Join(formats, ","))
// if using table format add flag for selecting a table column(s)
if defaultFormat == FormatTable {
cmd.Flags().StringSliceVar(&o.Fields, "field", o.Fields,
`specify one or more columns to include in the output.
The field name may also be specified explicitly if different than the column name.
This option only has effect with the 'table' and 'csv' formats.`)
cmd.Use = fmt.Sprintf("%s [--field COLUMN[:FIELD]]...", cmd.Use)
}
}
// AddMultiValueFormattingFlags adds formatting command line flags to a cobra command.
// This function includes tabular formatting parameters. If the command only outputs
// single objects, use AddSingleValueFormattingFlags instead.
func (o *FormattingOptions) AddMultiValueFormattingFlags(cmd *cobra.Command) {
o.addFormattingFlags(cmd, FormatTable)
}
// AddSingleValueFormattingFlags adds formatting command line flags to a cobra command with the
// specified output format as default.
// This function does not include tabular formatting parameters. If the command also outputs
// lists of objects that can be formatted using a tabular layout, use AddMultiValueFormattingFlags
// instead.
func (o *FormattingOptions) AddSingleValueFormattingFlags(cmd *cobra.Command, defaultFormat OutputFormat) {
o.addFormattingFlags(cmd, defaultFormat)
}
// Recursive function that extracts a subfield from a generic hierarchical structure.
// This really a simple and far less powerful alternative to JSONPath and YAML path.
func getFieldValue(valueMap map[string]interface{}, fields []string) interface{} {
fieldValue, hasField := valueMap[fields[0]]
// field not found
if !hasField {
return nil
}
// field found and no sub-fields specified
if len(fields) == 1 {
return fieldValue
}
// field found and sub-fields specified and field is a sub-map
if subMap, isMap := fieldValue.(map[string]interface{}); isMap {
return getFieldValue(subMap, fields[1:])
}
// field found, but sub-fields specified and field is not a sub-map
return nil
}
func (o *FormattingOptions) formatTable(out io.Writer, values []interface{}) {
// Convert the formatting field specifiers into column names and field/subfield names
columns := make([]string, len(o.Fields))
fields := make([][]string, len(o.Fields))
for i, f := range o.Fields {
f = strings.TrimSpace(f)
// Split the field name and extract the explicit field name, if supplied, then
// split each field name into sub-fields.
s := strings.Split(f, ":")
if len(s) > 1 {
columns[i] = s[0]
fields[i] = strings.Split(strings.ToLower(s[len(s)-1]), ".")
} else if len(s) == 1 {
columns[i] = s[0]
fields[i] = strings.Split(strings.ToLower(s[0]), ".")
} else {
columns[i] = f
fields[i] = strings.Split(strings.ToLower(f), ".")
}
}
t := table.NewWriter()
t.SetOutputMirror(out)
t.SetStyle(table.Style{
Name: "fuseml",
Format: table.FormatOptions{
Footer: text.FormatUpper,
Header: text.FormatUpper,
Row: text.FormatDefault,
},
Box: table.StyleBoxDefault,
Options: table.Options{
DrawBorder: true,
SeparateColumns: true,
SeparateFooter: true,
SeparateHeader: true,
SeparateRows: false,
},
})
header := table.Row{}
for _, h := range columns {
header = append(header, h)
}
t.AppendHeader(header)
for _, value := range values {
// First, we encode the value as JSON, then decode it back as a generic unstructured
// data into a map. We do this to be able to more easily extract its fields.
// Another nice side-effect of doing the conversion is we don't need to worry about
// upper-case chars in the key names, given that all keys are lower-cased.
var valueMap map[string]interface{}
jsonValue, err := yaml.MarshalWithOptions(value, yaml.JSON())
if err != nil {
fmt.Fprintf(os.Stderr, "Error: failed to encode value %v as JSON: %s", value, err.Error())
}
err = yaml.Unmarshal(jsonValue, &valueMap)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: failed to decode value %s as map: %s", jsonValue, err.Error())
}
row := make([]interface{}, len(o.Fields))
for i, f := range fields {
v := getFieldValue(valueMap, f)
// if a custom formatter is configured for the column, use it
if fmt, hasFmt := o.Formatters[columns[i]]; hasFmt {
v = fmt(value, columns[i], v)
}
if v == nil {
// the table formatter doesn't handle nil values very well
v = ""
}
// if the value is not a scalar, format it using YAML
t := reflect.TypeOf(v)
if t.Kind() == reflect.Array || t.Kind() == reflect.Slice || t.Kind() == reflect.Map {
if j, err := yaml.Marshal(v); err == nil {
v = string(j)
}
}
row[i] = v
}
t.AppendRow(row)
}
t.SortBy(o.SortBy)
if o.Format == FormatCSV {
t.RenderCSV()
} else {
t.Render()
}
}
func (o *FormattingOptions) formatObject(out io.Writer, value interface{}) {
var m []byte
if o.Format == FormatYAML {
fmt.Fprintln(out, "---")
m, _ = yaml.Marshal(value)
} else {
m, _ = yaml.MarshalWithOptions(value, yaml.JSON())
}
fmt.Fprintln(out, string(m))
}
// FormatValue formats any go struct or list of structs that can be converted into JSON,
// according to the configured formatting options.
func (o *FormattingOptions) FormatValue(out io.Writer, value interface{}) {
switch o.Format {
case FormatTable, FormatCSV:
s := reflect.ValueOf(value)
if s.Kind() == reflect.Array || s.Kind() == reflect.Slice {
valueList := make([]interface{}, s.Len())
for i := 0; i < s.Len(); i++ {
valueList[i] = s.Index(i).Interface()
}
o.formatTable(out, valueList)
} else {
o.formatObject(out, value)
}
case FormatYAML, FormatJSON:
o.formatObject(out, value)
}
}
// FormatMapField is a generic formatter for map fields
func FormatMapField(object interface{}, column string, field interface{}) (formated string) {
if m, ok := field.(map[string]interface{}); ok {
values := make([]string, 0)
for k, v := range m {
values = append(values, fmt.Sprintf("%s: %s", k, v))
}
formated += strings.Join(values, "\n")
}
return
}
// FormatSliceField is a generic formatter for slice fields
func FormatSliceField(object interface{}, column string, field interface{}) (formated string) {
if s, ok := field.([]interface{}); ok {
values := make([]string, len(s))
for i, v := range s {
values[i] = fmt.Sprintf("%s", v)
}
formated += strings.Join(values, "\n")
}
return
}