Refactors ungron main, updates help and readme

This commit is contained in:
Tom Hudson 2016-06-30 21:02:42 +01:00
parent 4c131fe26c
commit 9a2b7530f5
5 changed files with 161 additions and 34 deletions

View file

@ -79,6 +79,53 @@ The output of `gron` is valid JavaScript:
name: 'Tom' }
```
## ungronning
gron can also turn its output back into JSON:
```
▶ gron testdata/two.json | gron -u
{
"contact": {
"email": "mail@tomnomnom.com",
"twitter": "@TomNomNom"
},
"github": "https://github.com/tomnomnom/",
"likes": [
"code",
"cheese",
"meat"
],
"name": "Tom"
}
```
This means use can use gron with `grep` and other tools to modify JSON:
```
▶ gron testdata/two.json | grep likes | gron --ungron
{
"likes": [
"code",
"cheese",
"meat"
]
}
```
To preserve array keys, arrays are padded with `null` when values are missing:
```
▶ gron testdata/two.json | grep likes | grep -v cheese
json.likes = [];
json.likes[0] = "code";
json.likes[2] = "meat";
▶ gron testdata/two.json | grep likes | grep -v cheese | gron --ungron
{
"likes": [
"code",
null,
"meat"
]
}
```
## Get Help
```
@ -86,20 +133,26 @@ The output of `gron` is valid JavaScript:
Transform JSON (from a file, URL, or stdin) into discrete assignments to make it greppable
Usage:
gron [file|url]
gron [OPTIONS] [FILE|URL|-]
Options:
-u, --ungron Reverse the operation (turn assignments back into JSON)
Exit Codes:
0 OK
1 Failed to open file
2 Failed to read input
3 Failed to decode JSON
4 Failed to from statements
4 Failed to form statements
5 Failed to fetch URL
6 Failed to parse statements
7 Failed to encode JSON
Examples:
gron /tmp/apiresponse.json
gron http://headers.jsontest.com/
curl -s http://headers.jsontest.com/ | gron
gron http://jsonplaceholder.typicode.com/users/1
curl -s http://jsonplaceholder.typicode.com/users/1 | gron
gron http://jsonplaceholder.typicode.com/users/1 | grep company | gron --ungron
```
## FAQ

57
main.go
View file

@ -18,7 +18,8 @@ const (
exitJSONDecode
exitFormStatements
exitFetchURL
exitUnknown
exitParseStatements
exitJSONEncode
)
func init() {
@ -26,28 +27,38 @@ func init() {
h := "Transform JSON (from a file, URL, or stdin) into discrete assignments to make it greppable\n\n"
h += "Usage:\n"
h += " gron [file|url]\n\n"
h += " gron [OPTIONS] [FILE|URL|-]\n\n"
h += "Options:\n"
h += " -u, --ungron\tReverse the operation (turn assignments back into JSON)\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", exitJSONDecode, "Failed to decode JSON")
h += fmt.Sprintf(" %d\t%s\n", exitFormStatements, "Failed to from statements")
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://headers.jsontest.com/ \n"
h += " curl -s http://headers.jsontest.com/ | gron\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)
}
}
var ungronFlag bool
func main() {
ungronFlag := flag.Bool("ungron", false, "Turn statements into JSON instead")
flag.BoolVar(&ungronFlag, "ungron", false, "Turn statements into JSON instead")
flag.BoolVar(&ungronFlag, "u", false, "Turn statements into JSON instead")
flag.Parse()
var raw io.Reader
@ -73,7 +84,7 @@ func main() {
var exitCode int
var err error
if *ungronFlag {
if ungronFlag {
exitCode, err = ungron(raw, os.Stdout)
} else {
exitCode, err = gron(raw, os.Stdout)
@ -121,31 +132,19 @@ func gron(r io.Reader, w io.Writer) (int, error) {
func ungron(r io.Reader, w io.Writer) (int, error) {
scanner := bufio.NewScanner(r)
// Get all the idividually parsed statements
var parsed []interface{}
// Make a list of statements from the input
var ss statements
for scanner.Scan() {
l := newLexer(scanner.Text())
u, err := ungronTokens(l.lex())
if err != nil {
return exitUnknown, fmt.Errorf("failed to translate tokens into datastructure: %s", err)
}
parsed = append(parsed, u)
ss.AddFull(scanner.Text())
}
// TODO: Handle any scanner errors
if len(parsed) == 0 {
return exitUnknown, fmt.Errorf("no statements were parsed")
if err := scanner.Err(); err != nil {
return exitReadInput, fmt.Errorf("failed to read input statements")
}
merged := parsed[0]
for _, p := range parsed[1:] {
m, err := recursiveMerge(merged, p)
if err != nil {
return exitUnknown, fmt.Errorf("failed to merge statements: %s", err)
}
merged = m
// ungron the statements
merged, err := ss.ungron()
if err != nil {
return exitParseStatements, fmt.Errorf("failed to parse input statements")
}
// If there's only one top level key and it's "json", make that the top level thing
@ -160,7 +159,7 @@ func ungron(r io.Reader, w io.Writer) (int, error) {
j, err := json.MarshalIndent(merged, "", " ")
if err != nil {
return exitUnknown, fmt.Errorf("failed to convert statements to JSON: %s", err)
return exitJSONEncode, fmt.Errorf("failed to convert statements to JSON: %s", err)
}
fmt.Fprintf(w, "%s\n", j)

View file

@ -17,6 +17,11 @@ func (ss *statements) Add(prefix, value string) {
*ss = append(*ss, fmt.Sprintf("%s = %s;", prefix, value))
}
// AddFull adds a new statement to the list given the entire statement
func (ss *statements) AddFull(s string) {
*ss = append(*ss, s)
}
// AddMulti adds a whole other list of statements
func (ss *statements) AddMulti(l statements) {
*ss = append(*ss, l...)
@ -32,6 +37,38 @@ func (ss statements) Swap(i, j int) {
ss[i], ss[j] = ss[j], ss[i]
}
// ungron turns statements into a proper datastructur
func (ss statements) ungron() (interface{}, error) {
// Get all the idividually parsed statements
var parsed []interface{}
for _, s := range ss {
l := newLexer(s)
u, err := ungronTokens(l.lex())
if err != nil {
return nil, fmt.Errorf("failed to translate tokens into datastructure: %s", err)
}
parsed = append(parsed, u)
}
if len(parsed) == 0 {
return nil, fmt.Errorf("no statements were parsed")
}
merged := parsed[0]
for _, p := range parsed[1:] {
m, err := recursiveMerge(merged, p)
if err != nil {
return nil, fmt.Errorf("failed to merge statements: %s", err)
}
merged = m
}
return merged, nil
}
// Less compares two statements for sort.Sort
// Implements a natural sort to keep array indexes in order
func (ss statements) Less(a, b int) bool {

View file

@ -2,6 +2,7 @@ package main
import (
"encoding/json"
"reflect"
"sort"
"testing"
)
@ -291,3 +292,40 @@ func BenchmarkMakePrefixInt(b *testing.B) {
_, _ = makePrefix("json", 212)
}
}
func TestUngronStatements(t *testing.T) {
in := statements{
`json.contact = {};`,
`json.contact["e-mail"][0] = "mail@tomnomnom.com";`,
`json.contact["e-mail"][1] = "test@tomnomnom.com";`,
`json.contact["e-mail"][3] = "foo@tomnomnom.com";`,
`json.contact.twitter = "@TomNomNom";`,
}
want := map[string]interface{}{
"json": map[string]interface{}{
"contact": map[string]interface{}{
"e-mail": []interface{}{
0: "mail@tomnomnom.com",
1: "test@tomnomnom.com",
3: "foo@tomnomnom.com",
},
"twitter": "@TomNomNom",
},
},
}
have, err := in.ungron()
if err != nil {
t.Fatalf("want nil error but have: %s", err)
}
t.Logf("Have: %#v", have)
t.Logf("Want: %#v", want)
eq := reflect.DeepEqual(have, want)
if !eq {
t.Errorf("have and want are not equal")
}
}

2
url.go
View file

@ -18,7 +18,7 @@ func getURL(url string) (io.Reader, error) {
if err != nil {
return nil, err
}
req.Header.Set("User-Agent", "gron/0.1")
req.Header.Set("User-Agent", "gron/0.2")
req.Header.Set("Accept", "application/json")
resp, err := client.Do(req)