-
Notifications
You must be signed in to change notification settings - Fork 328
/
main.go
466 lines (399 loc) · 11.3 KB
/
main.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
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
package main
import (
"bufio"
"bytes"
"encoding/json"
"flag"
"fmt"
"io"
"os"
"sort"
"strings"
"github.com/fatih/color"
"github.com/mattn/go-colorable"
"github.com/nwidger/jsoncolor"
"github.com/pkg/errors"
)
// Exit codes
const (
exitOK = iota
exitOpenFile
exitReadInput
exitFormStatements
exitFetchURL
exitParseStatements
exitJSONEncode
)
// Option bitfields
const (
optMonochrome = 1 << iota
optNoSort
optJSON
)
// Output colors
var (
strColor = color.New(color.FgYellow)
braceColor = color.New(color.FgMagenta)
bareColor = color.New(color.FgBlue, color.Bold)
numColor = color.New(color.FgRed)
boolColor = color.New(color.FgCyan)
)
// gronVersion stores the current gron version, set at build
// time with the ldflags -X option
var gronVersion = "dev"
func init() {
flag.Usage = func() {
h := "Transform JSON (from a file, URL, or stdin) into discrete assignments to make it greppable\n\n"
h += "Usage:\n"
h += " gron [OPTIONS] [FILE|URL|-]\n\n"
h += "Options:\n"
h += " -u, --ungron Reverse the operation (turn assignments back into JSON)\n"
h += " -v, --values Print just the values of provided assignments\n"
h += " -c, --colorize Colorize output (default on tty)\n"
h += " -m, --monochrome Monochrome (don't colorize output)\n"
h += " -s, --stream Treat each line of input as a separate JSON object\n"
h += " -k, --insecure Disable certificate validation\n"
h += " -j, --json Represent gron data as JSON stream\n"
h += " --no-sort Don't sort output (faster)\n"
h += " --version Print version information\n\n"
h += "Exit Codes:\n"
h += fmt.Sprintf(" %d\t%s\n", exitOK, "OK")
h += fmt.Sprintf(" %d\t%s\n", exitOpenFile, "Failed to open file")
h += fmt.Sprintf(" %d\t%s\n", exitReadInput, "Failed to read input")
h += fmt.Sprintf(" %d\t%s\n", exitFormStatements, "Failed to form statements")
h += fmt.Sprintf(" %d\t%s\n", exitFetchURL, "Failed to fetch URL")
h += fmt.Sprintf(" %d\t%s\n", exitParseStatements, "Failed to parse statements")
h += fmt.Sprintf(" %d\t%s\n", exitJSONEncode, "Failed to encode JSON")
h += "\n"
h += "Examples:\n"
h += " gron /tmp/apiresponse.json\n"
h += " gron http://jsonplaceholder.typicode.com/users/1 \n"
h += " curl -s http://jsonplaceholder.typicode.com/users/1 | gron\n"
h += " gron http://jsonplaceholder.typicode.com/users/1 | grep company | gron --ungron\n"
fmt.Fprintf(os.Stderr, h)
}
}
func main() {
var (
ungronFlag bool
colorizeFlag bool
monochromeFlag bool
streamFlag bool
noSortFlag bool
versionFlag bool
insecureFlag bool
jsonFlag bool
valuesFlag bool
)
flag.BoolVar(&ungronFlag, "ungron", false, "")
flag.BoolVar(&ungronFlag, "u", false, "")
flag.BoolVar(&colorizeFlag, "colorize", false, "")
flag.BoolVar(&colorizeFlag, "c", false, "")
flag.BoolVar(&monochromeFlag, "monochrome", false, "")
flag.BoolVar(&monochromeFlag, "m", false, "")
flag.BoolVar(&streamFlag, "s", false, "")
flag.BoolVar(&streamFlag, "stream", false, "")
flag.BoolVar(&noSortFlag, "no-sort", false, "")
flag.BoolVar(&versionFlag, "version", false, "")
flag.BoolVar(&insecureFlag, "k", false, "")
flag.BoolVar(&insecureFlag, "insecure", false, "")
flag.BoolVar(&jsonFlag, "j", false, "")
flag.BoolVar(&jsonFlag, "json", false, "")
flag.BoolVar(&valuesFlag, "values", false, "")
flag.BoolVar(&valuesFlag, "value", false, "")
flag.BoolVar(&valuesFlag, "v", false, "")
flag.Parse()
// Print version information
if versionFlag {
fmt.Printf("gron version %s\n", gronVersion)
os.Exit(exitOK)
}
// If executed as 'ungron' set the --ungron flag
if strings.HasSuffix(os.Args[0], "ungron") {
ungronFlag = true
}
// Determine what the program's input should be:
// file, HTTP URL or stdin
var rawInput io.Reader
filename := flag.Arg(0)
if filename == "" || filename == "-" {
rawInput = os.Stdin
} else if validURL(filename) {
r, err := getURL(filename, insecureFlag)
if err != nil {
fatal(exitFetchURL, err)
}
rawInput = r
} else {
r, err := os.Open(filename)
if err != nil {
fatal(exitOpenFile, err)
}
rawInput = r
}
var opts int
// The monochrome option should be forced if the output isn't a terminal
// to avoid doing unnecessary work calling the color functions
switch {
case colorizeFlag:
color.NoColor = false
case monochromeFlag || color.NoColor:
opts = opts | optMonochrome
}
if noSortFlag {
opts = opts | optNoSort
}
if jsonFlag {
opts = opts | optJSON
}
// Pick the appropriate action: gron, ungron, gronValues, or gronStream
var a actionFn = gron
if ungronFlag {
a = ungron
} else if valuesFlag {
a = gronValues
} else if streamFlag {
a = gronStream
}
exitCode, err := a(rawInput, colorable.NewColorableStdout(), opts)
if exitCode != exitOK {
fatal(exitCode, err)
}
os.Exit(exitOK)
}
// an actionFn represents a main action of the program, it accepts
// an input, output and a bitfield of options; returning an exit
// code and any error that occurred
type actionFn func(io.Reader, io.Writer, int) (int, error)
// gron is the default action. Given JSON as the input it returns a list
// of assignment statements. Possible options are optNoSort and optMonochrome
func gron(r io.Reader, w io.Writer, opts int) (int, error) {
var err error
var conv statementconv
if opts&optMonochrome > 0 {
conv = statementToString
} else {
conv = statementToColorString
}
ss, err := statementsFromJSON(r, statement{{"json", typBare}})
if err != nil {
goto out
}
// Go's maps do not have well-defined ordering, but we want a consistent
// output for a given input, so we must sort the statements
if opts&optNoSort == 0 {
sort.Sort(ss)
}
for _, s := range ss {
if opts&optJSON > 0 {
s, err = s.jsonify()
if err != nil {
goto out
}
}
fmt.Fprintln(w, conv(s))
}
out:
if err != nil {
return exitFormStatements, fmt.Errorf("failed to form statements: %s", err)
}
return exitOK, nil
}
// gronStream is like the gron action, but it treats the input as one
// JSON object per line. There's a bit of code duplication from the
// gron action, but it'd be fairly messy to combine the two actions
func gronStream(r io.Reader, w io.Writer, opts int) (int, error) {
var err error
errstr := "failed to form statements"
var i int
var sc *bufio.Scanner
var buf []byte
var conv func(s statement) string
if opts&optMonochrome > 0 {
conv = statementToString
} else {
conv = statementToColorString
}
// Helper function to make the prefix statements for each line
makePrefix := func(index int) statement {
return statement{
{"json", typBare},
{"[", typLBrace},
{fmt.Sprintf("%d", index), typNumericKey},
{"]", typRBrace},
}
}
// The first line of output needs to establish that the top-level
// thing is actually an array...
top := statement{
{"json", typBare},
{"=", typEquals},
{"[]", typEmptyArray},
{";", typSemi},
}
if opts&optJSON > 0 {
top, err = top.jsonify()
if err != nil {
goto out
}
}
fmt.Fprintln(w, conv(top))
// Read the input line by line
sc = bufio.NewScanner(r)
buf = make([]byte, 0, 64*1024)
sc.Buffer(buf, 1024*1024)
i = 0
for sc.Scan() {
line := bytes.NewBuffer(sc.Bytes())
var ss statements
ss, err = statementsFromJSON(line, makePrefix(i))
i++
if err != nil {
goto out
}
// Go's maps do not have well-defined ordering, but we want a consistent
// output for a given input, so we must sort the statements
if opts&optNoSort == 0 {
sort.Sort(ss)
}
for _, s := range ss {
if opts&optJSON > 0 {
s, err = s.jsonify()
if err != nil {
goto out
}
}
fmt.Fprintln(w, conv(s))
}
}
if err = sc.Err(); err != nil {
errstr = "error reading multiline input: %s"
}
out:
if err != nil {
return exitFormStatements, fmt.Errorf(errstr+": %s", err)
}
return exitOK, nil
}
// ungron is the reverse of gron. Given assignment statements as input,
// it returns JSON. The only option is optMonochrome
func ungron(r io.Reader, w io.Writer, opts int) (int, error) {
scanner := bufio.NewScanner(r)
var maker statementmaker
// Allow larger internal buffer of the scanner (min: 64KiB ~ max: 1MiB)
scanner.Buffer(make([]byte, 64*1024), 1024*1024)
if opts&optJSON > 0 {
maker = statementFromJSONSpec
} else {
maker = statementFromStringMaker
}
// Make a list of statements from the input
var ss statements
for scanner.Scan() {
s, err := maker(scanner.Text())
if err != nil {
return exitParseStatements, err
}
ss.add(s)
}
if err := scanner.Err(); err != nil {
return exitReadInput, fmt.Errorf("failed to read input statements")
}
// turn the statements into a single merged interface{} type
merged, err := ss.toInterface()
if err != nil {
return exitParseStatements, err
}
// If there's only one top level key and it's "json", make that the top level thing
mergedMap, ok := merged.(map[string]interface{})
if ok {
if len(mergedMap) == 1 {
if _, exists := mergedMap["json"]; exists {
merged = mergedMap["json"]
}
}
}
// Marshal the output into JSON to display to the user
out := &bytes.Buffer{}
enc := json.NewEncoder(out)
enc.SetIndent("", " ")
enc.SetEscapeHTML(false)
err = enc.Encode(merged)
if err != nil {
return exitJSONEncode, errors.Wrap(err, "failed to convert statements to JSON")
}
j := out.Bytes()
// If the output isn't monochrome, add color to the JSON
if opts&optMonochrome == 0 {
c, err := colorizeJSON(j)
// If we failed to colorize the JSON for whatever reason,
// we'll just fall back to monochrome output, otherwise
// replace the monochrome JSON with glorious technicolor
if err == nil {
j = c
}
}
// For whatever reason, the monochrome version of the JSON
// has a trailing newline character, but the colorized version
// does not. Strip the whitespace so that neither has the newline
// character on the end, and then we'll add a newline in the
// Fprintf below
j = bytes.TrimSpace(j)
fmt.Fprintf(w, "%s\n", j)
return exitOK, nil
}
// gronValues prints just the scalar values from some input gron statements
// without any quotes or anything of that sort; a bit like jq -r
// e.g. json[0].user.name = "Sam"; -> Sam
func gronValues(r io.Reader, w io.Writer, opts int) (int, error) {
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
s := statementFromString(scanner.Text())
// strip off the leading 'json' bare key
if s[0].typ == typBare && s[0].text == "json" {
s = s[1:]
}
// strip off the leading dots
if s[0].typ == typDot || s[0].typ == typLBrace {
s = s[1:]
}
for _, t := range s {
switch t.typ {
case typString:
var text string
err := json.Unmarshal([]byte(t.text), &text)
if err != nil {
// just swallow errors and try to continue
continue
}
fmt.Println(text)
case typNumber, typTrue, typFalse, typNull:
fmt.Println(t.text)
default:
// Nothing
}
}
}
return exitOK, nil
}
func colorizeJSON(src []byte) ([]byte, error) {
out := &bytes.Buffer{}
f := jsoncolor.NewFormatter()
f.StringColor = strColor
f.ObjectColor = braceColor
f.ArrayColor = braceColor
f.FieldColor = bareColor
f.NumberColor = numColor
f.TrueColor = boolColor
f.FalseColor = boolColor
f.NullColor = boolColor
err := f.Format(out, src)
if err != nil {
return out.Bytes(), err
}
return out.Bytes(), nil
}
func fatal(code int, err error) {
fmt.Fprintf(os.Stderr, "%s\n", err)
os.Exit(code)
}