├── .github └── workflows │ ├── build.yml │ └── release.yml ├── .gitignore ├── .goreleaser.yml ├── .travis.yml ├── ADVANCED.mkd ├── CHANGELOG.mkd ├── CONTRIBUTING.mkd ├── LICENSE ├── README.mkd ├── completions ├── gron.bash └── gron.fish ├── docs ├── images │ ├── bareword.png │ ├── input.png │ ├── key.png │ ├── path.png │ ├── statement.png │ ├── string.png │ ├── unescapedrune.png │ └── value.png └── index.html ├── go.mod ├── go.sum ├── identifier.go ├── identifier_test.go ├── main.go ├── main_test.go ├── original-gron.php ├── script ├── example ├── lint ├── precommit ├── profile ├── release └── test ├── statements.go ├── statements_test.go ├── testdata ├── big.json ├── github.gron ├── github.jgron ├── github.json ├── grep-separators.gron ├── grep-separators.json ├── invalid-type-mismatch.gron ├── invalid-value.gron ├── large-line.gron ├── large-line.json ├── long-stream.gron ├── long-stream.json ├── one.gron ├── one.jgron ├── one.json ├── scalar-stream.gron ├── scalar-stream.jgron ├── scalar-stream.json ├── stream.gron ├── stream.jgron ├── stream.json ├── three.gron ├── three.jgron ├── three.json ├── two-b.json ├── two.gron ├── two.jgron └── two.json ├── token.go ├── token_test.go ├── ungron.go ├── ungron_test.go ├── url.go └── url_test.go /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | workflow_dispatch: 8 | jobs: 9 | 10 | build: 11 | strategy: 12 | matrix: 13 | go-version: ['stable', 'oldstable'] 14 | os: [ubuntu-latest] 15 | runs-on: ${{ matrix.os }} 16 | 17 | steps: 18 | - name: Check out code into the Go module directory 19 | uses: actions/checkout@v4 20 | - name: Install Go 21 | if: success() 22 | uses: actions/setup-go@v5 23 | with: 24 | go-version: ${{ matrix.go-version }} 25 | cache: true 26 | 27 | - name: Run tests 28 | run: go test 29 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | on: 3 | push: 4 | tags: 5 | - "*" 6 | jobs: 7 | goreleaser: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v4 12 | with: 13 | fetch-depth: 0 14 | - name: Setup Go 15 | uses: actions/setup-go@v5 16 | with: 17 | go-version: 'stable' 18 | - name: Run GoReleaser 19 | uses: goreleaser/goreleaser-action@v6 20 | with: 21 | version: latest 22 | args: release --clean 23 | env: 24 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 25 | TAP_GITHUB_TOKEN: ${{ secrets.TAP_GITHUB_TOKEN }} 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | gron 2 | *.tgz 3 | *.zip 4 | *.swp 5 | *.exe 6 | cpu.out 7 | gron.test 8 | vendor 9 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | before: 4 | hooks: 5 | - go mod tidy 6 | - go mod vendor 7 | 8 | source: 9 | enabled: true 10 | files: 11 | - vendor 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - "1.13" 5 | - "1.14" 6 | - "1.15" 7 | - "1.16" 8 | - "1.17" 9 | - "1.18" 10 | - "1.19" 11 | - "1.20" 12 | - "1.21" 13 | - "1.22" 14 | - "1.23" 15 | - tip 16 | -------------------------------------------------------------------------------- /ADVANCED.mkd: -------------------------------------------------------------------------------- 1 | # Advanced Usage 2 | 3 | Although gron's primary purpose is API discovery, when combined with other tools like `grep` it can do some interesting things. 4 | 5 | As an exercise, let's try to mimick some of the examples from the [jq tutorial](https://stedolan.github.io/jq/tutorial/). 6 | 7 | > Disclaimer: munging data on the command line with gron can be useful, but using tools like `grep` and `sed` to manipulate the 8 | > data is error-prone and shouldn't be relied on in scripts. 9 | 10 | Get the last 5 commits from the gron repo: 11 | ``` 12 | ▶ gron "https://api.github.com/repos/tomnomnom/gron/commits?per_page=5" 13 | json = []; 14 | json[0] = {}; 15 | json[0].author = {}; 16 | json[0].author.avatar_url = "https://avatars.githubusercontent.com/u/58276?v=3"; 17 | json[0].author.events_url = "https://api.github.com/users/tomnomnom/events{/privacy}"; 18 | ... 19 | json[4].parents[0].html_url = "https://github.com/tomnomnom/gron/commit/cbcad2299e55c28a9922776e58b2a0b5a0f05016"; 20 | json[4].parents[0].sha = "cbcad2299e55c28a9922776e58b2a0b5a0f05016"; 21 | json[4].parents[0].url = "https://api.github.com/repos/tomnomnom/gron/commits/cbcad2299e55c28a9922776e58b2a0b5a0f05016"; 22 | json[4].sha = "91b204972e63a1166c9d148fbbfd839f8697f91b"; 23 | json[4].url = "https://api.github.com/repos/tomnomnom/gron/commits/91b204972e63a1166c9d148fbbfd839f8697f91b"; 24 | ``` 25 | 26 | To make the rest of this a little more readable, let's add an alias for that: 27 | 28 | ``` 29 | ▶ alias ggh='gron "https://api.github.com/repos/tomnomnom/gron/commits?per_page=5"' 30 | ``` 31 | 32 | Extract just the first commit using `fgrep "json[0]"`: 33 | ``` 34 | ▶ ggh | fgrep "json[0]" 35 | json[0] = {}; 36 | json[0].author = {}; 37 | json[0].author.avatar_url = "https://avatars.githubusercontent.com/u/58276?v=3"; 38 | json[0].author.events_url = "https://api.github.com/users/tomnomnom/events{/privacy}"; 39 | json[0].author.followers_url = "https://api.github.com/users/tomnomnom/followers"; 40 | ... 41 | json[0].parents[0].html_url = "https://github.com/tomnomnom/gron/commit/48aba5325ece087ae24ab72684851cbe77ce8311"; 42 | json[0].parents[0].sha = "48aba5325ece087ae24ab72684851cbe77ce8311"; 43 | json[0].parents[0].url = "https://api.github.com/repos/tomnomnom/gron/commits/48aba5325ece087ae24ab72684851cbe77ce8311"; 44 | json[0].sha = "7da81e29c27241c0a5c2e5d083ddebcfcc525908"; 45 | json[0].url = "https://api.github.com/repos/tomnomnom/gron/commits/7da81e29c27241c0a5c2e5d083ddebcfcc525908"; 46 | ``` 47 | 48 | Get just the committer's name and the commit message using `egrep "(committer.name|commit.message)"`: 49 | ``` 50 | ▶ ggh | fgrep "json[0]" | egrep "(committer.name|commit.message)" 51 | json[0].commit.committer.name = "Tom Hudson"; 52 | json[0].commit.message = "Adds 0.1.7 to changelog"; 53 | ``` 54 | 55 | Turn the result back into JSON using `gron --ungron`: 56 | ``` 57 | ▶ ggh | fgrep "json[0]" | egrep "(committer.name|commit.message)" | gron --ungron 58 | [ 59 | { 60 | "commit": { 61 | "committer": { 62 | "name": "Tom Hudson" 63 | }, 64 | "message": "Adds 0.1.7 to changelog" 65 | } 66 | } 67 | ] 68 | ``` 69 | 70 | gron preserves the location of values in the JSON, but you can use `sed` to remove keys from the path: 71 | ``` 72 | ▶ ggh | fgrep "json[0]" | egrep "(committer.name|commit.message)" | sed -r "s/(commit|committer)\.//g" 73 | json[0].name = "Tom Hudson"; 74 | json[0].message = "Adds 0.1.7 to changelog" 75 | 76 | ``` 77 | 78 | With those keys removed, the result is a 'flattened' object, which looks much cleaner when turned 79 | back into JSON with `gron --ungron`: 80 | 81 | ``` 82 | ▶ ggh | fgrep "json[0]" | egrep "(committer.name|commit.message)" | sed -r "s/(commit|committer)\.//g" | gron --ungron 83 | [ 84 | { 85 | "message": "Adds 0.1.7 to changelog", 86 | "name": "Tom Hudson" 87 | } 88 | ] 89 | ``` 90 | 91 | Removing the `fgrep "json[0]"` from the pipeline means we do the same for all commits: 92 | ``` 93 | ▶ ggh | egrep "(committer.name|commit.message)" | sed -r "s/(commit|committer)\.//g" | gron --ungron 94 | [ 95 | { 96 | "message": "Adds 0.1.7 to changelog", 97 | "name": "Tom Hudson" 98 | }, 99 | { 100 | "message": "Refactors natural sort to actually work + be more readable", 101 | "name": "Tom Hudson" 102 | }, 103 | ... 104 | ``` 105 | 106 | To include the `html_url` key for each commit's parents, all we need to do is add `parents.*html_url` into our call to `egrep`: 107 | ``` 108 | ▶ ggh | egrep "(committer.name|commit.message|parents.*html_url)" | sed -r "s/(commit|committer)\.//g" 109 | json[0].name = "Tom Hudson"; 110 | json[0].message = "Adds 0.1.7 to changelog"; 111 | json[0].parents[0].html_url = "https://github.com/tomnomnom/gron/commit/48aba5325ece087ae24ab72684851cbe77ce8311"; 112 | json[1].name = "Tom Hudson"; 113 | json[1].message = "Refactors natural sort to actually work + be more readable"; 114 | json[1].parents[0].html_url = "https://github.com/tomnomnom/gron/commit/3eca8bf5e07151f077cebf0d942c1fa8bc51e8f2"; 115 | ... 116 | ``` 117 | 118 | To make the structure more like that in the final example in the `jq` tutorial, we can use `sed -r "s/\.html_url//"` to remove the `.html_url` part of the path: 119 | ``` 120 | ▶ ggh | egrep "(committer.name|commit.message|parents.*html_url)" | sed -r "s/(commit|committer)\.//g" | sed -r "s/\.html_url//" 121 | json[0].name = "Tom Hudson"; 122 | json[0].message = "Adds 0.1.7 to changelog"; 123 | json[0].parents[0] = "https://github.com/tomnomnom/gron/commit/48aba5325ece087ae24ab72684851cbe77ce8311"; 124 | json[1].name = "Tom Hudson"; 125 | json[1].message = "Refactors natural sort to actually work + be more readable"; 126 | json[1].parents[0] = "https://github.com/tomnomnom/gron/commit/3eca8bf5e07151f077cebf0d942c1fa8bc51e8f2"; 127 | ... 128 | ``` 129 | 130 | And, of course, the statements can be turned back into JSON with `gron --ungron`: 131 | ``` 132 | ▶ ggh | egrep "(committer.name|commit.message|parents.*html_url)" | sed -r "s/(commit|committer)\.//g" | sed -r "s/\.html_url//" | gron --ungron 133 | [ 134 | { 135 | "message": "Adds 0.1.7 to changelog", 136 | "name": "Tom Hudson", 137 | "parents": [ 138 | "https://github.com/tomnomnom/gron/commit/48aba5325ece087ae24ab72684851cbe77ce8311" 139 | ] 140 | }, 141 | { 142 | "message": "Refactors natural sort to actually work + be more readable", 143 | "name": "Tom Hudson", 144 | "parents": [ 145 | "https://github.com/tomnomnom/gron/commit/3eca8bf5e07151f077cebf0d942c1fa8bc51e8f2" 146 | ] 147 | }, 148 | ... 149 | ``` 150 | -------------------------------------------------------------------------------- /CHANGELOG.mkd: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.6.0 4 | - Adds `--json`/JSON stream output support (thanks @csabahenk!) 5 | - Removes trailing newline character for monochrome output (issue #43) 6 | 7 | ## 0.5.2 8 | - Built with Go 1.10 to fix issue #32 - Thanks @VladimirAlexiev and @joekyo! 9 | 10 | ## 0.5.1 11 | - Fixes bug where empty identifiers would be treated as bare words (thanks for the report, @waltertross!) 12 | 13 | ## 0.5.0 14 | - Adds `-k`/`--insecure` to disable validation of certificates when fetching URLs (thanks @jagsta!) 15 | 16 | ## 0.4.0 17 | - Adds `-c`/`--colorize` to force colorization of output (thanks @orivej!) 18 | - Adds `-s`/`--stream` option to read one JSON object per line 19 | - Native string quoting (performance improvement) 20 | - Fixes bug with strings ending in a double-slash (issue #25) 21 | 22 | ## 0.3.7 23 | - HTML characters (`<`, `>` etc) are no-longer escaped in gron output (issue #22) 24 | 25 | ## 0.3.6 26 | - Fixes bug where invalid statements were outputted 27 | 28 | ## 0.3.5 29 | - General performance improvements; 5 to 17 times faster (see issue #21 for details) 30 | 31 | ## 0.3.4 32 | - Speed improvements when using `--monochrome` 33 | - Adds `--no-sort` option 34 | 35 | ## 0.3.3 36 | - Slightly improved error reporting when ungronning 37 | - 20 second timeout on HTTP(s) requests (thanks @gummiboll!) 38 | - Version information added at build time + `--version` option (issue #19) 39 | 40 | ## 0.3.2 41 | - Adds handling of `--` lines produced by grep -A etc (issue #15) 42 | 43 | ## 0.3.1 44 | - Built with Go 1.7! 45 | - Up to 25% faster 46 | - 40% smaller binaries 47 | 48 | ## 0.3.0 49 | - Adds colorized gron output 50 | - Fixes formatting of large ints in ungron output (issue #12) 51 | 52 | ## 0.2.9 53 | - Adds colorized ungron output (thanks @nwidger!) 54 | - Adds 32 bit binaries to releases 55 | 56 | ## 0.2.8 57 | - Adds freebsd release binaries 58 | 59 | ## 0.2.7 60 | - Fixes bad handling of escape sequences when ungronning - but properly this time (issue #7) 61 | 62 | ## 0.2.5 63 | - Fixes bad handling of escape sequences when ungronning (issue #7) 64 | 65 | ## 0.2.4 66 | - Fixes handling of large integers (issue #6) 67 | 68 | ## 0.2.3 69 | - Switches Windows binary packaging to zip instead of tgz 70 | 71 | ## 0.2.2 72 | - Tweaks release automation, no user-facing changes 73 | 74 | ## 0.2.1 75 | - Adds windows binary 76 | 77 | ## 0.2.0 78 | - Adds [ungronning](README.mkd#ungronning)! 79 | 80 | ## 0.1.7 81 | - Fixes sorting of array keys; now uses natural sort 82 | 83 | ## 0.1.6 84 | - Adds proper handling of key quoting using Unicode ranges 85 | - Adds basic benchmarks 86 | - Adds profiling script 87 | 88 | ## 0.1.5 89 | - Adds scripted builds for darwin on amd64 90 | 91 | ## 0.1.4 92 | - Minor changes to release script 93 | 94 | ## 0.1.3 95 | - Releases are now tarballs 96 | 97 | ## 0.1.2 98 | - Underscores no-longer cause keys to be quoted 99 | - HTTP requests are now done with `Accept: application/json` 100 | - HTTP requests are now done with `User-Agent: gron/0.1` 101 | 102 | ## 0.1.1 103 | - Adds support for fetching URLs directly 104 | 105 | ## 0.1.0 106 | - Support for files 107 | - Support for `stdin` 108 | 109 | -------------------------------------------------------------------------------- /CONTRIBUTING.mkd: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | * Raise an issue if appropriate 4 | * Fork the repo 5 | * Make your changes 6 | * Use [gofmt](https://golang.org/cmd/gofmt/) 7 | * Make sure the tests pass (run `./script/test`) 8 | * Make sure the linters pass (run `./script/lint`) 9 | * Issue a pull request 10 | 11 | ## Commit Messages 12 | I'd prefer it if commit messages describe what the *commit does*, not what *you did*. 13 | 14 | For example, I'd prefer: 15 | 16 | ``` 17 | Adds lint; removes fluff 18 | ``` 19 | 20 | Over: 21 | 22 | ``` 23 | Added lint; removed fluff 24 | ``` 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Tom Hudson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.mkd: -------------------------------------------------------------------------------- 1 | # gron 2 | [![Build Status](https://travis-ci.org/tomnomnom/gron.svg?branch=master)](https://travis-ci.org/tomnomnom/gron) 3 | 4 | Make JSON greppable! 5 | 6 | gron transforms JSON into discrete assignments to make it easier to `grep` for what you want and see the absolute 'path' to it. 7 | It eases the exploration of APIs that return large blobs of JSON but have terrible documentation. 8 | 9 |
 10 | ▶ gron "https://api.github.com/repos/tomnomnom/gron/commits?per_page=1" | fgrep "commit.author"
 11 | json[0].commit.author = {};
 12 | json[0].commit.author.date = "2016-07-02T10:51:21Z";
 13 | json[0].commit.author.email = "mail@tomnomnom.com";
 14 | json[0].commit.author.name = "Tom Hudson";
 15 | 
16 | 17 | gron can work backwards too, enabling you to turn your filtered data back into JSON: 18 |
 19 | ▶ gron "https://api.github.com/repos/tomnomnom/gron/commits?per_page=1" | fgrep "commit.author" | gron --ungron
 20 | [
 21 |   {
 22 |     "commit": {
 23 |       "author": {
 24 |         "date": "2016-07-02T10:51:21Z",
 25 |         "email": "mail@tomnomnom.com",
 26 |         "name": "Tom Hudson"
 27 |       }
 28 |     }
 29 |   }
 30 | ]
 31 | 
32 | 33 | > Disclaimer: the GitHub API has fantastic documentation, but it makes for a good example. 34 | 35 | ## Installation 36 | 37 | gron has no runtime dependencies. You can just [download a binary for Linux, Mac, Windows or FreeBSD and run it](https://github.com/tomnomnom/gron/releases). 38 | Put the binary in your `$PATH` (e.g. in `/usr/local/bin`) to make it easy to use: 39 | ``` 40 | ▶ tar xzf gron-linux-amd64-0.1.5.tgz 41 | ▶ sudo mv gron /usr/local/bin/ 42 | ``` 43 | 44 | If you're a Mac user you can also [install gron via brew](http://braumeister.org/formula/gron): 45 | ``` 46 | ▶ brew install gron 47 | ``` 48 | 49 | Or if you're a Go user you can use `go install`: 50 | 51 | ``` 52 | ▶ go install github.com/tomnomnom/gron@latest 53 | ``` 54 | 55 | It's recommended that you alias `ungron` or `norg` (or both!) to `gron --ungron`. Put something like this in your shell profile (e.g. in `~/.bashrc`): 56 | ``` 57 | alias norg="gron --ungron" 58 | alias ungron="gron --ungron" 59 | ``` 60 | Or you could create a shell script in your $PATH named `ungron` or `norg` to affect all users: 61 | ``` 62 | gron --ungron "$@" 63 | ``` 64 | 65 | ## Usage 66 | 67 | Get JSON from a file: 68 | 69 | ``` 70 | ▶ gron testdata/two.json 71 | json = {}; 72 | json.contact = {}; 73 | json.contact.email = "mail@tomnomnom.com"; 74 | json.contact.twitter = "@TomNomNom"; 75 | json.github = "https://github.com/tomnomnom/"; 76 | json.likes = []; 77 | json.likes[0] = "code"; 78 | json.likes[1] = "cheese"; 79 | json.likes[2] = "meat"; 80 | json.name = "Tom"; 81 | ``` 82 | 83 | From a URL: 84 | 85 | ``` 86 | ▶ gron http://headers.jsontest.com/ 87 | json = {}; 88 | json.Host = "headers.jsontest.com"; 89 | json["User-Agent"] = "gron/0.1"; 90 | json["X-Cloud-Trace-Context"] = "6917a823919477919dbc1523584ba25d/11970839830843610056"; 91 | ``` 92 | 93 | Or from `stdin`: 94 | 95 | ``` 96 | ▶ curl -s http://headers.jsontest.com/ | gron 97 | json = {}; 98 | json.Accept = "*/*"; 99 | json.Host = "headers.jsontest.com"; 100 | json["User-Agent"] = "curl/7.43.0"; 101 | json["X-Cloud-Trace-Context"] = "c70f7bf26661c67d0b9f2cde6f295319/13941186890243645147"; 102 | ``` 103 | 104 | Grep for something and easily see the path to it: 105 | 106 | ``` 107 | ▶ gron testdata/two.json | grep twitter 108 | json.contact.twitter = "@TomNomNom"; 109 | ``` 110 | 111 | gron makes diffing JSON easy too: 112 | 113 | ``` 114 | ▶ diff <(gron two.json) <(gron two-b.json) 115 | 3c3 116 | < json.contact.email = "mail@tomnomnom.com"; 117 | --- 118 | > json.contact.email = "contact@tomnomnom.com"; 119 | ``` 120 | 121 | The output of `gron` is valid JavaScript: 122 | 123 | ``` 124 | ▶ gron testdata/two.json > tmp.js 125 | ▶ echo "console.log(json);" >> tmp.js 126 | ▶ nodejs tmp.js 127 | { contact: { email: 'mail@tomnomnom.com', twitter: '@TomNomNom' }, 128 | github: 'https://github.com/tomnomnom/', 129 | likes: [ 'code', 'cheese', 'meat' ], 130 | name: 'Tom' } 131 | ``` 132 | 133 | It's also possible to obtain the `gron` output as JSON stream via 134 | the `--json` switch: 135 | 136 | ``` 137 | ▶ curl -s http://headers.jsontest.com/ | gron --json 138 | [[],{}] 139 | [["Accept"],"*/*"] 140 | [["Host"],"headers.jsontest.com"] 141 | [["User-Agent"],"curl/7.43.0"] 142 | [["X-Cloud-Trace-Context"],"c70f7bf26661c67d0b9f2cde6f295319/13941186890243645147"] 143 | ``` 144 | 145 | ## ungronning 146 | gron can also turn its output back into JSON: 147 | ``` 148 | ▶ gron testdata/two.json | gron -u 149 | { 150 | "contact": { 151 | "email": "mail@tomnomnom.com", 152 | "twitter": "@TomNomNom" 153 | }, 154 | "github": "https://github.com/tomnomnom/", 155 | "likes": [ 156 | "code", 157 | "cheese", 158 | "meat" 159 | ], 160 | "name": "Tom" 161 | } 162 | ``` 163 | 164 | This means you use can use gron with `grep` and other tools to modify JSON: 165 | ``` 166 | ▶ gron testdata/two.json | grep likes | gron --ungron 167 | { 168 | "likes": [ 169 | "code", 170 | "cheese", 171 | "meat" 172 | ] 173 | } 174 | ``` 175 | 176 | or 177 | 178 | 179 | ``` 180 | ▶ gron --json testdata/two.json | grep likes | gron --json --ungron 181 | { 182 | "likes": [ 183 | "code", 184 | "cheese", 185 | "meat" 186 | ] 187 | } 188 | ``` 189 | 190 | To preserve array keys, arrays are padded with `null` when values are missing: 191 | ``` 192 | ▶ gron testdata/two.json | grep likes | grep -v cheese 193 | json.likes = []; 194 | json.likes[0] = "code"; 195 | json.likes[2] = "meat"; 196 | ▶ gron testdata/two.json | grep likes | grep -v cheese | gron --ungron 197 | { 198 | "likes": [ 199 | "code", 200 | null, 201 | "meat" 202 | ] 203 | } 204 | ``` 205 | 206 | If you get creative you can do [some pretty neat tricks with gron](ADVANCED.mkd), and 207 | then ungron the output back into JSON. 208 | 209 | ## Get Help 210 | 211 | ``` 212 | ▶ gron --help 213 | Transform JSON (from a file, URL, or stdin) into discrete assignments to make it greppable 214 | 215 | Usage: 216 | gron [OPTIONS] [FILE|URL|-] 217 | 218 | Options: 219 | -u, --ungron Reverse the operation (turn assignments back into JSON) 220 | -v, --values Print just the values of provided assignments 221 | -c, --colorize Colorize output (default on tty) 222 | -m, --monochrome Monochrome (don't colorize output) 223 | -s, --stream Treat each line of input as a separate JSON object 224 | -k, --insecure Disable certificate validation 225 | -j, --json Represent gron data as JSON stream 226 | --no-sort Don't sort output (faster) 227 | --version Print version information 228 | 229 | Exit Codes: 230 | 0 OK 231 | 1 Failed to open file 232 | 2 Failed to read input 233 | 3 Failed to form statements 234 | 4 Failed to fetch URL 235 | 5 Failed to parse statements 236 | 6 Failed to encode JSON 237 | 238 | Examples: 239 | gron /tmp/apiresponse.json 240 | gron http://jsonplaceholder.typicode.com/users/1 241 | curl -s http://jsonplaceholder.typicode.com/users/1 | gron 242 | gron http://jsonplaceholder.typicode.com/users/1 | grep company | gron --ungron 243 | ``` 244 | 245 | ## FAQ 246 | ### Wasn't this written in PHP before? 247 | Yes it was! The original version is [preserved here for posterity](https://github.com/tomnomnom/gron/blob/master/original-gron.php). 248 | 249 | ### Why the change to Go? 250 | Mostly to remove PHP as a dependency. There's a lot of people who work with JSON who don't have PHP installed. 251 | 252 | ### Why shouldn't I just use jq? 253 | [jq](https://stedolan.github.io/jq/) is *awesome*, and a lot more powerful than gron, but with that power comes 254 | complexity. gron aims to make it easier to use the tools you already know, like `grep` and `sed`. 255 | 256 | gron's primary purpose is to make it easy to find the path to a value in a deeply nested JSON blob 257 | when you don't already know the structure; much of jq's power is unlocked only once you know that structure. 258 | -------------------------------------------------------------------------------- /completions/gron.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Bash shell commandline completions for gron. 4 | # 5 | # Copy the contents of this file into your ~/.bashrc file or whatever 6 | # file you use for Bash completions. 7 | # 8 | # Example: cat ./completions/gron.bash >> ~/.bashrc 9 | 10 | function _gron_completion { 11 | local AVAILABLE_COMMANDS="--colorize --insecure --json --monochrome --no-sort --stream --ungron --values --version" 12 | COMPREPLY=() 13 | 14 | local CURRENT_WORD=${COMP_WORDS[COMP_CWORD]} 15 | COMPREPLY=($(compgen -W "$AVAILABLE_COMMANDS" -- "$CURRENT_WORD")) 16 | } 17 | 18 | complete -F _gron_completion gron 19 | -------------------------------------------------------------------------------- /completions/gron.fish: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env fish 2 | 3 | # Fish shell commandline completions for gron. 4 | # 5 | # Stick this file in your ~/.config/fish/completions/ directory. 6 | 7 | complete -c gron -s u -l ungron --description "Reverse the operation (turn assignments back into JSON)" 8 | complete -c gron -s c -l colorize --description "Colorize output (default on tty)" 9 | complete -c gron -s m -l monochrome --description "Monochrome (don't colorize output)" 10 | complete -c gron -s s -l stream --description "Treat each line of input as a separate JSON object" 11 | complete -c gron -s k -l insecure --description "Disable certificate validation" 12 | complete -c gron -s j -l json --description "Represent gron data as JSON stream" 13 | complete -c gron -l no-sort --description "Don't sort output (faster)" 14 | complete -c gron -l version --description "Print version information" 15 | 16 | # eof 17 | -------------------------------------------------------------------------------- /docs/images/bareword.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomnomnom/gron/88a6234ea2d0c487090988182ad9a7cdf6def924/docs/images/bareword.png -------------------------------------------------------------------------------- /docs/images/input.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomnomnom/gron/88a6234ea2d0c487090988182ad9a7cdf6def924/docs/images/input.png -------------------------------------------------------------------------------- /docs/images/key.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomnomnom/gron/88a6234ea2d0c487090988182ad9a7cdf6def924/docs/images/key.png -------------------------------------------------------------------------------- /docs/images/path.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomnomnom/gron/88a6234ea2d0c487090988182ad9a7cdf6def924/docs/images/path.png -------------------------------------------------------------------------------- /docs/images/statement.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomnomnom/gron/88a6234ea2d0c487090988182ad9a7cdf6def924/docs/images/statement.png -------------------------------------------------------------------------------- /docs/images/string.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomnomnom/gron/88a6234ea2d0c487090988182ad9a7cdf6def924/docs/images/string.png -------------------------------------------------------------------------------- /docs/images/unescapedrune.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomnomnom/gron/88a6234ea2d0c487090988182ad9a7cdf6def924/docs/images/unescapedrune.png -------------------------------------------------------------------------------- /docs/images/value.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomnomnom/gron/88a6234ea2d0c487090988182ad9a7cdf6def924/docs/images/value.png -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | gron 5 | 6 | 7 | 8 |

gron

9 |

Ungron Input Grammar

10 | 11 |
12 | Input ::= '--'* Statement (Statement | '--')*
13 | Statement ::= Path Space* "=" Space* Value ";" "\n"
14 | Path ::= (BareWord) ("." BareWord | ("[" Key "]"))*
15 | Value ::= String | Number | "true" | "false" | "null" | "[]" | "{}"
16 | BareWord ::= (UnicodeLu | UnicodeLl | UnicodeLm | UnicodeLo | UnicodeNl | '$' | '_') (UnicodeLu | UnicodeLl | UnicodeLm | UnicodeLo | UnicodeNl | UnicodeMn | UnicodeMc | UnicodeNd | UnicodePc | '$' | '_')*
17 | Key ::= [0-9]+ | String
18 | String ::= '"' (UnescapedRune | ("\" (["\/bfnrt] | ('u' Hex))))* '"'
19 | UnescapedRune ::= [^#x0-#x1f"\]
20 |     
21 | 22 |

Input

23 | 24 | 25 |

Statement

26 | 27 | 28 |

Path

29 | 30 | 31 |

Value

32 | 33 | 34 |

BareWord

35 | 36 | 37 |

Key

38 | 39 | 40 |

String

41 | 42 | 43 |

UnescapedRune

44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tomnomnom/gron 2 | 3 | go 1.24 4 | 5 | require ( 6 | github.com/fatih/color v1.18.0 7 | github.com/mattn/go-colorable v0.1.14 8 | github.com/nwidger/jsoncolor v0.3.2 9 | github.com/pkg/errors v0.9.1 10 | ) 11 | 12 | require ( 13 | github.com/mattn/go-isatty v0.0.20 // indirect 14 | golang.org/x/sys v0.33.0 // indirect 15 | ) 16 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= 2 | github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= 3 | github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= 4 | github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 5 | github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= 6 | github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= 7 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 8 | github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= 9 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 10 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 11 | github.com/nwidger/jsoncolor v0.3.2 h1:rVJJlwAWDJShnbTYOQ5RM7yTA20INyKXlJ/fg4JMhHQ= 12 | github.com/nwidger/jsoncolor v0.3.2/go.mod h1:Cs34umxLbJvgBMnVNVqhji9BhoT/N/KinHqZptQ7cf4= 13 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 14 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 15 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 16 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 17 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 18 | golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= 19 | golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 20 | -------------------------------------------------------------------------------- /identifier.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "unicode" 4 | 5 | // The javascript reserved words cannot be used as unquoted keys 6 | var reservedWords = map[string]bool{ 7 | "break": true, 8 | "case": true, 9 | "catch": true, 10 | "class": true, 11 | "const": true, 12 | "continue": true, 13 | "debugger": true, 14 | "default": true, 15 | "delete": true, 16 | "do": true, 17 | "else": true, 18 | "export": true, 19 | "extends": true, 20 | "false": true, 21 | "finally": true, 22 | "for": true, 23 | "function": true, 24 | "if": true, 25 | "import": true, 26 | "in": true, 27 | "instanceof": true, 28 | "new": true, 29 | "null": true, 30 | "return": true, 31 | "super": true, 32 | "switch": true, 33 | "this": true, 34 | "throw": true, 35 | "true": true, 36 | "try": true, 37 | "typeof": true, 38 | "var": true, 39 | "void": true, 40 | "while": true, 41 | "with": true, 42 | "yield": true, 43 | } 44 | 45 | // validIdentifier checks to see if a string is a valid 46 | // JavaScript identifier 47 | // E.g: 48 | // 49 | // justLettersAndNumbers1 -> true 50 | // a key with spaces -> false 51 | // 1startsWithANumber -> false 52 | func validIdentifier(s string) bool { 53 | if reservedWords[s] || s == "" { 54 | return false 55 | } 56 | 57 | for i, r := range s { 58 | if i == 0 && !validFirstRune(r) { 59 | return false 60 | } 61 | if i != 0 && !validSecondaryRune(r) { 62 | return false 63 | } 64 | } 65 | 66 | return true 67 | } 68 | 69 | // validFirstRune returns true for runes that are valid 70 | // as the first rune in an identifier. 71 | // E.g: 72 | // 73 | // 'r' -> true 74 | // '7' -> false 75 | func validFirstRune(r rune) bool { 76 | return unicode.In(r, 77 | unicode.Lu, 78 | unicode.Ll, 79 | unicode.Lm, 80 | unicode.Lo, 81 | unicode.Nl, 82 | ) || r == '$' || r == '_' 83 | } 84 | 85 | // validSecondaryRune returns true for runes that are valid 86 | // as anything other than the first rune in an identifier. 87 | func validSecondaryRune(r rune) bool { 88 | return validFirstRune(r) || 89 | unicode.In(r, unicode.Mn, unicode.Mc, unicode.Nd, unicode.Pc) 90 | } 91 | -------------------------------------------------------------------------------- /identifier_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "testing" 4 | 5 | func TestValidIdentifier(t *testing.T) { 6 | tests := []struct { 7 | key string 8 | want bool 9 | }{ 10 | // Valid Identifiers 11 | {"dotted", true}, 12 | {"dotted123", true}, 13 | {"_under_scores", true}, 14 | {"ಠ_ಠ", true}, 15 | 16 | // Invalid chars 17 | {"is-quoted", false}, 18 | {"Definitely quoted!", false}, 19 | 20 | // Reserved words 21 | {"true", false}, 22 | {"else", false}, 23 | {"null", false}, 24 | 25 | // Empty string 26 | {"", false}, 27 | } 28 | 29 | for _, test := range tests { 30 | have := validIdentifier(test.key) 31 | if have != test.want { 32 | t.Errorf("Want %t for validIdentifier(%s); have %t", test.want, test.key, have) 33 | } 34 | } 35 | } 36 | 37 | func TestValidFirstRune(t *testing.T) { 38 | tests := []struct { 39 | in rune 40 | want bool 41 | }{ 42 | {'r', true}, 43 | {'ಠ', true}, 44 | {'4', false}, 45 | {'-', false}, 46 | } 47 | 48 | for _, test := range tests { 49 | have := validFirstRune(test.in) 50 | if have != test.want { 51 | t.Errorf("Want %t for validFirstRune(%#U); have %t", test.want, test.in, have) 52 | } 53 | } 54 | } 55 | 56 | func TestValidSecondaryRune(t *testing.T) { 57 | tests := []struct { 58 | in rune 59 | want bool 60 | }{ 61 | {'r', true}, 62 | {'ಠ', true}, 63 | {'4', true}, 64 | {'-', false}, 65 | } 66 | 67 | for _, test := range tests { 68 | have := validSecondaryRune(test.in) 69 | if have != test.want { 70 | t.Errorf("Want %t for validSecondaryRune(%#U); have %t", test.want, test.in, have) 71 | } 72 | } 73 | } 74 | 75 | func BenchmarkValidIdentifier(b *testing.B) { 76 | for i := 0; i < b.N; i++ { 77 | validIdentifier("must-be-quoted") 78 | } 79 | } 80 | 81 | func BenchmarkValidIdentifierUnquoted(b *testing.B) { 82 | for i := 0; i < b.N; i++ { 83 | validIdentifier("canbeunquoted") 84 | } 85 | } 86 | 87 | func BenchmarkValidIdentifierReserved(b *testing.B) { 88 | for i := 0; i < b.N; i++ { 89 | validIdentifier("function") 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "encoding/json" 7 | "flag" 8 | "fmt" 9 | "io" 10 | "os" 11 | "sort" 12 | "strings" 13 | 14 | "github.com/fatih/color" 15 | "github.com/mattn/go-colorable" 16 | "github.com/nwidger/jsoncolor" 17 | "github.com/pkg/errors" 18 | ) 19 | 20 | // Exit codes 21 | const ( 22 | exitOK = iota 23 | exitOpenFile 24 | exitReadInput 25 | exitFormStatements 26 | exitFetchURL 27 | exitParseStatements 28 | exitJSONEncode 29 | ) 30 | 31 | // Option bitfields 32 | const ( 33 | optMonochrome = 1 << iota 34 | optNoSort 35 | optJSON 36 | ) 37 | 38 | // Output colors 39 | var ( 40 | strColor = color.New(color.FgYellow) 41 | braceColor = color.New(color.FgMagenta) 42 | bareColor = color.New(color.FgBlue, color.Bold) 43 | numColor = color.New(color.FgRed) 44 | boolColor = color.New(color.FgCyan) 45 | ) 46 | 47 | // gronVersion stores the current gron version, set at build 48 | // time with the ldflags -X option 49 | var gronVersion = "dev" 50 | 51 | // Default value that is set if proxy/noproxy variables is not specified. 52 | // Cannot be an empty string, because an empty string explicitly deactivates the 53 | // proxy. 54 | var undefinedProxy = "-" 55 | 56 | func init() { 57 | flag.Usage = func() { 58 | h := "Transform JSON (from a file, URL, or stdin) into discrete assignments to make it greppable\n\n" 59 | 60 | h += "Usage:\n" 61 | h += " gron [OPTIONS] [FILE|URL|-]\n\n" 62 | 63 | h += "Options:\n" 64 | h += " -u, --ungron Reverse the operation (turn assignments back into JSON)\n" 65 | h += " -v, --values Print just the values of provided assignments\n" 66 | h += " -c, --colorize Colorize output (default on tty)\n" 67 | h += " -m, --monochrome Monochrome (don't colorize output)\n" 68 | h += " -s, --stream Treat each line of input as a separate JSON object\n" 69 | h += " -k, --insecure Disable certificate validation\n" 70 | h += " -x, --proxy Set proxy configuration\n" 71 | h += " --noproxy Comma-separated list of hosts for which not to use a proxy, if one is specified.\n" 72 | h += " -j, --json Represent gron data as JSON stream\n" 73 | h += " --no-sort Don't sort output (faster)\n" 74 | h += " --version Print version information\n\n" 75 | 76 | h += "Exit Codes:\n" 77 | h += fmt.Sprintf(" %d\t%s\n", exitOK, "OK") 78 | h += fmt.Sprintf(" %d\t%s\n", exitOpenFile, "Failed to open file") 79 | h += fmt.Sprintf(" %d\t%s\n", exitReadInput, "Failed to read input") 80 | h += fmt.Sprintf(" %d\t%s\n", exitFormStatements, "Failed to form statements") 81 | h += fmt.Sprintf(" %d\t%s\n", exitFetchURL, "Failed to fetch URL") 82 | h += fmt.Sprintf(" %d\t%s\n", exitParseStatements, "Failed to parse statements") 83 | h += fmt.Sprintf(" %d\t%s\n", exitJSONEncode, "Failed to encode JSON") 84 | h += "\n" 85 | 86 | h += "Examples:\n" 87 | h += " gron /tmp/apiresponse.json\n" 88 | h += " gron http://jsonplaceholder.typicode.com/users/1 \n" 89 | h += " curl -s http://jsonplaceholder.typicode.com/users/1 | gron\n" 90 | h += " gron http://jsonplaceholder.typicode.com/users/1 | grep company | gron --ungron\n" 91 | 92 | fmt.Fprint(os.Stderr, h) 93 | } 94 | } 95 | 96 | func main() { 97 | var ( 98 | ungronFlag bool 99 | colorizeFlag bool 100 | monochromeFlag bool 101 | streamFlag bool 102 | noSortFlag bool 103 | versionFlag bool 104 | insecureFlag bool 105 | jsonFlag bool 106 | valuesFlag bool 107 | proxyURL string 108 | noProxy string 109 | ) 110 | 111 | flag.BoolVar(&ungronFlag, "ungron", false, "") 112 | flag.BoolVar(&ungronFlag, "u", false, "") 113 | flag.BoolVar(&colorizeFlag, "colorize", false, "") 114 | flag.BoolVar(&colorizeFlag, "c", false, "") 115 | flag.BoolVar(&monochromeFlag, "monochrome", false, "") 116 | flag.BoolVar(&monochromeFlag, "m", false, "") 117 | flag.BoolVar(&streamFlag, "s", false, "") 118 | flag.BoolVar(&streamFlag, "stream", false, "") 119 | flag.BoolVar(&noSortFlag, "no-sort", false, "") 120 | flag.BoolVar(&versionFlag, "version", false, "") 121 | flag.BoolVar(&insecureFlag, "k", false, "") 122 | flag.BoolVar(&insecureFlag, "insecure", false, "") 123 | flag.BoolVar(&jsonFlag, "j", false, "") 124 | flag.BoolVar(&jsonFlag, "json", false, "") 125 | flag.BoolVar(&valuesFlag, "values", false, "") 126 | flag.BoolVar(&valuesFlag, "value", false, "") 127 | flag.BoolVar(&valuesFlag, "v", false, "") 128 | flag.StringVar(&proxyURL, "x", undefinedProxy, "") 129 | flag.StringVar(&proxyURL, "proxy", undefinedProxy, "") 130 | flag.StringVar(&noProxy, "noproxy", undefinedProxy, "") 131 | 132 | flag.Parse() 133 | 134 | // Print version information 135 | if versionFlag { 136 | fmt.Printf("gron version %s\n", gronVersion) 137 | os.Exit(exitOK) 138 | } 139 | 140 | // If executed as 'ungron' set the --ungron flag 141 | if strings.HasSuffix(os.Args[0], "ungron") { 142 | ungronFlag = true 143 | } 144 | 145 | // Determine what the program's input should be: 146 | // file, HTTP URL or stdin 147 | var rawInput io.Reader 148 | filename := flag.Arg(0) 149 | if filename == "" || filename == "-" { 150 | rawInput = os.Stdin 151 | } else if validURL(filename) { 152 | r, err := getURL(filename, insecureFlag, proxyURL, noProxy) 153 | if err != nil { 154 | fatal(exitFetchURL, err) 155 | } 156 | rawInput = r 157 | } else { 158 | r, err := os.Open(filename) 159 | if err != nil { 160 | fatal(exitOpenFile, err) 161 | } 162 | rawInput = r 163 | } 164 | 165 | var opts int 166 | // The monochrome option should be forced if the output isn't a terminal 167 | // to avoid doing unnecessary work calling the color functions 168 | switch { 169 | case colorizeFlag: 170 | color.NoColor = false 171 | case monochromeFlag || color.NoColor: 172 | opts = opts | optMonochrome 173 | } 174 | if noSortFlag { 175 | opts = opts | optNoSort 176 | } 177 | if jsonFlag { 178 | opts = opts | optJSON 179 | } 180 | 181 | // Pick the appropriate action: gron, ungron, gronValues, or gronStream 182 | var a actionFn = gron 183 | if ungronFlag { 184 | a = ungron 185 | } else if valuesFlag { 186 | a = gronValues 187 | } else if streamFlag { 188 | a = gronStream 189 | } 190 | exitCode, err := a(rawInput, colorable.NewColorableStdout(), opts) 191 | 192 | if exitCode != exitOK { 193 | fatal(exitCode, err) 194 | } 195 | 196 | os.Exit(exitOK) 197 | } 198 | 199 | // an actionFn represents a main action of the program, it accepts 200 | // an input, output and a bitfield of options; returning an exit 201 | // code and any error that occurred 202 | type actionFn func(io.Reader, io.Writer, int) (int, error) 203 | 204 | // gron is the default action. Given JSON as the input it returns a list 205 | // of assignment statements. Possible options are optNoSort and optMonochrome 206 | func gron(r io.Reader, w io.Writer, opts int) (int, error) { 207 | var err error 208 | 209 | var conv statementconv 210 | if opts&optMonochrome > 0 { 211 | conv = statementToString 212 | } else { 213 | conv = statementToColorString 214 | } 215 | 216 | ss, err := statementsFromJSON(r, statement{{"json", typBare}}) 217 | if err != nil { 218 | goto out 219 | } 220 | 221 | // Go's maps do not have well-defined ordering, but we want a consistent 222 | // output for a given input, so we must sort the statements 223 | if opts&optNoSort == 0 { 224 | sort.Sort(ss) 225 | } 226 | 227 | for _, s := range ss { 228 | if opts&optJSON > 0 { 229 | s, err = s.jsonify() 230 | if err != nil { 231 | goto out 232 | } 233 | } 234 | fmt.Fprintln(w, conv(s)) 235 | } 236 | 237 | out: 238 | if err != nil { 239 | return exitFormStatements, fmt.Errorf("failed to form statements: %s", err) 240 | } 241 | return exitOK, nil 242 | } 243 | 244 | // gronStream is like the gron action, but it treats the input as one 245 | // JSON object per line. There's a bit of code duplication from the 246 | // gron action, but it'd be fairly messy to combine the two actions 247 | func gronStream(r io.Reader, w io.Writer, opts int) (int, error) { 248 | var err error 249 | errstr := "failed to form statements" 250 | var i int 251 | var sc *bufio.Scanner 252 | var buf []byte 253 | 254 | var conv func(s statement) string 255 | if opts&optMonochrome > 0 { 256 | conv = statementToString 257 | } else { 258 | conv = statementToColorString 259 | } 260 | 261 | // Helper function to make the prefix statements for each line 262 | makePrefix := func(index int) statement { 263 | return statement{ 264 | {"json", typBare}, 265 | {"[", typLBrace}, 266 | {fmt.Sprintf("%d", index), typNumericKey}, 267 | {"]", typRBrace}, 268 | } 269 | } 270 | 271 | // The first line of output needs to establish that the top-level 272 | // thing is actually an array... 273 | top := statement{ 274 | {"json", typBare}, 275 | {"=", typEquals}, 276 | {"[]", typEmptyArray}, 277 | {";", typSemi}, 278 | } 279 | 280 | if opts&optJSON > 0 { 281 | top, err = top.jsonify() 282 | if err != nil { 283 | goto out 284 | } 285 | } 286 | 287 | fmt.Fprintln(w, conv(top)) 288 | 289 | // Read the input line by line 290 | sc = bufio.NewScanner(r) 291 | buf = make([]byte, 0, 64*1024) 292 | sc.Buffer(buf, 1024*1024) 293 | i = 0 294 | for sc.Scan() { 295 | 296 | line := bytes.NewBuffer(sc.Bytes()) 297 | 298 | var ss statements 299 | ss, err = statementsFromJSON(line, makePrefix(i)) 300 | i++ 301 | if err != nil { 302 | goto out 303 | } 304 | 305 | // Go's maps do not have well-defined ordering, but we want a consistent 306 | // output for a given input, so we must sort the statements 307 | if opts&optNoSort == 0 { 308 | sort.Sort(ss) 309 | } 310 | 311 | for _, s := range ss { 312 | if opts&optJSON > 0 { 313 | s, err = s.jsonify() 314 | if err != nil { 315 | goto out 316 | } 317 | 318 | } 319 | fmt.Fprintln(w, conv(s)) 320 | } 321 | } 322 | if err = sc.Err(); err != nil { 323 | errstr = "error reading multiline input: %s" 324 | } 325 | 326 | out: 327 | if err != nil { 328 | return exitFormStatements, fmt.Errorf(errstr+": %s", err) 329 | } 330 | return exitOK, nil 331 | 332 | } 333 | 334 | // ungron is the reverse of gron. Given assignment statements as input, 335 | // it returns JSON. The only option is optMonochrome 336 | func ungron(r io.Reader, w io.Writer, opts int) (int, error) { 337 | scanner := bufio.NewScanner(r) 338 | var maker statementmaker 339 | 340 | // Allow larger internal buffer of the scanner (min: 64KiB ~ max: 1MiB) 341 | scanner.Buffer(make([]byte, 64*1024), 1024*1024) 342 | 343 | if opts&optJSON > 0 { 344 | maker = statementFromJSONSpec 345 | } else { 346 | maker = statementFromStringMaker 347 | } 348 | 349 | // Make a list of statements from the input 350 | var ss statements 351 | for scanner.Scan() { 352 | s, err := maker(scanner.Text()) 353 | if err != nil { 354 | return exitParseStatements, err 355 | } 356 | ss.add(s) 357 | } 358 | if err := scanner.Err(); err != nil { 359 | return exitReadInput, fmt.Errorf("failed to read input statements") 360 | } 361 | 362 | // turn the statements into a single merged interface{} type 363 | merged, err := ss.toInterface() 364 | if err != nil { 365 | return exitParseStatements, err 366 | } 367 | 368 | // If there's only one top level key and it's "json", make that the top level thing 369 | mergedMap, ok := merged.(map[string]interface{}) 370 | if ok { 371 | if len(mergedMap) == 1 { 372 | if _, exists := mergedMap["json"]; exists { 373 | merged = mergedMap["json"] 374 | } 375 | } 376 | } 377 | 378 | // Marshal the output into JSON to display to the user 379 | out := &bytes.Buffer{} 380 | enc := json.NewEncoder(out) 381 | enc.SetIndent("", " ") 382 | enc.SetEscapeHTML(false) 383 | err = enc.Encode(merged) 384 | if err != nil { 385 | return exitJSONEncode, errors.Wrap(err, "failed to convert statements to JSON") 386 | } 387 | j := out.Bytes() 388 | 389 | // If the output isn't monochrome, add color to the JSON 390 | if opts&optMonochrome == 0 { 391 | c, err := colorizeJSON(j) 392 | 393 | // If we failed to colorize the JSON for whatever reason, 394 | // we'll just fall back to monochrome output, otherwise 395 | // replace the monochrome JSON with glorious technicolor 396 | if err == nil { 397 | j = c 398 | } 399 | } 400 | 401 | // For whatever reason, the monochrome version of the JSON 402 | // has a trailing newline character, but the colorized version 403 | // does not. Strip the whitespace so that neither has the newline 404 | // character on the end, and then we'll add a newline in the 405 | // Fprintf below 406 | j = bytes.TrimSpace(j) 407 | 408 | fmt.Fprintf(w, "%s\n", j) 409 | 410 | return exitOK, nil 411 | } 412 | 413 | // gronValues prints just the scalar values from some input gron statements 414 | // without any quotes or anything of that sort; a bit like jq -r 415 | // e.g. json[0].user.name = "Sam"; -> Sam 416 | func gronValues(r io.Reader, w io.Writer, opts int) (int, error) { 417 | scanner := bufio.NewScanner(os.Stdin) 418 | 419 | for scanner.Scan() { 420 | s := statementFromString(scanner.Text()) 421 | 422 | if len(s) == 0 { 423 | return exitParseStatements, fmt.Errorf("failed to parse '%s' as gron statement", scanner.Text()) 424 | } 425 | 426 | // strip off the leading 'json' bare key 427 | if s[0].typ == typBare && s[0].text == "json" { 428 | s = s[1:] 429 | } 430 | 431 | // strip off the leading dots 432 | if s[0].typ == typDot || s[0].typ == typLBrace { 433 | s = s[1:] 434 | } 435 | 436 | for _, t := range s { 437 | switch t.typ { 438 | case typString: 439 | var text string 440 | err := json.Unmarshal([]byte(t.text), &text) 441 | if err != nil { 442 | // just swallow errors and try to continue 443 | continue 444 | } 445 | fmt.Println(text) 446 | 447 | case typNumber, typTrue, typFalse, typNull: 448 | fmt.Println(t.text) 449 | 450 | default: 451 | // Nothing 452 | } 453 | } 454 | } 455 | 456 | return exitOK, nil 457 | } 458 | 459 | func colorizeJSON(src []byte) ([]byte, error) { 460 | out := &bytes.Buffer{} 461 | f := jsoncolor.NewFormatter() 462 | 463 | f.StringColor = strColor 464 | f.ObjectColor = braceColor 465 | f.ArrayColor = braceColor 466 | f.FieldColor = bareColor 467 | f.NumberColor = numColor 468 | f.TrueColor = boolColor 469 | f.FalseColor = boolColor 470 | f.NullColor = boolColor 471 | 472 | err := f.Format(out, src) 473 | if err != nil { 474 | return out.Bytes(), err 475 | } 476 | return out.Bytes(), nil 477 | } 478 | 479 | func fatal(code int, err error) { 480 | fmt.Fprintf(os.Stderr, "%s\n", err) 481 | os.Exit(code) 482 | } 483 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "io/ioutil" 7 | "os" 8 | "reflect" 9 | "testing" 10 | ) 11 | 12 | func TestGron(t *testing.T) { 13 | cases := []struct { 14 | inFile string 15 | outFile string 16 | }{ 17 | {"testdata/one.json", "testdata/one.gron"}, 18 | {"testdata/two.json", "testdata/two.gron"}, 19 | {"testdata/three.json", "testdata/three.gron"}, 20 | {"testdata/github.json", "testdata/github.gron"}, 21 | } 22 | 23 | for _, c := range cases { 24 | in, err := os.Open(c.inFile) 25 | if err != nil { 26 | t.Fatalf("failed to open input file: %s", err) 27 | } 28 | 29 | want, err := ioutil.ReadFile(c.outFile) 30 | if err != nil { 31 | t.Fatalf("failed to open want file: %s", err) 32 | } 33 | 34 | out := &bytes.Buffer{} 35 | code, err := gron(in, out, optMonochrome) 36 | 37 | if code != exitOK { 38 | t.Errorf("want exitOK; have %d", code) 39 | } 40 | if err != nil { 41 | t.Errorf("want nil error; have %s", err) 42 | } 43 | 44 | if !reflect.DeepEqual(want, out.Bytes()) { 45 | t.Logf("want: %s", want) 46 | t.Logf("have: %s", out.Bytes()) 47 | t.Errorf("gronned %s does not match %s", c.inFile, c.outFile) 48 | } 49 | } 50 | 51 | } 52 | 53 | func TestGronStream(t *testing.T) { 54 | cases := []struct { 55 | inFile string 56 | outFile string 57 | }{ 58 | {"testdata/stream.json", "testdata/stream.gron"}, 59 | {"testdata/scalar-stream.json", "testdata/scalar-stream.gron"}, 60 | } 61 | 62 | for _, c := range cases { 63 | in, err := os.Open(c.inFile) 64 | if err != nil { 65 | t.Fatalf("failed to open input file: %s", err) 66 | } 67 | 68 | want, err := ioutil.ReadFile(c.outFile) 69 | if err != nil { 70 | t.Fatalf("failed to open want file: %s", err) 71 | } 72 | 73 | out := &bytes.Buffer{} 74 | code, err := gronStream(in, out, optMonochrome) 75 | 76 | if code != exitOK { 77 | t.Errorf("want exitOK; have %d", code) 78 | } 79 | if err != nil { 80 | t.Errorf("want nil error; have %s", err) 81 | } 82 | 83 | if !reflect.DeepEqual(want, out.Bytes()) { 84 | t.Logf("want: %s", want) 85 | t.Logf("have: %s", out.Bytes()) 86 | t.Errorf("gronned %s does not match %s", c.inFile, c.outFile) 87 | } 88 | } 89 | 90 | } 91 | 92 | func TestLargeGronStream(t *testing.T) { 93 | cases := []struct { 94 | inFile string 95 | outFile string 96 | }{ 97 | {"testdata/long-stream.json", "testdata/long-stream.gron"}, 98 | } 99 | 100 | for _, c := range cases { 101 | in, err := os.Open(c.inFile) 102 | if err != nil { 103 | t.Fatalf("failed to open input file: %s", err) 104 | } 105 | 106 | want, err := ioutil.ReadFile(c.outFile) 107 | if err != nil { 108 | t.Fatalf("failed to open want file: %s", err) 109 | } 110 | 111 | out := &bytes.Buffer{} 112 | code, err := gronStream(in, out, optMonochrome) 113 | 114 | if code != exitOK { 115 | t.Errorf("want exitOK; have %d", code) 116 | } 117 | if err != nil { 118 | t.Errorf("want nil error; have %s", err) 119 | } 120 | 121 | if !reflect.DeepEqual(want, out.Bytes()) { 122 | t.Logf("want: %s", want) 123 | t.Logf("have: %s", out.Bytes()) 124 | t.Errorf("gronned %s does not match %s", c.inFile, c.outFile) 125 | } 126 | } 127 | 128 | } 129 | 130 | func TestUngron(t *testing.T) { 131 | cases := []struct { 132 | inFile string 133 | outFile string 134 | }{ 135 | {"testdata/one.gron", "testdata/one.json"}, 136 | {"testdata/two.gron", "testdata/two.json"}, 137 | {"testdata/three.gron", "testdata/three.json"}, 138 | {"testdata/grep-separators.gron", "testdata/grep-separators.json"}, 139 | {"testdata/github.gron", "testdata/github.json"}, 140 | {"testdata/large-line.gron", "testdata/large-line.json"}, 141 | } 142 | 143 | for _, c := range cases { 144 | wantF, err := ioutil.ReadFile(c.outFile) 145 | if err != nil { 146 | t.Fatalf("failed to open want file: %s", err) 147 | } 148 | 149 | var want interface{} 150 | err = json.Unmarshal(wantF, &want) 151 | if err != nil { 152 | t.Fatalf("failed to unmarshal JSON from want file: %s", err) 153 | } 154 | 155 | in, err := os.Open(c.inFile) 156 | if err != nil { 157 | t.Fatalf("failed to open input file: %s", err) 158 | } 159 | 160 | out := &bytes.Buffer{} 161 | code, err := ungron(in, out, optMonochrome) 162 | 163 | if code != exitOK { 164 | t.Errorf("want exitOK; have %d", code) 165 | } 166 | if err != nil { 167 | t.Errorf("want nil error; have %s", err) 168 | } 169 | 170 | var have interface{} 171 | err = json.Unmarshal(out.Bytes(), &have) 172 | if err != nil { 173 | t.Fatalf("failed to unmarshal JSON from ungron output: %s", err) 174 | } 175 | 176 | if !reflect.DeepEqual(want, have) { 177 | t.Logf("want: %#v", want) 178 | t.Logf("have: %#v", have) 179 | t.Errorf("ungronned %s does not match %s", c.inFile, c.outFile) 180 | } 181 | 182 | } 183 | } 184 | 185 | func TestGronJ(t *testing.T) { 186 | cases := []struct { 187 | inFile string 188 | outFile string 189 | }{ 190 | {"testdata/one.json", "testdata/one.jgron"}, 191 | {"testdata/two.json", "testdata/two.jgron"}, 192 | {"testdata/three.json", "testdata/three.jgron"}, 193 | {"testdata/github.json", "testdata/github.jgron"}, 194 | } 195 | 196 | for _, c := range cases { 197 | in, err := os.Open(c.inFile) 198 | if err != nil { 199 | t.Fatalf("failed to open input file: %s", err) 200 | } 201 | 202 | want, err := ioutil.ReadFile(c.outFile) 203 | if err != nil { 204 | t.Fatalf("failed to open want file: %s", err) 205 | } 206 | 207 | out := &bytes.Buffer{} 208 | code, err := gron(in, out, optMonochrome|optJSON) 209 | 210 | if code != exitOK { 211 | t.Errorf("want exitOK; have %d", code) 212 | } 213 | if err != nil { 214 | t.Errorf("want nil error; have %s", err) 215 | } 216 | 217 | if !reflect.DeepEqual(want, out.Bytes()) { 218 | t.Logf("want: %s", want) 219 | t.Logf("have: %s", out.Bytes()) 220 | t.Errorf("gronned %s does not match %s", c.inFile, c.outFile) 221 | } 222 | } 223 | 224 | } 225 | 226 | func TestGronStreamJ(t *testing.T) { 227 | cases := []struct { 228 | inFile string 229 | outFile string 230 | }{ 231 | {"testdata/stream.json", "testdata/stream.jgron"}, 232 | {"testdata/scalar-stream.json", "testdata/scalar-stream.jgron"}, 233 | } 234 | 235 | for _, c := range cases { 236 | in, err := os.Open(c.inFile) 237 | if err != nil { 238 | t.Fatalf("failed to open input file: %s", err) 239 | } 240 | 241 | want, err := ioutil.ReadFile(c.outFile) 242 | if err != nil { 243 | t.Fatalf("failed to open want file: %s", err) 244 | } 245 | 246 | out := &bytes.Buffer{} 247 | code, err := gronStream(in, out, optMonochrome|optJSON) 248 | 249 | if code != exitOK { 250 | t.Errorf("want exitOK; have %d", code) 251 | } 252 | if err != nil { 253 | t.Errorf("want nil error; have %s", err) 254 | } 255 | 256 | if !reflect.DeepEqual(want, out.Bytes()) { 257 | t.Logf("want: %s", want) 258 | t.Logf("have: %s", out.Bytes()) 259 | t.Errorf("gronned %s does not match %s", c.inFile, c.outFile) 260 | } 261 | } 262 | 263 | } 264 | 265 | func TestUngronJ(t *testing.T) { 266 | cases := []struct { 267 | inFile string 268 | outFile string 269 | }{ 270 | {"testdata/one.jgron", "testdata/one.json"}, 271 | {"testdata/two.jgron", "testdata/two.json"}, 272 | {"testdata/three.jgron", "testdata/three.json"}, 273 | {"testdata/github.jgron", "testdata/github.json"}, 274 | } 275 | 276 | for _, c := range cases { 277 | wantF, err := ioutil.ReadFile(c.outFile) 278 | if err != nil { 279 | t.Fatalf("failed to open want file: %s", err) 280 | } 281 | 282 | var want interface{} 283 | err = json.Unmarshal(wantF, &want) 284 | if err != nil { 285 | t.Fatalf("failed to unmarshal JSON from want file: %s", err) 286 | } 287 | 288 | in, err := os.Open(c.inFile) 289 | if err != nil { 290 | t.Fatalf("failed to open input file: %s", err) 291 | } 292 | 293 | out := &bytes.Buffer{} 294 | code, err := ungron(in, out, optMonochrome|optJSON) 295 | 296 | if code != exitOK { 297 | t.Errorf("want exitOK; have %d", code) 298 | } 299 | if err != nil { 300 | t.Errorf("want nil error; have %s", err) 301 | } 302 | 303 | var have interface{} 304 | err = json.Unmarshal(out.Bytes(), &have) 305 | if err != nil { 306 | t.Fatalf("failed to unmarshal JSON from ungron output: %s", err) 307 | } 308 | 309 | if !reflect.DeepEqual(want, have) { 310 | t.Logf("want: %#v", want) 311 | t.Logf("have: %#v", have) 312 | t.Errorf("ungronned %s does not match %s", c.inFile, c.outFile) 313 | } 314 | 315 | } 316 | } 317 | 318 | func BenchmarkBigJSON(b *testing.B) { 319 | in, err := os.Open("testdata/big.json") 320 | if err != nil { 321 | b.Fatalf("failed to open test data file: %s", err) 322 | } 323 | 324 | for i := 0; i < b.N; i++ { 325 | out := &bytes.Buffer{} 326 | _, err = in.Seek(0, 0) 327 | if err != nil { 328 | b.Fatalf("failed to rewind input: %s", err) 329 | } 330 | 331 | _, err := gron(in, out, optMonochrome|optNoSort) 332 | if err != nil { 333 | b.Fatalf("failed to gron: %s", err) 334 | } 335 | } 336 | } 337 | -------------------------------------------------------------------------------- /original-gron.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | $v){ 94 | $k = json_encode($k); 95 | printSruct($v, "{$prefix}[{$k}]"); 96 | } 97 | } 98 | 99 | exit(0); 100 | -------------------------------------------------------------------------------- /script/example: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | PROJDIR=$(cd `dirname $0`/.. && pwd) 3 | cd $PROJDIR 4 | go build 5 | ./gron testdata/one.json 6 | -------------------------------------------------------------------------------- /script/lint: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | PROJDIR=$(cd `dirname $0`/.. && pwd) 3 | cd ${PROJDIR} 4 | 5 | which gometalinter > /dev/null 6 | 7 | if [ $? -ne 0 ]; then 8 | echo "You don't have gometalinter. Installing it..." 9 | go get github.com/alecthomas/gometalinter 10 | gometalinter --install 11 | fi 12 | 13 | # dupl is disabled because it has a habbit of identifying tests as duplicated code. 14 | # in its defence: it's right. gocyclo is disabled because I'm a terrible programmer. 15 | # gas is disabled because it doesn't like InsecureSkipVerify set to true for HTTP 16 | # requests - but we only do that if the user asks for it. 17 | gometalinter --disable=gocyclo --disable=dupl --enable=goimports --disable=gas 18 | -------------------------------------------------------------------------------- /script/precommit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | PROJDIR=$(cd `dirname $0`/.. && pwd) 4 | cd ${PROJDIR} 5 | 6 | ./script/test 7 | -------------------------------------------------------------------------------- /script/profile: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | PROJDIR=$(cd `dirname $0`/.. && pwd) 4 | cd ${PROJDIR} 5 | 6 | PAT="." 7 | if [ ! -z "${1}" ]; then 8 | PAT="${1}" 9 | fi 10 | 11 | go test -bench ${PAT} -benchmem -cpuprofile cpu.out 12 | go tool pprof gron.test cpu.out 13 | -------------------------------------------------------------------------------- /script/release: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | PROJDIR=$(cd `dirname $0`/.. && pwd) 3 | 4 | VERSION="${1}" 5 | TAG="v${VERSION}" 6 | USER="tomnomnom" 7 | REPO="gron" 8 | BINARY="${REPO}" 9 | 10 | if [[ -z "${VERSION}" ]]; then 11 | echo "Usage: ${0} " 12 | exit 1 13 | fi 14 | 15 | if [[ -z "${GITHUB_TOKEN}" ]]; then 16 | echo "You forgot to set your GITHUB_TOKEN" 17 | exit 2 18 | fi 19 | 20 | cd ${PROJDIR} 21 | 22 | # Run the tests 23 | go test 24 | if [ $? -ne 0 ]; then 25 | echo "Tests failed. Aborting." 26 | exit 3 27 | fi 28 | 29 | FILELIST="" 30 | 31 | for ARCH in "amd64" "386" "arm64" "arm"; do 32 | for OS in "darwin" "linux" "windows" "freebsd"; do 33 | 34 | if [[ "${OS}" == "darwin" && ("${ARCH}" == "386" || "${ARCH}" == "arm") ]]; then 35 | continue 36 | fi 37 | 38 | BINFILE="${BINARY}" 39 | 40 | if [[ "${OS}" == "windows" ]]; then 41 | BINFILE="${BINFILE}.exe" 42 | fi 43 | 44 | rm -f ${BINFILE} 45 | 46 | GOOS=${OS} GOARCH=${ARCH} go build -ldflags "-X main.gronVersion=${VERSION}" github.com/${USER}/${REPO} 47 | 48 | if [[ "${OS}" == "windows" ]]; then 49 | ARCHIVE="${BINARY}-${OS}-${ARCH}-${VERSION}.zip" 50 | zip ${ARCHIVE} ${BINFILE} 51 | rm ${BINFILE} 52 | else 53 | ARCHIVE="${BINARY}-${OS}-${ARCH}-${VERSION}.tgz" 54 | tar --create --gzip --file=${ARCHIVE} ${BINFILE} 55 | fi 56 | 57 | FILELIST="${FILELIST} ${PROJDIR}/${ARCHIVE}" 58 | done 59 | done 60 | 61 | gh release create ${TAG} ${FILELIST} 62 | rm ${FILELIST} 63 | 64 | -------------------------------------------------------------------------------- /script/test: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | PROJDIR=$(cd `dirname $0`/.. && pwd) 3 | cd ${PROJDIR} 4 | 5 | go test 6 | -------------------------------------------------------------------------------- /statements.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "reflect" 8 | "strconv" 9 | "strings" 10 | 11 | "github.com/pkg/errors" 12 | ) 13 | 14 | // A statement is a slice of tokens representing an assignment statement. 15 | // An assignment statement is something like: 16 | // 17 | // json.city = "Leeds"; 18 | // 19 | // Where 'json', '.', 'city', '=', '"Leeds"' and ';' are discrete tokens. 20 | // Statements are stored as tokens to make sorting more efficient, and so 21 | // that the same type can easily be used when gronning and ungronning. 22 | type statement []token 23 | 24 | // String returns the string form of a statement rather than the 25 | // underlying slice of tokens 26 | func (s statement) String() string { 27 | out := make([]string, 0, len(s)+2) 28 | for _, t := range s { 29 | out = append(out, t.format()) 30 | } 31 | return strings.Join(out, "") 32 | } 33 | 34 | // colorString returns the string form of a statement with ASCII color codes 35 | func (s statement) colorString() string { 36 | out := make([]string, 0, len(s)+2) 37 | for _, t := range s { 38 | out = append(out, t.formatColor()) 39 | } 40 | return strings.Join(out, "") 41 | } 42 | 43 | // a statementconv converts a statement to string 44 | type statementconv func(s statement) string 45 | 46 | // statementconv variant of statement.String 47 | func statementToString(s statement) string { 48 | return s.String() 49 | } 50 | 51 | // statementconv variant of statement.colorString 52 | func statementToColorString(s statement) string { 53 | return s.colorString() 54 | } 55 | 56 | // withBare returns a copy of a statement with a new bare 57 | // word token appended to it 58 | func (s statement) withBare(k string) statement { 59 | new := make(statement, len(s), len(s)+2) 60 | copy(new, s) 61 | return append( 62 | new, 63 | token{".", typDot}, 64 | token{k, typBare}, 65 | ) 66 | } 67 | 68 | // jsonify converts an assignment statement to a JSON representation 69 | func (s statement) jsonify() (statement, error) { 70 | // If m is the number of keys occurring in the left hand side 71 | // of s, then len(s) is in between 2*m+4 and 3*m+4. The resultant 72 | // statement j (carrying the JSON representation) is always 2*m+5 73 | // long. So len(s)+1 ≥ 2*m+5 = len(j). Therefore an initaial 74 | // allocation of j with capacity len(s)+1 will allow us to carry 75 | // through without reallocation. 76 | j := make(statement, 0, len(s)+1) 77 | if len(s) < 4 || s[0].typ != typBare || s[len(s)-3].typ != typEquals || 78 | s[len(s)-1].typ != typSemi { 79 | return nil, errors.New("non-assignment statement") 80 | } 81 | 82 | j = append(j, token{"[", typLBrace}) 83 | j = append(j, token{"[", typLBrace}) 84 | for _, t := range s[1 : len(s)-3] { 85 | switch t.typ { 86 | case typNumericKey, typQuotedKey: 87 | j = append(j, t) 88 | j = append(j, token{",", typComma}) 89 | case typBare: 90 | j = append(j, token{quoteString(t.text), typQuotedKey}) 91 | j = append(j, token{",", typComma}) 92 | } 93 | } 94 | if j[len(j)-1].typ == typComma { 95 | j = j[:len(j)-1] 96 | } 97 | j = append(j, token{"]", typLBrace}) 98 | j = append(j, token{",", typComma}) 99 | j = append(j, s[len(s)-2]) 100 | j = append(j, token{"]", typLBrace}) 101 | 102 | return j, nil 103 | } 104 | 105 | // withQuotedKey returns a copy of a statement with a new 106 | // quoted key token appended to it 107 | func (s statement) withQuotedKey(k string) statement { 108 | new := make(statement, len(s), len(s)+3) 109 | copy(new, s) 110 | return append( 111 | new, 112 | token{"[", typLBrace}, 113 | token{quoteString(k), typQuotedKey}, 114 | token{"]", typRBrace}, 115 | ) 116 | } 117 | 118 | // withNumericKey returns a copy of a statement with a new 119 | // numeric key token appended to it 120 | func (s statement) withNumericKey(k int) statement { 121 | new := make(statement, len(s), len(s)+3) 122 | copy(new, s) 123 | return append( 124 | new, 125 | token{"[", typLBrace}, 126 | token{strconv.Itoa(k), typNumericKey}, 127 | token{"]", typRBrace}, 128 | ) 129 | } 130 | 131 | // statements is a list of assignment statements. 132 | // E.g statement: json.foo = "bar"; 133 | type statements []statement 134 | 135 | // addWithValue takes a statement representing a path, copies it, 136 | // adds a value token to the end of the statement and appends 137 | // the new statement to the list of statements 138 | func (ss *statements) addWithValue(path statement, value token) { 139 | s := make(statement, len(path), len(path)+3) 140 | copy(s, path) 141 | s = append(s, token{"=", typEquals}, value, token{";", typSemi}) 142 | *ss = append(*ss, s) 143 | } 144 | 145 | // add appends a new complete statement to list of statements 146 | func (ss *statements) add(s statement) { 147 | *ss = append(*ss, s) 148 | } 149 | 150 | // Len returns the number of statements for sort.Sort 151 | func (ss statements) Len() int { 152 | return len(ss) 153 | } 154 | 155 | // Swap swaps two statements for sort.Sort 156 | func (ss statements) Swap(i, j int) { 157 | ss[i], ss[j] = ss[j], ss[i] 158 | } 159 | 160 | // a statementmaker is a function that makes a statement 161 | // from string 162 | type statementmaker func(str string) (statement, error) 163 | 164 | // statementFromString takes statement string, lexes it and returns 165 | // the corresponding statement 166 | func statementFromString(str string) statement { 167 | l := newLexer(str) 168 | s := l.lex() 169 | return s 170 | } 171 | 172 | // statementmaker variant of statementFromString 173 | func statementFromStringMaker(str string) (statement, error) { 174 | return statementFromString(str), nil 175 | } 176 | 177 | // statementFromJson returns statement encoded by 178 | // JSON specification 179 | func statementFromJSONSpec(str string) (statement, error) { 180 | var a []interface{} 181 | var ok bool 182 | var v interface{} 183 | var s statement 184 | var t tokenTyp 185 | var nstr string 186 | var nbuf []byte 187 | 188 | err := json.Unmarshal([]byte(str), &a) 189 | if err != nil { 190 | return nil, err 191 | } 192 | if len(a) != 2 { 193 | goto out 194 | } 195 | 196 | v = a[1] 197 | a, ok = a[0].([]interface{}) 198 | if !ok { 199 | goto out 200 | } 201 | 202 | // We'll append one initial token, then 3 tokens for each element of a, 203 | // then 3 closing tokens, that's alltogether 3*len(a)+4. 204 | s = make(statement, 0, 3*len(a)+4) 205 | s = append(s, token{"json", typBare}) 206 | for _, e := range a { 207 | s = append(s, token{"[", typLBrace}) 208 | switch e := e.(type) { 209 | case string: 210 | s = append(s, token{quoteString(e), typQuotedKey}) 211 | case float64: 212 | nbuf, err = json.Marshal(e) 213 | if err != nil { 214 | return nil, errors.Wrap(err, "JSON internal error") 215 | } 216 | nstr = string(nbuf) 217 | s = append(s, token{nstr, typNumericKey}) 218 | default: 219 | ok = false 220 | goto out 221 | } 222 | s = append(s, token{"]", typRBrace}) 223 | } 224 | 225 | s = append(s, token{"=", typEquals}) 226 | 227 | switch v := v.(type) { 228 | case bool: 229 | if v { 230 | t = typTrue 231 | } else { 232 | t = typFalse 233 | } 234 | case float64: 235 | t = typNumber 236 | case string: 237 | t = typString 238 | case []interface{}: 239 | ok = (len(v) == 0) 240 | if !ok { 241 | goto out 242 | } 243 | t = typEmptyArray 244 | case map[string]interface{}: 245 | ok = (len(v) == 0) 246 | if !ok { 247 | goto out 248 | } 249 | t = typEmptyObject 250 | default: 251 | ok = (v == nil) 252 | if !ok { 253 | goto out 254 | } 255 | t = typNull 256 | } 257 | 258 | nbuf, err = json.Marshal(v) 259 | if err != nil { 260 | return nil, errors.Wrap(err, "JSON internal error") 261 | } 262 | nstr = string(nbuf) 263 | s = append(s, token{nstr, t}) 264 | 265 | s = append(s, token{";", typSemi}) 266 | 267 | out: 268 | if !ok { 269 | return nil, errors.New("invalid JSON layout") 270 | } 271 | return s, nil 272 | } 273 | 274 | // ungron turns statements into a proper datastructure 275 | func (ss statements) toInterface() (interface{}, error) { 276 | 277 | // Get all the individually parsed statements 278 | var parsed []interface{} 279 | for _, s := range ss { 280 | u, err := ungronTokens(s) 281 | 282 | switch err.(type) { 283 | case nil: 284 | // no problem :) 285 | case errRecoverable: 286 | continue 287 | default: 288 | return nil, errors.Wrapf(err, "ungron failed for `%s`", s) 289 | } 290 | 291 | parsed = append(parsed, u) 292 | } 293 | 294 | if len(parsed) == 0 { 295 | return nil, fmt.Errorf("no statements were parsed") 296 | } 297 | 298 | merged := parsed[0] 299 | for _, p := range parsed[1:] { 300 | m, err := recursiveMerge(merged, p) 301 | if err != nil { 302 | return nil, errors.Wrap(err, "failed to merge statements") 303 | } 304 | merged = m 305 | } 306 | return merged, nil 307 | 308 | } 309 | 310 | // Less compares two statements for sort.Sort 311 | // Implements a natural sort to keep array indexes in order 312 | func (ss statements) Less(a, b int) bool { 313 | 314 | // ss[a] and ss[b] are both slices of tokens. The first 315 | // thing we need to do is find the first token (if any) 316 | // that differs, then we can use that token to decide 317 | // if ss[a] or ss[b] should come first in the sort. 318 | diffIndex := -1 319 | for i := range ss[a] { 320 | 321 | if len(ss[b]) < i+1 { 322 | // b must be shorter than a, so it 323 | // should come first 324 | return false 325 | } 326 | 327 | // The tokens match, so just carry on 328 | if ss[a][i] == ss[b][i] { 329 | continue 330 | } 331 | 332 | // We've found a difference 333 | diffIndex = i 334 | break 335 | } 336 | 337 | // If diffIndex is still -1 then the only difference must be 338 | // that ss[b] is longer than ss[a], so ss[a] should come first 339 | if diffIndex == -1 { 340 | return true 341 | } 342 | 343 | // Get the tokens that differ 344 | ta := ss[a][diffIndex] 345 | tb := ss[b][diffIndex] 346 | 347 | // An equals always comes first 348 | if ta.typ == typEquals { 349 | return true 350 | } 351 | if tb.typ == typEquals { 352 | return false 353 | } 354 | 355 | // If both tokens are numeric keys do an integer comparison 356 | if ta.typ == typNumericKey && tb.typ == typNumericKey { 357 | ia, _ := strconv.Atoi(ta.text) 358 | ib, _ := strconv.Atoi(tb.text) 359 | return ia < ib 360 | } 361 | 362 | // If neither token is a number, just do a string comparison 363 | if ta.typ != typNumber || tb.typ != typNumber { 364 | return ta.text < tb.text 365 | } 366 | 367 | // We have two numbers to compare so turn them into json.Number 368 | // for comparison 369 | na, _ := json.Number(ta.text).Float64() 370 | nb, _ := json.Number(tb.text).Float64() 371 | return na < nb 372 | 373 | } 374 | 375 | // Contains searches the statements for a given statement 376 | // Mostly to make testing things easier 377 | func (ss statements) Contains(search statement) bool { 378 | for _, i := range ss { 379 | if reflect.DeepEqual(i, search) { 380 | return true 381 | } 382 | } 383 | return false 384 | } 385 | 386 | // statementsFromJSON takes an io.Reader containing JSON 387 | // and returns statements or an error on failure 388 | func statementsFromJSON(r io.Reader, prefix statement) (statements, error) { 389 | var top interface{} 390 | d := json.NewDecoder(r) 391 | d.UseNumber() 392 | err := d.Decode(&top) 393 | if err != nil { 394 | return nil, err 395 | } 396 | ss := make(statements, 0, 32) 397 | ss.fill(prefix, top) 398 | return ss, nil 399 | } 400 | 401 | // fill takes a prefix statement and some value and recursively fills 402 | // the statement list using that value 403 | func (ss *statements) fill(prefix statement, v interface{}) { 404 | 405 | // Add a statement for the current prefix and value 406 | ss.addWithValue(prefix, valueTokenFromInterface(v)) 407 | 408 | // Recurse into objects and arrays 409 | switch vv := v.(type) { 410 | 411 | case map[string]interface{}: 412 | // It's an object 413 | for k, sub := range vv { 414 | if validIdentifier(k) { 415 | ss.fill(prefix.withBare(k), sub) 416 | } else { 417 | ss.fill(prefix.withQuotedKey(k), sub) 418 | } 419 | } 420 | 421 | case []interface{}: 422 | // It's an array 423 | for k, sub := range vv { 424 | ss.fill(prefix.withNumericKey(k), sub) 425 | } 426 | } 427 | 428 | } 429 | -------------------------------------------------------------------------------- /statements_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "reflect" 7 | "sort" 8 | "testing" 9 | ) 10 | 11 | func statementsFromStringSlice(strs []string) statements { 12 | ss := make(statements, len(strs)) 13 | for i, str := range strs { 14 | ss[i] = statementFromString(str) 15 | } 16 | return ss 17 | } 18 | 19 | func TestStatementsSimple(t *testing.T) { 20 | 21 | j := []byte(`{ 22 | "dotted": "A dotted value", 23 | "a quoted": "value", 24 | "bool1": true, 25 | "bool2": false, 26 | "anull": null, 27 | "anarr": [1, 1.5], 28 | "anob": { 29 | "foo": "bar" 30 | }, 31 | "else": 1, 32 | "id": 66912849, 33 | "": 2 34 | }`) 35 | 36 | ss, err := statementsFromJSON(bytes.NewReader(j), statement{{"json", typBare}}) 37 | 38 | if err != nil { 39 | t.Errorf("Want nil error from makeStatementsFromJSON() but got %s", err) 40 | } 41 | 42 | wants := statementsFromStringSlice([]string{ 43 | `json = {};`, 44 | `json.dotted = "A dotted value";`, 45 | `json["a quoted"] = "value";`, 46 | `json.bool1 = true;`, 47 | `json.bool2 = false;`, 48 | `json.anull = null;`, 49 | `json.anarr = [];`, 50 | `json.anarr[0] = 1;`, 51 | `json.anarr[1] = 1.5;`, 52 | `json.anob = {};`, 53 | `json.anob.foo = "bar";`, 54 | `json["else"] = 1;`, 55 | `json.id = 66912849;`, 56 | `json[""] = 2;`, 57 | }) 58 | 59 | t.Logf("Have: %#v", ss) 60 | for _, want := range wants { 61 | if !ss.Contains(want) { 62 | t.Errorf("Statement group should contain `%s` but doesn't", want) 63 | } 64 | } 65 | 66 | } 67 | 68 | func TestStatementsSorting(t *testing.T) { 69 | want := statementsFromStringSlice([]string{ 70 | `json.a = true;`, 71 | `json.b = true;`, 72 | `json.c[0] = true;`, 73 | `json.c[2] = true;`, 74 | `json.c[10] = true;`, 75 | `json.c[11] = true;`, 76 | `json.c[21][2] = true;`, 77 | `json.c[21][11] = true;`, 78 | }) 79 | 80 | have := statementsFromStringSlice([]string{ 81 | `json.c[11] = true;`, 82 | `json.c[21][2] = true;`, 83 | `json.c[0] = true;`, 84 | `json.c[2] = true;`, 85 | `json.b = true;`, 86 | `json.c[10] = true;`, 87 | `json.c[21][11] = true;`, 88 | `json.a = true;`, 89 | }) 90 | 91 | sort.Sort(have) 92 | 93 | for i := range want { 94 | if !reflect.DeepEqual(have[i], want[i]) { 95 | t.Errorf("Statements sorted incorrectly; want `%s` at index %d, have `%s`", want[i], i, have[i]) 96 | } 97 | } 98 | } 99 | 100 | func BenchmarkStatementsLess(b *testing.B) { 101 | ss := statementsFromStringSlice([]string{ 102 | `json.c[21][2] = true;`, 103 | `json.c[21][11] = true;`, 104 | }) 105 | 106 | for i := 0; i < b.N; i++ { 107 | _ = ss.Less(0, 1) 108 | } 109 | } 110 | 111 | func BenchmarkFill(b *testing.B) { 112 | j := []byte(`{ 113 | "dotted": "A dotted value", 114 | "a quoted": "value", 115 | "bool1": true, 116 | "bool2": false, 117 | "anull": null, 118 | "anarr": [1, 1.5], 119 | "anob": { 120 | "foo": "bar" 121 | }, 122 | "else": 1 123 | }`) 124 | 125 | var top interface{} 126 | err := json.Unmarshal(j, &top) 127 | if err != nil { 128 | b.Fatalf("Failed to unmarshal test file: %s", err) 129 | } 130 | 131 | for i := 0; i < b.N; i++ { 132 | ss := make(statements, 0) 133 | ss.fill(statement{{"json", typBare}}, top) 134 | } 135 | } 136 | 137 | func TestUngronStatementsSimple(t *testing.T) { 138 | in := statementsFromStringSlice([]string{ 139 | `json.contact = {};`, 140 | `json.contact["e-mail"][0] = "mail@tomnomnom.com";`, 141 | `json.contact["e-mail"][1] = "test@tomnomnom.com";`, 142 | `json.contact["e-mail"][3] = "foo@tomnomnom.com";`, 143 | `json.contact.twitter = "@TomNomNom";`, 144 | }) 145 | 146 | want := map[string]interface{}{ 147 | "json": map[string]interface{}{ 148 | "contact": map[string]interface{}{ 149 | "e-mail": []interface{}{ 150 | 0: "mail@tomnomnom.com", 151 | 1: "test@tomnomnom.com", 152 | 3: "foo@tomnomnom.com", 153 | }, 154 | "twitter": "@TomNomNom", 155 | }, 156 | }, 157 | } 158 | 159 | have, err := in.toInterface() 160 | 161 | if err != nil { 162 | t.Fatalf("want nil error but have: %s", err) 163 | } 164 | 165 | t.Logf("Have: %#v", have) 166 | t.Logf("Want: %#v", want) 167 | 168 | eq := reflect.DeepEqual(have, want) 169 | if !eq { 170 | t.Errorf("have and want are not equal") 171 | } 172 | } 173 | 174 | func TestUngronStatementsInvalid(t *testing.T) { 175 | cases := []statements{ 176 | statementsFromStringSlice([]string{``}), 177 | statementsFromStringSlice([]string{`this isn't a statement at all`}), 178 | statementsFromStringSlice([]string{`json[0] = 1;`, `json.bar = 1;`}), 179 | } 180 | 181 | for _, c := range cases { 182 | _, err := c.toInterface() 183 | if err == nil { 184 | t.Errorf("want non-nil error; have nil") 185 | } 186 | } 187 | } 188 | 189 | func TestStatement(t *testing.T) { 190 | s := statement{ 191 | token{"json", typBare}, 192 | token{".", typDot}, 193 | token{"foo", typBare}, 194 | token{"=", typEquals}, 195 | token{"2", typNumber}, 196 | token{";", typSemi}, 197 | } 198 | 199 | have := s.String() 200 | want := "json.foo = 2;" 201 | if have != want { 202 | t.Errorf("have: `%s` want: `%s`", have, want) 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /testdata/github.gron: -------------------------------------------------------------------------------- 1 | json = []; 2 | json[0] = {}; 3 | json[0].author = {}; 4 | json[0].author.avatar_url = "https://avatars.githubusercontent.com/u/58276?v=3"; 5 | json[0].author.events_url = "https://api.github.com/users/tomnomnom/events{/privacy}"; 6 | json[0].author.followers_url = "https://api.github.com/users/tomnomnom/followers"; 7 | json[0].author.following_url = "https://api.github.com/users/tomnomnom/following{/other_user}"; 8 | json[0].author.gists_url = "https://api.github.com/users/tomnomnom/gists{/gist_id}"; 9 | json[0].author.gravatar_id = ""; 10 | json[0].author.html_url = "https://github.com/tomnomnom"; 11 | json[0].author.id = 58276; 12 | json[0].author.login = "tomnomnom"; 13 | json[0].author.organizations_url = "https://api.github.com/users/tomnomnom/orgs"; 14 | json[0].author.received_events_url = "https://api.github.com/users/tomnomnom/received_events"; 15 | json[0].author.repos_url = "https://api.github.com/users/tomnomnom/repos"; 16 | json[0].author.site_admin = false; 17 | json[0].author.starred_url = "https://api.github.com/users/tomnomnom/starred{/owner}{/repo}"; 18 | json[0].author.subscriptions_url = "https://api.github.com/users/tomnomnom/subscriptions"; 19 | json[0].author.type = "User"; 20 | json[0].author.url = "https://api.github.com/users/tomnomnom"; 21 | json[0].comments_url = "https://api.github.com/repos/tomnomnom/gron/commits/cfade78b10e9c0cb9e4e0a902f5383ee4e5efc9e/comments"; 22 | json[0].commit = {}; 23 | json[0].commit.author = {}; 24 | json[0].commit.author.date = "2016-09-09T20:08:58Z"; 25 | json[0].commit.author.email = "mail@tomnomnom.com"; 26 | json[0].commit.author.name = "Tom Hudson"; 27 | json[0].commit.comment_count = 0; 28 | json[0].commit.committer = {}; 29 | json[0].commit.committer.date = "2016-09-09T20:08:58Z"; 30 | json[0].commit.committer.email = "mail@tomnomnom.com"; 31 | json[0].commit.committer.name = "Tom Hudson"; 32 | json[0].commit.message = "Unexports statements add/addFull methods"; 33 | json[0].commit.tree = {}; 34 | json[0].commit.tree.sha = "536ec3c40a0510cd048921cd8757c401aad94b91"; 35 | json[0].commit.tree.url = "https://api.github.com/repos/tomnomnom/gron/git/trees/536ec3c40a0510cd048921cd8757c401aad94b91"; 36 | json[0].commit.url = "https://api.github.com/repos/tomnomnom/gron/git/commits/cfade78b10e9c0cb9e4e0a902f5383ee4e5efc9e"; 37 | json[0].committer = {}; 38 | json[0].committer.avatar_url = "https://avatars.githubusercontent.com/u/58276?v=3"; 39 | json[0].committer.events_url = "https://api.github.com/users/tomnomnom/events{/privacy}"; 40 | json[0].committer.followers_url = "https://api.github.com/users/tomnomnom/followers"; 41 | json[0].committer.following_url = "https://api.github.com/users/tomnomnom/following{/other_user}"; 42 | json[0].committer.gists_url = "https://api.github.com/users/tomnomnom/gists{/gist_id}"; 43 | json[0].committer.gravatar_id = ""; 44 | json[0].committer.html_url = "https://github.com/tomnomnom"; 45 | json[0].committer.id = 58276; 46 | json[0].committer.login = "tomnomnom"; 47 | json[0].committer.organizations_url = "https://api.github.com/users/tomnomnom/orgs"; 48 | json[0].committer.received_events_url = "https://api.github.com/users/tomnomnom/received_events"; 49 | json[0].committer.repos_url = "https://api.github.com/users/tomnomnom/repos"; 50 | json[0].committer.site_admin = false; 51 | json[0].committer.starred_url = "https://api.github.com/users/tomnomnom/starred{/owner}{/repo}"; 52 | json[0].committer.subscriptions_url = "https://api.github.com/users/tomnomnom/subscriptions"; 53 | json[0].committer.type = "User"; 54 | json[0].committer.url = "https://api.github.com/users/tomnomnom"; 55 | json[0].html_url = "https://github.com/tomnomnom/gron/commit/cfade78b10e9c0cb9e4e0a902f5383ee4e5efc9e"; 56 | json[0].parents = []; 57 | json[0].parents[0] = {}; 58 | json[0].parents[0].html_url = "https://github.com/tomnomnom/gron/commit/ca66db90d96332a85ec29ea527b3966decf1c6fc"; 59 | json[0].parents[0].sha = "ca66db90d96332a85ec29ea527b3966decf1c6fc"; 60 | json[0].parents[0].url = "https://api.github.com/repos/tomnomnom/gron/commits/ca66db90d96332a85ec29ea527b3966decf1c6fc"; 61 | json[0].sha = "cfade78b10e9c0cb9e4e0a902f5383ee4e5efc9e"; 62 | json[0].url = "https://api.github.com/repos/tomnomnom/gron/commits/cfade78b10e9c0cb9e4e0a902f5383ee4e5efc9e"; 63 | -------------------------------------------------------------------------------- /testdata/github.jgron: -------------------------------------------------------------------------------- 1 | [[],[]] 2 | [[0],{}] 3 | [[0,"author"],{}] 4 | [[0,"author","avatar_url"],"https://avatars.githubusercontent.com/u/58276?v=3"] 5 | [[0,"author","events_url"],"https://api.github.com/users/tomnomnom/events{/privacy}"] 6 | [[0,"author","followers_url"],"https://api.github.com/users/tomnomnom/followers"] 7 | [[0,"author","following_url"],"https://api.github.com/users/tomnomnom/following{/other_user}"] 8 | [[0,"author","gists_url"],"https://api.github.com/users/tomnomnom/gists{/gist_id}"] 9 | [[0,"author","gravatar_id"],""] 10 | [[0,"author","html_url"],"https://github.com/tomnomnom"] 11 | [[0,"author","id"],58276] 12 | [[0,"author","login"],"tomnomnom"] 13 | [[0,"author","organizations_url"],"https://api.github.com/users/tomnomnom/orgs"] 14 | [[0,"author","received_events_url"],"https://api.github.com/users/tomnomnom/received_events"] 15 | [[0,"author","repos_url"],"https://api.github.com/users/tomnomnom/repos"] 16 | [[0,"author","site_admin"],false] 17 | [[0,"author","starred_url"],"https://api.github.com/users/tomnomnom/starred{/owner}{/repo}"] 18 | [[0,"author","subscriptions_url"],"https://api.github.com/users/tomnomnom/subscriptions"] 19 | [[0,"author","type"],"User"] 20 | [[0,"author","url"],"https://api.github.com/users/tomnomnom"] 21 | [[0,"comments_url"],"https://api.github.com/repos/tomnomnom/gron/commits/cfade78b10e9c0cb9e4e0a902f5383ee4e5efc9e/comments"] 22 | [[0,"commit"],{}] 23 | [[0,"commit","author"],{}] 24 | [[0,"commit","author","date"],"2016-09-09T20:08:58Z"] 25 | [[0,"commit","author","email"],"mail@tomnomnom.com"] 26 | [[0,"commit","author","name"],"Tom Hudson"] 27 | [[0,"commit","comment_count"],0] 28 | [[0,"commit","committer"],{}] 29 | [[0,"commit","committer","date"],"2016-09-09T20:08:58Z"] 30 | [[0,"commit","committer","email"],"mail@tomnomnom.com"] 31 | [[0,"commit","committer","name"],"Tom Hudson"] 32 | [[0,"commit","message"],"Unexports statements add/addFull methods"] 33 | [[0,"commit","tree"],{}] 34 | [[0,"commit","tree","sha"],"536ec3c40a0510cd048921cd8757c401aad94b91"] 35 | [[0,"commit","tree","url"],"https://api.github.com/repos/tomnomnom/gron/git/trees/536ec3c40a0510cd048921cd8757c401aad94b91"] 36 | [[0,"commit","url"],"https://api.github.com/repos/tomnomnom/gron/git/commits/cfade78b10e9c0cb9e4e0a902f5383ee4e5efc9e"] 37 | [[0,"committer"],{}] 38 | [[0,"committer","avatar_url"],"https://avatars.githubusercontent.com/u/58276?v=3"] 39 | [[0,"committer","events_url"],"https://api.github.com/users/tomnomnom/events{/privacy}"] 40 | [[0,"committer","followers_url"],"https://api.github.com/users/tomnomnom/followers"] 41 | [[0,"committer","following_url"],"https://api.github.com/users/tomnomnom/following{/other_user}"] 42 | [[0,"committer","gists_url"],"https://api.github.com/users/tomnomnom/gists{/gist_id}"] 43 | [[0,"committer","gravatar_id"],""] 44 | [[0,"committer","html_url"],"https://github.com/tomnomnom"] 45 | [[0,"committer","id"],58276] 46 | [[0,"committer","login"],"tomnomnom"] 47 | [[0,"committer","organizations_url"],"https://api.github.com/users/tomnomnom/orgs"] 48 | [[0,"committer","received_events_url"],"https://api.github.com/users/tomnomnom/received_events"] 49 | [[0,"committer","repos_url"],"https://api.github.com/users/tomnomnom/repos"] 50 | [[0,"committer","site_admin"],false] 51 | [[0,"committer","starred_url"],"https://api.github.com/users/tomnomnom/starred{/owner}{/repo}"] 52 | [[0,"committer","subscriptions_url"],"https://api.github.com/users/tomnomnom/subscriptions"] 53 | [[0,"committer","type"],"User"] 54 | [[0,"committer","url"],"https://api.github.com/users/tomnomnom"] 55 | [[0,"html_url"],"https://github.com/tomnomnom/gron/commit/cfade78b10e9c0cb9e4e0a902f5383ee4e5efc9e"] 56 | [[0,"parents"],[]] 57 | [[0,"parents",0],{}] 58 | [[0,"parents",0,"html_url"],"https://github.com/tomnomnom/gron/commit/ca66db90d96332a85ec29ea527b3966decf1c6fc"] 59 | [[0,"parents",0,"sha"],"ca66db90d96332a85ec29ea527b3966decf1c6fc"] 60 | [[0,"parents",0,"url"],"https://api.github.com/repos/tomnomnom/gron/commits/ca66db90d96332a85ec29ea527b3966decf1c6fc"] 61 | [[0,"sha"],"cfade78b10e9c0cb9e4e0a902f5383ee4e5efc9e"] 62 | [[0,"url"],"https://api.github.com/repos/tomnomnom/gron/commits/cfade78b10e9c0cb9e4e0a902f5383ee4e5efc9e"] 63 | -------------------------------------------------------------------------------- /testdata/github.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "sha": "cfade78b10e9c0cb9e4e0a902f5383ee4e5efc9e", 4 | "commit": { 5 | "author": { 6 | "name": "Tom Hudson", 7 | "email": "mail@tomnomnom.com", 8 | "date": "2016-09-09T20:08:58Z" 9 | }, 10 | "committer": { 11 | "name": "Tom Hudson", 12 | "email": "mail@tomnomnom.com", 13 | "date": "2016-09-09T20:08:58Z" 14 | }, 15 | "message": "Unexports statements add/addFull methods", 16 | "tree": { 17 | "sha": "536ec3c40a0510cd048921cd8757c401aad94b91", 18 | "url": "https://api.github.com/repos/tomnomnom/gron/git/trees/536ec3c40a0510cd048921cd8757c401aad94b91" 19 | }, 20 | "url": "https://api.github.com/repos/tomnomnom/gron/git/commits/cfade78b10e9c0cb9e4e0a902f5383ee4e5efc9e", 21 | "comment_count": 0 22 | }, 23 | "url": "https://api.github.com/repos/tomnomnom/gron/commits/cfade78b10e9c0cb9e4e0a902f5383ee4e5efc9e", 24 | "html_url": "https://github.com/tomnomnom/gron/commit/cfade78b10e9c0cb9e4e0a902f5383ee4e5efc9e", 25 | "comments_url": "https://api.github.com/repos/tomnomnom/gron/commits/cfade78b10e9c0cb9e4e0a902f5383ee4e5efc9e/comments", 26 | "author": { 27 | "login": "tomnomnom", 28 | "id": 58276, 29 | "avatar_url": "https://avatars.githubusercontent.com/u/58276?v=3", 30 | "gravatar_id": "", 31 | "url": "https://api.github.com/users/tomnomnom", 32 | "html_url": "https://github.com/tomnomnom", 33 | "followers_url": "https://api.github.com/users/tomnomnom/followers", 34 | "following_url": "https://api.github.com/users/tomnomnom/following{/other_user}", 35 | "gists_url": "https://api.github.com/users/tomnomnom/gists{/gist_id}", 36 | "starred_url": "https://api.github.com/users/tomnomnom/starred{/owner}{/repo}", 37 | "subscriptions_url": "https://api.github.com/users/tomnomnom/subscriptions", 38 | "organizations_url": "https://api.github.com/users/tomnomnom/orgs", 39 | "repos_url": "https://api.github.com/users/tomnomnom/repos", 40 | "events_url": "https://api.github.com/users/tomnomnom/events{/privacy}", 41 | "received_events_url": "https://api.github.com/users/tomnomnom/received_events", 42 | "type": "User", 43 | "site_admin": false 44 | }, 45 | "committer": { 46 | "login": "tomnomnom", 47 | "id": 58276, 48 | "avatar_url": "https://avatars.githubusercontent.com/u/58276?v=3", 49 | "gravatar_id": "", 50 | "url": "https://api.github.com/users/tomnomnom", 51 | "html_url": "https://github.com/tomnomnom", 52 | "followers_url": "https://api.github.com/users/tomnomnom/followers", 53 | "following_url": "https://api.github.com/users/tomnomnom/following{/other_user}", 54 | "gists_url": "https://api.github.com/users/tomnomnom/gists{/gist_id}", 55 | "starred_url": "https://api.github.com/users/tomnomnom/starred{/owner}{/repo}", 56 | "subscriptions_url": "https://api.github.com/users/tomnomnom/subscriptions", 57 | "organizations_url": "https://api.github.com/users/tomnomnom/orgs", 58 | "repos_url": "https://api.github.com/users/tomnomnom/repos", 59 | "events_url": "https://api.github.com/users/tomnomnom/events{/privacy}", 60 | "received_events_url": "https://api.github.com/users/tomnomnom/received_events", 61 | "type": "User", 62 | "site_admin": false 63 | }, 64 | "parents": [ 65 | { 66 | "sha": "ca66db90d96332a85ec29ea527b3966decf1c6fc", 67 | "url": "https://api.github.com/repos/tomnomnom/gron/commits/ca66db90d96332a85ec29ea527b3966decf1c6fc", 68 | "html_url": "https://github.com/tomnomnom/gron/commit/ca66db90d96332a85ec29ea527b3966decf1c6fc" 69 | } 70 | ] 71 | } 72 | ] 73 | -------------------------------------------------------------------------------- /testdata/grep-separators.gron: -------------------------------------------------------------------------------- 1 | json.contact.email = "mail@tomnomnom.com"; 2 | json.contact.twitter = "@TomNomNom"; 3 | -- 4 | json.likes[2] = "meat"; 5 | json.name = "Tom"; 6 | -------------------------------------------------------------------------------- /testdata/grep-separators.json: -------------------------------------------------------------------------------- 1 | { 2 | "contact": { 3 | "email": "mail@tomnomnom.com", 4 | "twitter": "@TomNomNom" 5 | }, 6 | "likes": [ 7 | null, 8 | null, 9 | "meat" 10 | ], 11 | "name": "Tom" 12 | } 13 | -------------------------------------------------------------------------------- /testdata/invalid-type-mismatch.gron: -------------------------------------------------------------------------------- 1 | json.foo = {}; 2 | json.foo = []; 3 | -------------------------------------------------------------------------------- /testdata/invalid-value.gron: -------------------------------------------------------------------------------- 1 | json = {}; 2 | json.contact = {}; 3 | json.contact.email = "mail@tomnomnom.com"; 4 | json.contact.twitter = "@TomNomNom"; 5 | json.github = "https://github.com/tomnomnom/"; 6 | json.likes = [; 7 | json.likes[0] = "code"; 8 | json.likes[1] = "cheese"; 9 | json.likes[2] = "meat"; 10 | json.name = "Tom"; 11 | -------------------------------------------------------------------------------- /testdata/long-stream.json: -------------------------------------------------------------------------------- 1 | {"details":[{"_id":"5ae0aa34b3f979b9b85509e0","index":0,"guid":"164542db-58d9-4d63-aacd-fbf3a7739474","isActive":true,"balance":"$2,644.04","picture":"http://placehold.it/32x32","age":38,"eyeColor":"blue","name":{"first":"Tracy","last":"Scott"},"company":"QUOTEZART","email":"tracy.scott@quotezart.io","phone":"+1 (998) 407-3468","address":"324 Cornelia Street, Echo, Indiana, 8046","about":"Nostrud magna ullamco id amet et incididunt officia dolor cillum do. Excepteur amet aliquip non veniam amet est nisi incididunt enim aliqua ad occaecat exercitation. Duis cupidatat ea voluptate consequat anim irure incididunt consequat consectetur amet. Enim exercitation ea eiusmod aliqua occaecat proident proident occaecat incididunt fugiat sint. Eu velit incididunt dolore voluptate in deserunt mollit officia.","registered":"Saturday, October 7, 2017 2:37 PM","latitude":"-19.206275","longitude":"-57.387994","tags":["quis","elit","minim","quis","ut","quis","et","laboris","et","quis"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Gwendolyn Barnett"},{"id":1,"name":"Hill Patrick"},{"id":2,"name":"Corinne Pearson"},{"id":3,"name":"Nona Walters"},{"id":4,"name":"Beach Hart"},{"id":5,"name":"Figueroa Russell"},{"id":6,"name":"Eve Cobb"},{"id":7,"name":"Sanchez Bradshaw"},{"id":8,"name":"Irma Turner"},{"id":9,"name":"Keisha Walls"}],"greeting":"Hello, Tracy! You have 10 unread messages.","favoriteFruit":"apple"},{"_id":"5ae0aa342f534020c0f882a2","index":1,"guid":"e3ceb40a-9dbd-4769-96cd-283c227952d4","isActive":true,"balance":"$2,449.54","picture":"http://placehold.it/32x32","age":33,"eyeColor":"green","name":{"first":"Nora","last":"Gates"},"company":"ENTALITY","email":"nora.gates@entality.name","phone":"+1 (845) 592-3644","address":"904 Randolph Street, Cowiche, District Of Columbia, 6897","about":"Duis in mollit ex consectetur laboris commodo ad laboris do officia. Proident aute officia aliquip mollit incididunt amet est deserunt. Sunt magna ea sit reprehenderit. Enim et non excepteur dolor proident quis dolor ea. Non dolore tempor esse aliqua ipsum cupidatat quis cupidatat sint veniam pariatur.","registered":"Wednesday, September 3, 2014 5:55 PM","latitude":"-17.863169","longitude":"175.110579","tags":["sunt","ut","sint","labore","adipisicing","velit","esse","consectetur","enim","ad"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Meagan Daugherty"},{"id":1,"name":"Morin Delacruz"},{"id":2,"name":"Robin Blanchard"},{"id":3,"name":"Georgia Mays"},{"id":4,"name":"Cheri Soto"},{"id":5,"name":"Hobbs Lucas"},{"id":6,"name":"Maritza Garza"},{"id":7,"name":"Kathy Whitney"},{"id":8,"name":"Burch Cortez"},{"id":9,"name":"Jenny Campos"}],"greeting":"Hello, Nora! You have 10 unread messages.","favoriteFruit":"apple"},{"_id":"5ae0aa34866f48fd0a12505e","index":2,"guid":"ddcd3d1e-8706-48bf-9dea-554b69706120","isActive":false,"balance":"$3,999.63","picture":"http://placehold.it/32x32","age":20,"eyeColor":"green","name":{"first":"Beatrice","last":"Foley"},"company":"KANGLE","email":"beatrice.foley@kangle.biz","phone":"+1 (832) 487-2607","address":"813 Garden Place, Hackneyville, Kentucky, 4301","about":"Minim dolor eiusmod ipsum laborum proident. Ad cillum ullamco cupidatat dolor proident irure veniam proident nostrud. Enim non cillum nostrud tempor dolor id dolore laboris mollit non adipisicing. In dolore amet dolor incididunt non dolor magna pariatur anim exercitation eiusmod reprehenderit. Ex elit aute eiusmod ullamco amet consequat nostrud est. Ullamco cupidatat adipisicing amet commodo non ipsum aliqua esse excepteur reprehenderit irure quis consectetur enim. Eu aliqua occaecat do amet exercitation ad fugiat.","registered":"Thursday, August 27, 2015 6:31 PM","latitude":"-20.102055","longitude":"-159.81829","tags":["reprehenderit","et","magna","aliquip","sit","pariatur","amet","labore","Lorem","aute"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Lawrence Ratliff"},{"id":1,"name":"Debbie Kent"},{"id":2,"name":"Lynda Morse"},{"id":3,"name":"Wiley Stephenson"},{"id":4,"name":"Mclaughlin Hines"},{"id":5,"name":"Erin Ashley"},{"id":6,"name":"Boyer Dotson"},{"id":7,"name":"Decker Rollins"},{"id":8,"name":"Eddie Lane"},{"id":9,"name":"Cecile Lindsey"}],"greeting":"Hello, Beatrice! You have 7 unread messages.","favoriteFruit":"banana"},{"_id":"5ae0aa34dc3fbdc4ed72a342","index":3,"guid":"5dbc0677-c792-420e-b31e-2ba3e520b9bf","isActive":false,"balance":"$1,227.64","picture":"http://placehold.it/32x32","age":20,"eyeColor":"brown","name":{"first":"Sherrie","last":"Brock"},"company":"NAVIR","email":"sherrie.brock@navir.us","phone":"+1 (886) 472-3610","address":"916 Oakland Place, Rodman, Louisiana, 8864","about":"Eiusmod est minim ullamco minim tempor sunt. Do aliquip tempor et nisi aute do ullamco. Sint non mollit fugiat Lorem laboris pariatur et commodo labore et tempor. Non officia tempor cupidatat aute deserunt ea ut laboris consequat magna.","registered":"Saturday, November 12, 2016 3:31 AM","latitude":"54.818965","longitude":"117.261723","tags":["veniam","voluptate","esse","amet","deserunt","aute","elit","anim","est","excepteur"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Sofia Ray"},{"id":1,"name":"Ronda Bates"},{"id":2,"name":"Robert Blankenship"},{"id":3,"name":"Bowers Potts"},{"id":4,"name":"Holcomb Nieves"},{"id":5,"name":"Marquita Glover"},{"id":6,"name":"Manuela Rocha"},{"id":7,"name":"Carey Fitzpatrick"},{"id":8,"name":"Joann Singleton"},{"id":9,"name":"Vincent Johns"}],"greeting":"Hello, Sherrie! You have 8 unread messages.","favoriteFruit":"strawberry"},{"_id":"5ae0aa348f55f735b48cfa46","index":4,"guid":"a1944b22-c22f-49a3-8ac4-45153a3d1de4","isActive":true,"balance":"$2,403.86","picture":"http://placehold.it/32x32","age":22,"eyeColor":"brown","name":{"first":"Stuart","last":"Mendoza"},"company":"SNOWPOKE","email":"stuart.mendoza@snowpoke.biz","phone":"+1 (940) 532-2198","address":"806 High Street, Dunnavant, Idaho, 5507","about":"Aute adipisicing deserunt et elit consequat deserunt amet amet officia aliqua in. Et qui minim ea sit exercitation aliquip do laborum consectetur voluptate eiusmod irure. Exercitation reprehenderit eiusmod proident fugiat laborum in aliqua laboris commodo sint et. Amet qui labore culpa excepteur magna voluptate anim mollit eiusmod. Officia mollit eu et laborum reprehenderit ullamco. Proident incididunt incididunt cillum laborum officia veniam aute voluptate officia irure elit.","registered":"Wednesday, November 12, 2014 9:10 AM","latitude":"-4.553307","longitude":"-92.981375","tags":["voluptate","fugiat","non","fugiat","cupidatat","deserunt","occaecat","dolor","aute","fugiat"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Valeria Burgess"},{"id":1,"name":"Cleveland Noel"},{"id":2,"name":"Arline Oneill"},{"id":3,"name":"Tasha Johnston"},{"id":4,"name":"Graciela Mosley"},{"id":5,"name":"Bartlett Gonzales"},{"id":6,"name":"Darla Bond"},{"id":7,"name":"Gail Hatfield"},{"id":8,"name":"Cecilia Clay"},{"id":9,"name":"Keri May"}],"greeting":"Hello, Stuart! You have 9 unread messages.","favoriteFruit":"apple"},{"_id":"5ae0aa342f229045f41285f7","index":5,"guid":"a087b56e-cd7c-4832-9c07-ec80217d5355","isActive":false,"balance":"$3,736.67","picture":"http://placehold.it/32x32","age":27,"eyeColor":"blue","name":{"first":"Marjorie","last":"Cunningham"},"company":"IMMUNICS","email":"marjorie.cunningham@immunics.com","phone":"+1 (807) 427-2665","address":"649 Whitty Lane, Thatcher, Michigan, 9747","about":"Aliquip quis proident consequat magna id ullamco nulla culpa laborum. Quis laborum laborum adipisicing duis duis eu sint exercitation deserunt cillum ea. Incididunt laborum magna Lorem quis officia sint cillum commodo laboris exercitation proident proident. Sit sunt nostrud sint duis. Cillum minim cupidatat sunt ipsum eiusmod ipsum fugiat fugiat culpa consequat. Esse irure consectetur ad officia Lorem eu culpa est ad deserunt velit amet exercitation aute.","registered":"Tuesday, October 7, 2014 5:24 PM","latitude":"6.552001","longitude":"-139.202433","tags":["ipsum","est","officia","eiusmod","ea","fugiat","quis","voluptate","proident","dolor"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Glenna Beck"},{"id":1,"name":"Castaneda Spears"},{"id":2,"name":"Justice Michael"},{"id":3,"name":"Shelton Parsons"},{"id":4,"name":"Robertson Good"},{"id":5,"name":"Sheena Burch"},{"id":6,"name":"Tessa Miranda"},{"id":7,"name":"Perkins Barker"},{"id":8,"name":"Maribel Lester"},{"id":9,"name":"Hodge Cabrera"}],"greeting":"Hello, Marjorie! You have 9 unread messages.","favoriteFruit":"banana"},{"_id":"5ae0aa3482a305f0781a3aba","index":6,"guid":"ccfdf87d-b381-4b64-b517-cd60a8fc9f04","isActive":false,"balance":"$2,116.04","picture":"http://placehold.it/32x32","age":21,"eyeColor":"green","name":{"first":"Yvonne","last":"Duffy"},"company":"KENEGY","email":"yvonne.duffy@kenegy.co.uk","phone":"+1 (956) 575-2780","address":"742 Bergen Court, Mahtowa, Alaska, 4598","about":"Anim nulla est eu laborum proident et. Sunt anim dolore voluptate non laboris sint mollit laborum exercitation enim aliqua exercitation. Veniam consectetur excepteur esse nostrud cupidatat aute nulla aute ad id mollit. Enim exercitation labore culpa voluptate in ea nulla nulla ex. Id tempor nisi ad proident dolore est cillum eiusmod dolor veniam consectetur labore. Mollit velit cupidatat est mollit adipisicing est exercitation. Veniam aliqua reprehenderit ea tempor.","registered":"Monday, December 8, 2014 2:57 AM","latitude":"23.863564","longitude":"150.896594","tags":["voluptate","ex","consequat","in","sit","nulla","commodo","adipisicing","nulla","commodo"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Clemons Snyder"},{"id":1,"name":"Bernard Navarro"},{"id":2,"name":"Burton Burnett"},{"id":3,"name":"Suzette Callahan"},{"id":4,"name":"William Hill"},{"id":5,"name":"Mays Larsen"},{"id":6,"name":"Joanna Graham"},{"id":7,"name":"Fowler Berry"},{"id":8,"name":"Lacey Hayden"},{"id":9,"name":"Shauna Frederick"}],"greeting":"Hello, Yvonne! You have 6 unread messages.","favoriteFruit":"banana"},{"_id":"5ae0aa349dcd18765c1626dc","index":7,"guid":"425bc7f1-467f-4ed6-be2c-ccbcd9555674","isActive":false,"balance":"$3,403.00","picture":"http://placehold.it/32x32","age":33,"eyeColor":"blue","name":{"first":"Dana","last":"Reed"},"company":"NETPLAX","email":"dana.reed@netplax.ca","phone":"+1 (988) 463-2342","address":"980 Central Avenue, Strykersville, Maryland, 873","about":"Sunt nisi duis dolor cupidatat magna quis. Pariatur eu id commodo ea eiusmod dolore cillum veniam voluptate. Anim ex occaecat sit ipsum proident tempor quis ipsum laborum ad. Minim ut quis enim incididunt aliqua tempor. Ullamco exercitation consectetur quis fugiat cupidatat esse est consequat ad aliqua duis ea consectetur consequat. Id ea commodo minim aute.","registered":"Sunday, March 22, 2015 7:03 PM","latitude":"61.534583","longitude":"123.100188","tags":["non","elit","Lorem","commodo","enim","dolor","sint","qui","aliqua","cupidatat"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Chan Keller"},{"id":1,"name":"Mary Dyer"},{"id":2,"name":"Genevieve Whitfield"},{"id":3,"name":"Meyers York"},{"id":4,"name":"James Carr"},{"id":5,"name":"Rollins Frazier"},{"id":6,"name":"Aurora Nguyen"},{"id":7,"name":"Shawn Jacobs"},{"id":8,"name":"Reba Mcfarland"},{"id":9,"name":"Paige Pitts"}],"greeting":"Hello, Dana! You have 9 unread messages.","favoriteFruit":"banana"},{"_id":"5ae0aa34d760d6da02fde756","index":8,"guid":"be208b32-be08-4c6d-86ea-da589d1ec977","isActive":false,"balance":"$1,012.38","picture":"http://placehold.it/32x32","age":23,"eyeColor":"brown","name":{"first":"Schmidt","last":"Sweeney"},"company":"PROWASTE","email":"schmidt.sweeney@prowaste.info","phone":"+1 (994) 493-3642","address":"245 Veterans Avenue, Catherine, Arkansas, 1444","about":"Voluptate ut deserunt nostrud nulla anim fugiat occaecat fugiat nisi anim commodo minim consequat. Tempor velit amet cillum aliquip nisi ut. Eu sunt excepteur amet dolor excepteur laborum. Enim minim elit irure proident pariatur ex nulla aute sunt.","registered":"Wednesday, April 6, 2016 12:10 PM","latitude":"22.555683","longitude":"63.232647","tags":["aute","deserunt","ad","sint","quis","tempor","sunt","consectetur","labore","Lorem"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Browning House"},{"id":1,"name":"Beverly Hoover"},{"id":2,"name":"Mitchell Figueroa"},{"id":3,"name":"Blackburn Camacho"},{"id":4,"name":"Leila Doyle"},{"id":5,"name":"Lee Montoya"},{"id":6,"name":"Beck Velasquez"},{"id":7,"name":"Charlotte Bird"},{"id":8,"name":"Lillie Velez"},{"id":9,"name":"Williamson Elliott"}],"greeting":"Hello, Schmidt! You have 6 unread messages.","favoriteFruit":"apple"},{"_id":"5ae0aa34a2574825a9fad671","index":9,"guid":"b3a104cf-4361-421c-9e13-91ae146e517d","isActive":true,"balance":"$1,454.62","picture":"http://placehold.it/32x32","age":38,"eyeColor":"blue","name":{"first":"Davenport","last":"Marquez"},"company":"MUSANPOLY","email":"davenport.marquez@musanpoly.me","phone":"+1 (843) 438-2469","address":"109 Victor Road, Fruitdale, American Samoa, 8856","about":"Culpa aliquip Lorem elit laborum aliquip laboris dolore non ea. Est occaecat do minim pariatur pariatur mollit velit laborum eu consectetur qui eiusmod. Consectetur nostrud occaecat dolore sunt mollit. Anim excepteur et nulla consequat laborum dolor Lorem ad.","registered":"Sunday, February 16, 2014 11:28 PM","latitude":"5.32883","longitude":"-128.790048","tags":["veniam","cillum","non","qui","ullamco","quis","consequat","incididunt","laborum","deserunt"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Beard Gomez"},{"id":1,"name":"Elaine Lamb"},{"id":2,"name":"Gallegos Merritt"},{"id":3,"name":"Margarita Shepherd"},{"id":4,"name":"Mia Griffin"},{"id":5,"name":"Deena Foreman"},{"id":6,"name":"Muriel Howe"},{"id":7,"name":"Wilder Chang"},{"id":8,"name":"Pearlie Ford"},{"id":9,"name":"Tabatha Mathews"}],"greeting":"Hello, Davenport! You have 10 unread messages.","favoriteFruit":"apple"},{"_id":"5ae0aa34ce8ffcd27683f469","index":10,"guid":"d6d9c317-c71c-48ff-a522-905d54cef18b","isActive":true,"balance":"$2,197.16","picture":"http://placehold.it/32x32","age":39,"eyeColor":"green","name":{"first":"Julianne","last":"Miles"},"company":"EXIAND","email":"julianne.miles@exiand.org","phone":"+1 (981) 429-3214","address":"560 Louis Place, Sperryville, Vermont, 7450","about":"Aute irure Lorem laborum cillum Lorem ad dolore proident tempor in non. Excepteur fugiat amet proident ut exercitation adipisicing cupidatat esse. Occaecat cillum labore dolore cupidatat consectetur est exercitation mollit dolor dolore.","registered":"Wednesday, June 14, 2017 11:19 AM","latitude":"-32.254528","longitude":"-97.815247","tags":["consectetur","cupidatat","est","esse","anim","et","tempor","et","ex","fugiat"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Trevino Nash"},{"id":1,"name":"Mills Conley"},{"id":2,"name":"Deirdre Cruz"},{"id":3,"name":"Marsh Webster"},{"id":4,"name":"Frye Johnson"},{"id":5,"name":"Jeanine Craft"},{"id":6,"name":"Fern Richmond"},{"id":7,"name":"Janelle Ramsey"},{"id":8,"name":"Kirby Valentine"},{"id":9,"name":"Lessie Acevedo"}],"greeting":"Hello, Julianne! You have 8 unread messages.","favoriteFruit":"apple"},{"_id":"5ae0aa34d5b29d6058c2450f","index":11,"guid":"925f4c31-5a6b-4cf2-b90d-fb0ee37680a6","isActive":false,"balance":"$1,111.42","picture":"http://placehold.it/32x32","age":34,"eyeColor":"blue","name":{"first":"Elva","last":"Woods"},"company":"MAXIMIND","email":"elva.woods@maximind.tv","phone":"+1 (883) 569-3812","address":"540 Trucklemans Lane, Stollings, Oregon, 2549","about":"Consequat ut deserunt in esse do nisi. Aliquip laboris ex nulla excepteur occaecat et nulla. Incididunt enim irure officia ex aute nulla est consectetur aute eiusmod. Ea ipsum magna nisi quis ex labore qui occaecat eu aute culpa adipisicing aute cupidatat. Consequat mollit dolore ex deserunt. Deserunt do eiusmod Lorem anim laborum dolor esse aute laboris dolor officia esse officia. Cupidatat consectetur veniam ipsum et mollit cupidatat ullamco et non officia exercitation nisi ea.","registered":"Saturday, July 25, 2015 1:20 PM","latitude":"-14.967803","longitude":"160.339242","tags":["incididunt","nulla","aliquip","et","quis","voluptate","aliqua","qui","in","ea"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Compton Holt"},{"id":1,"name":"Christy Padilla"},{"id":2,"name":"Roy Boyer"},{"id":3,"name":"Celina Wright"},{"id":4,"name":"Patton Fowler"},{"id":5,"name":"Juanita Blair"},{"id":6,"name":"Thornton Townsend"},{"id":7,"name":"Mcintyre William"},{"id":8,"name":"Lorena Mckee"},{"id":9,"name":"Alisha Mullen"}],"greeting":"Hello, Elva! You have 10 unread messages.","favoriteFruit":"banana"},{"_id":"5ae0aa34040ef52f7d92ee6a","index":12,"guid":"9ccba22d-2de3-4784-b291-93a18c017a82","isActive":false,"balance":"$1,147.66","picture":"http://placehold.it/32x32","age":26,"eyeColor":"green","name":{"first":"Lizzie","last":"Melton"},"company":"FUTURITY","email":"lizzie.melton@futurity.io","phone":"+1 (875) 595-2680","address":"584 Indiana Place, Blue, New Jersey, 420","about":"Culpa culpa irure duis fugiat magna cillum ut aliqua incididunt. Deserunt nostrud cillum duis reprehenderit ad duis voluptate dolore. Elit officia ullamco minim nisi officia eu eu Lorem sint. Est aute quis occaecat sunt. Excepteur minim commodo exercitation consectetur eiusmod aute elit proident nulla enim ullamco enim. Tempor labore duis ex cupidatat et.","registered":"Friday, October 17, 2014 2:39 PM","latitude":"-52.473183","longitude":"111.529289","tags":["enim","ut","ullamco","tempor","commodo","esse","nisi","eiusmod","exercitation","sit"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Sasha Porter"},{"id":1,"name":"Pittman Wilkerson"},{"id":2,"name":"Lloyd Carter"},{"id":3,"name":"Mathews Armstrong"},{"id":4,"name":"Geraldine Harrison"},{"id":5,"name":"Margo Giles"},{"id":6,"name":"Ewing Pollard"},{"id":7,"name":"Esperanza Simon"},{"id":8,"name":"Christa Wiggins"},{"id":9,"name":"Lorraine Wilkinson"}],"greeting":"Hello, Lizzie! You have 9 unread messages.","favoriteFruit":"banana"},{"_id":"5ae0aa3468cb0abe9cf31577","index":13,"guid":"3a1566ab-fb93-417b-8c4f-ead8269d6cc8","isActive":false,"balance":"$1,824.72","picture":"http://placehold.it/32x32","age":22,"eyeColor":"blue","name":{"first":"Ford","last":"Randolph"},"company":"INSECTUS","email":"ford.randolph@insectus.name","phone":"+1 (982) 589-2146","address":"231 Centre Street, Coalmont, Washington, 5554","about":"Commodo consequat occaecat ad ea ut id reprehenderit laborum elit ullamco magna. Non aliquip nisi fugiat anim veniam consequat eu qui esse sint magna. Cupidatat officia sunt mollit irure sit nulla excepteur dolor nulla. Fugiat elit deserunt mollit est fugiat laborum magna sint ullamco et in. Commodo voluptate aliqua sit dolore ut excepteur laborum qui dolore.","registered":"Friday, February 28, 2014 9:04 PM","latitude":"-85.796706","longitude":"82.662603","tags":["amet","exercitation","proident","do","incididunt","nisi","sunt","ullamco","commodo","reprehenderit"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Delaney Knowles"},{"id":1,"name":"Simone Nichols"},{"id":2,"name":"Tate Brewer"},{"id":3,"name":"Madeline Paul"},{"id":4,"name":"Jeanette Roberson"},{"id":5,"name":"Sallie Holden"},{"id":6,"name":"Clements Hinton"},{"id":7,"name":"Lourdes Tyler"},{"id":8,"name":"Aline Davidson"},{"id":9,"name":"Tia Collier"}],"greeting":"Hello, Ford! You have 9 unread messages.","favoriteFruit":"banana"},{"_id":"5ae0aa34d4c1ab9766cdd713","index":14,"guid":"0665f792-8282-4efd-b409-9c791ecb1091","isActive":true,"balance":"$2,174.56","picture":"http://placehold.it/32x32","age":35,"eyeColor":"brown","name":{"first":"Grace","last":"Fuentes"},"company":"ZINCA","email":"grace.fuentes@zinca.biz","phone":"+1 (802) 553-2237","address":"166 Sackman Street, Brownsville, Utah, 7296","about":"Lorem incididunt deserunt dolor nulla. Minim sit eu do commodo. Sint aliqua et exercitation duis Lorem laborum irure enim quis pariatur laboris incididunt veniam magna. Ea labore eiusmod incididunt fugiat. Sunt cillum nisi culpa Lorem aliqua nostrud quis dolore incididunt nostrud amet anim cupidatat. Ex pariatur enim esse sunt eiusmod. Aliquip consectetur reprehenderit nisi labore eu nostrud sint consectetur.","registered":"Friday, December 18, 2015 11:18 PM","latitude":"-66.13255","longitude":"179.72211","tags":["ea","sunt","elit","Lorem","nisi","aute","do","fugiat","in","fugiat"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Dixon Fletcher"},{"id":1,"name":"Lambert Ingram"},{"id":2,"name":"Mendez Pate"},{"id":3,"name":"Ray Ward"},{"id":4,"name":"Heidi Cardenas"},{"id":5,"name":"Barton Meyer"},{"id":6,"name":"Velma Park"},{"id":7,"name":"Patricia Lynch"},{"id":8,"name":"Eugenia Grant"},{"id":9,"name":"York Sloan"}],"greeting":"Hello, Grace! You have 9 unread messages.","favoriteFruit":"apple"},{"_id":"5ae0aa343b56f5acad827190","index":15,"guid":"4c8eb809-8024-490f-b95a-0cace6949320","isActive":true,"balance":"$2,489.41","picture":"http://placehold.it/32x32","age":33,"eyeColor":"green","name":{"first":"Laurel","last":"Contreras"},"company":"DOGNOSIS","email":"laurel.contreras@dognosis.us","phone":"+1 (917) 546-3266","address":"147 Strong Place, Hayes, New Mexico, 2530","about":"Cupidatat nisi quis irure nostrud labore eu et voluptate. Nisi eiusmod enim dolor cillum. Ea dolor et ea adipisicing minim pariatur ea ad minim ad eu amet ea.","registered":"Saturday, December 24, 2016 9:20 AM","latitude":"-54.412123","longitude":"-169.324571","tags":["reprehenderit","quis","irure","dolor","do","adipisicing","veniam","in","aliqua","ipsum"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Laurie Becker"},{"id":1,"name":"Butler Coffey"},{"id":2,"name":"Ruthie Benjamin"},{"id":3,"name":"Jean Lee"},{"id":4,"name":"Lowe Warren"},{"id":5,"name":"Zelma Hudson"},{"id":6,"name":"Wiggins Schwartz"},{"id":7,"name":"Brewer Goodman"},{"id":8,"name":"Melanie Haney"},{"id":9,"name":"Alyson Brooks"}],"greeting":"Hello, Laurel! You have 10 unread messages.","favoriteFruit":"banana"},{"_id":"5ae0aa3405bf5c4f96c29069","index":16,"guid":"e218adf6-22b7-4e1e-a6f4-31234460fd8e","isActive":true,"balance":"$1,324.19","picture":"http://placehold.it/32x32","age":31,"eyeColor":"green","name":{"first":"Ginger","last":"Pennington"},"company":"CYTREX","email":"ginger.pennington@cytrex.biz","phone":"+1 (827) 512-3179","address":"726 Lake Street, Shindler, Nevada, 4750","about":"Deserunt elit et labore minim officia cupidatat exercitation esse adipisicing enim amet culpa in adipisicing. Amet eiusmod velit amet laboris deserunt minim. Anim duis deserunt proident ex consequat incididunt commodo sint excepteur Lorem proident esse duis. Ad adipisicing veniam anim Lorem sit eu laboris ex nostrud qui nostrud ipsum. Labore exercitation excepteur voluptate aliqua labore anim id fugiat.","registered":"Saturday, December 9, 2017 10:11 AM","latitude":"14.567831","longitude":"66.731219","tags":["incididunt","dolor","commodo","eiusmod","est","est","sint","ad","ut","minim"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Iva Guerrero"},{"id":1,"name":"Cathleen Young"},{"id":2,"name":"Welch Pierce"},{"id":3,"name":"Tyson Raymond"},{"id":4,"name":"Josefa Crawford"},{"id":5,"name":"Cross Burks"},{"id":6,"name":"Crosby Brady"},{"id":7,"name":"Barry Guthrie"},{"id":8,"name":"Margret Duran"},{"id":9,"name":"Reilly Patton"}],"greeting":"Hello, Ginger! You have 7 unread messages.","favoriteFruit":"apple"},{"_id":"5ae0aa343537eccc1ace3207","index":17,"guid":"7e65b665-28a9-4499-81c0-c1d9042bebdf","isActive":false,"balance":"$3,776.91","picture":"http://placehold.it/32x32","age":36,"eyeColor":"green","name":{"first":"Mckenzie","last":"Chan"},"company":"ENDIPIN","email":"mckenzie.chan@endipin.com","phone":"+1 (824) 566-2134","address":"448 Terrace Place, Cetronia, Puerto Rico, 9914","about":"Deserunt elit nostrud cupidatat labore deserunt cupidatat ea cupidatat consectetur. Labore est velit duis voluptate occaecat quis veniam nostrud incididunt proident minim elit. Nulla velit elit tempor occaecat cillum adipisicing exercitation exercitation id incididunt officia ipsum fugiat sunt. Aliqua pariatur reprehenderit in aliqua esse do quis aliquip. Proident exercitation ullamco nisi id minim id cillum eiusmod mollit ea Lorem. Laborum sit exercitation exercitation Lorem eiusmod ad laborum nisi ex id non.","registered":"Friday, June 23, 2017 11:32 AM","latitude":"-13.394667","longitude":"-158.627154","tags":["ut","quis","laboris","ea","esse","non","do","irure","ea","excepteur"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Woodward Little"},{"id":1,"name":"Patty Gamble"},{"id":2,"name":"Erickson Macias"},{"id":3,"name":"Tanya Pace"},{"id":4,"name":"Hunt Tate"},{"id":5,"name":"Maricela Crane"},{"id":6,"name":"Hanson Rodriguez"},{"id":7,"name":"Goodman Austin"},{"id":8,"name":"Short Donovan"},{"id":9,"name":"Blanche England"}],"greeting":"Hello, Mckenzie! You have 8 unread messages.","favoriteFruit":"strawberry"},{"_id":"5ae0aa34290ac8fa63a12eac","index":18,"guid":"547f7c12-6dc2-46bd-ba15-29730366275d","isActive":true,"balance":"$2,585.89","picture":"http://placehold.it/32x32","age":27,"eyeColor":"green","name":{"first":"Frazier","last":"Pickett"},"company":"GRUPOLI","email":"frazier.pickett@grupoli.co.uk","phone":"+1 (897) 581-2849","address":"678 Newton Street, Chelsea, Ohio, 490","about":"Ex non cupidatat non cupidatat enim sint occaecat ut incididunt irure nulla. Ex id culpa voluptate id do id exercitation sit mollit esse id excepteur eu duis. Minim est veniam laboris irure ut minim consectetur fugiat enim ullamco minim. Sint mollit ullamco dolore consequat ipsum eu cillum. Et consectetur elit est sit aliquip et non occaecat.","registered":"Wednesday, March 29, 2017 7:15 PM","latitude":"1.80887","longitude":"28.819849","tags":["tempor","veniam","elit","mollit","aliquip","magna","laboris","labore","anim","proident"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Mcconnell Mccall"},{"id":1,"name":"Harper Fields"},{"id":2,"name":"Kathryn Schultz"},{"id":3,"name":"Campos Hensley"},{"id":4,"name":"Landry Owen"},{"id":5,"name":"Perez Greer"},{"id":6,"name":"Maldonado Bartlett"},{"id":7,"name":"Hendrix Stein"},{"id":8,"name":"Delgado Wade"},{"id":9,"name":"Rene Wallace"}],"greeting":"Hello, Frazier! You have 5 unread messages.","favoriteFruit":"apple"},{"_id":"5ae0aa34e17d55284d96d1f5","index":19,"guid":"c7255282-e5a6-4b8b-af24-3b637fd9b044","isActive":false,"balance":"$1,869.10","picture":"http://placehold.it/32x32","age":25,"eyeColor":"blue","name":{"first":"Ochoa","last":"Madden"},"company":"INSURITY","email":"ochoa.madden@insurity.ca","phone":"+1 (934) 422-2951","address":"422 Emerald Street, Sugartown, North Carolina, 2526","about":"Dolor est aliquip pariatur sunt irure deserunt pariatur excepteur do do. Nisi nostrud esse ullamco duis fugiat aliquip cillum. Sint pariatur occaecat ex id aliqua.","registered":"Friday, February 2, 2018 8:53 PM","latitude":"-67.260494","longitude":"76.369882","tags":["aute","minim","commodo","non","ex","qui","ut","veniam","commodo","aliquip"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Lilia Schroeder"},{"id":1,"name":"Amber Meyers"},{"id":2,"name":"Janette Richards"},{"id":3,"name":"Lucinda Key"},{"id":4,"name":"Kristine Freeman"},{"id":5,"name":"Tillman Horne"},{"id":6,"name":"Cervantes Rosario"},{"id":7,"name":"Kerr Mcdonald"},{"id":8,"name":"Dionne Hancock"},{"id":9,"name":"Huber Hester"}],"greeting":"Hello, Ochoa! You have 6 unread messages.","favoriteFruit":"apple"},{"_id":"5ae0aa3473ba36f5b47cb4fe","index":20,"guid":"9019a995-b226-4d0e-8dbc-c6dc102d3bcc","isActive":true,"balance":"$1,109.74","picture":"http://placehold.it/32x32","age":27,"eyeColor":"green","name":{"first":"Janie","last":"Hendricks"},"company":"TELEQUIET","email":"janie.hendricks@telequiet.info","phone":"+1 (830) 552-3242","address":"302 Dunham Place, Cataract, South Dakota, 2071","about":"Ea et eiusmod adipisicing labore minim. Veniam nostrud non esse non adipisicing aliquip exercitation incididunt laboris velit occaecat Lorem nisi. Veniam adipisicing veniam magna nulla qui in irure culpa. Sint est aute anim sit magna velit nulla labore eu. Mollit duis amet ea culpa amet exercitation officia. Exercitation pariatur deserunt consequat magna enim occaecat mollit sunt incididunt. Fugiat id non veniam elit in ipsum duis exercitation Lorem non culpa in occaecat.","registered":"Monday, February 29, 2016 4:08 PM","latitude":"52.718238","longitude":"-107.958872","tags":["nulla","aliquip","aliquip","reprehenderit","laboris","non","exercitation","anim","nisi","eiusmod"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Diann Robertson"},{"id":1,"name":"Forbes Molina"},{"id":2,"name":"Phelps Medina"},{"id":3,"name":"Billie James"},{"id":4,"name":"Addie Roach"},{"id":5,"name":"Vickie Petersen"},{"id":6,"name":"Navarro Sanford"},{"id":7,"name":"Mclean Sampson"},{"id":8,"name":"Barlow Bolton"},{"id":9,"name":"Nicholson Zamora"}],"greeting":"Hello, Janie! You have 5 unread messages.","favoriteFruit":"apple"},{"_id":"5ae0aa3485ccf4bd0ca5238a","index":21,"guid":"ca0b5dc7-21a3-4edb-849e-1d6d81c57f8a","isActive":false,"balance":"$3,102.46","picture":"http://placehold.it/32x32","age":28,"eyeColor":"blue","name":{"first":"Good","last":"Hays"},"company":"GYNKO","email":"good.hays@gynko.me","phone":"+1 (936) 448-2745","address":"538 Lott Place, Rivera, Pennsylvania, 7526","about":"Aliqua veniam aliqua adipisicing aliquip sit aute commodo. Fugiat ut esse laboris duis nisi ipsum ea dolor labore sit pariatur laborum officia. Ipsum ad do tempor nostrud in irure enim ad amet ex id. Nostrud dolore voluptate nulla cupidatat laboris minim.","registered":"Friday, December 9, 2016 10:14 AM","latitude":"-26.185693","longitude":"68.079701","tags":["laborum","dolor","cillum","dolor","duis","in","veniam","enim","aliquip","incididunt"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Erika Dalton"},{"id":1,"name":"Josie Cole"},{"id":2,"name":"Katelyn Dominguez"},{"id":3,"name":"Jasmine Mathis"},{"id":4,"name":"Phoebe Reynolds"},{"id":5,"name":"Bishop Barr"},{"id":6,"name":"Morton Flynn"},{"id":7,"name":"Brock Berger"},{"id":8,"name":"Dominique Obrien"},{"id":9,"name":"Ayers Garrison"}],"greeting":"Hello, Good! You have 10 unread messages.","favoriteFruit":"apple"},{"_id":"5ae0aa348b68911a5e827ca8","index":22,"guid":"e0df62b2-c28f-40bb-a00e-f95a667b3fa0","isActive":true,"balance":"$3,544.61","picture":"http://placehold.it/32x32","age":31,"eyeColor":"brown","name":{"first":"Golden","last":"Reid"},"company":"KIDGREASE","email":"golden.reid@kidgrease.org","phone":"+1 (882) 440-2209","address":"952 Holly Street, Monument, Minnesota, 7297","about":"Exercitation esse ullamco do ullamco aliquip eiusmod adipisicing proident. Labore do cupidatat in irure proident. Mollit velit fugiat voluptate aliqua qui voluptate laboris aliqua minim laborum sunt amet. Consequat magna duis commodo ea cupidatat ea culpa eiusmod ut cillum ut eu velit aute. Laboris amet ea irure fugiat nisi amet commodo labore officia excepteur dolor tempor mollit magna. Non consequat dolor reprehenderit ullamco dolor tempor quis voluptate magna ad reprehenderit adipisicing incididunt. Velit laboris elit esse nulla quis.","registered":"Monday, February 24, 2014 11:59 AM","latitude":"30.725621","longitude":"-158.551407","tags":["sit","pariatur","nulla","quis","nisi","ea","qui","deserunt","officia","aliquip"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Bradshaw Gilmore"},{"id":1,"name":"Pugh Munoz"},{"id":2,"name":"Nunez Hawkins"},{"id":3,"name":"Julia Washington"},{"id":4,"name":"Rivers Webb"},{"id":5,"name":"Tara Summers"},{"id":6,"name":"Cheryl Odonnell"},{"id":7,"name":"Theresa Williamson"},{"id":8,"name":"Lesley Duke"},{"id":9,"name":"Snyder Bush"}],"greeting":"Hello, Golden! You have 6 unread messages.","favoriteFruit":"apple"},{"_id":"5ae0aa34fc10000370445e6f","index":23,"guid":"22bcb41a-1eb7-458b-80e7-7428f316d8f9","isActive":false,"balance":"$1,233.98","picture":"http://placehold.it/32x32","age":25,"eyeColor":"green","name":{"first":"Green","last":"Mcknight"},"company":"BESTO","email":"green.mcknight@besto.tv","phone":"+1 (817) 590-3548","address":"672 Montauk Court, Grahamtown, Florida, 6020","about":"Non commodo non nisi cillum fugiat. Quis ad fugiat eu ad consequat veniam adipisicing dolor. Non aliqua nulla Lorem id culpa anim veniam. Aliqua ea et eiusmod minim ut. Id ullamco qui sit aute reprehenderit nulla fugiat velit magna nisi eiusmod sit minim.","registered":"Saturday, February 10, 2018 8:53 AM","latitude":"52.299418","longitude":"21.885332","tags":["incididunt","quis","ea","cupidatat","quis","irure","qui","tempor","voluptate","et"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Mercedes Lowe"},{"id":1,"name":"Chen Perry"},{"id":2,"name":"Maxine Price"},{"id":3,"name":"Bryant Koch"},{"id":4,"name":"Benita Adams"},{"id":5,"name":"Clara Winters"},{"id":6,"name":"Alta Cantu"},{"id":7,"name":"Freida Petty"},{"id":8,"name":"Kimberly Bender"},{"id":9,"name":"Mccormick Roberts"}],"greeting":"Hello, Green! You have 8 unread messages.","favoriteFruit":"apple"},{"_id":"5ae0aa3489c9bb951ae47da8","index":24,"guid":"22a0295f-7b80-4f06-9fc3-be1cdc79bdfb","isActive":false,"balance":"$2,006.10","picture":"http://placehold.it/32x32","age":35,"eyeColor":"brown","name":{"first":"Hilda","last":"Finch"},"company":"CODAX","email":"hilda.finch@codax.io","phone":"+1 (827) 575-2563","address":"395 Seaview Avenue, Hemlock, Connecticut, 5299","about":"Quis ex cillum ipsum id. Lorem ea velit minim ipsum ipsum irure excepteur nisi culpa officia dolore aliqua dolor deserunt. Et esse aliquip ex enim nisi excepteur mollit tempor non tempor in eu.","registered":"Tuesday, October 24, 2017 6:47 PM","latitude":"-50.758977","longitude":"-133.236945","tags":["et","cillum","elit","aliquip","amet","quis","enim","dolor","eiusmod","adipisicing"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Erma Robbins"},{"id":1,"name":"Gwen Evans"},{"id":2,"name":"Connie Cummings"},{"id":3,"name":"Bonnie Valenzuela"},{"id":4,"name":"Martin Bradford"},{"id":5,"name":"Mcmahon Long"},{"id":6,"name":"Trisha Vaughan"},{"id":7,"name":"Allison West"},{"id":8,"name":"Chapman Olsen"},{"id":9,"name":"Minerva Reeves"}],"greeting":"Hello, Hilda! You have 7 unread messages.","favoriteFruit":"strawberry"},{"_id":"5ae0aa340fc9dc39242531ff","index":25,"guid":"3d78eba2-0b29-46e1-8119-b2b170f4be97","isActive":true,"balance":"$2,534.73","picture":"http://placehold.it/32x32","age":21,"eyeColor":"blue","name":{"first":"Cora","last":"Franco"},"company":"ENTHAZE","email":"cora.franco@enthaze.name","phone":"+1 (916) 530-2675","address":"217 Beaver Street, Bergoo, Montana, 7013","about":"Amet anim ipsum velit anim magna incididunt proident excepteur laborum ut voluptate voluptate ex cillum. Labore ea amet eu exercitation excepteur magna ex cupidatat laborum. Nisi ipsum id ex reprehenderit eu enim reprehenderit adipisicing. Ullamco eu sint anim et aute. Laborum sit duis quis sit non duis. Enim minim irure laboris veniam incididunt sint cupidatat.","registered":"Thursday, July 16, 2015 4:42 PM","latitude":"-26.445079","longitude":"77.502053","tags":["id","culpa","consequat","deserunt","velit","sint","dolor","ad","nulla","do"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Barnett Jacobson"},{"id":1,"name":"Lynn Mcpherson"},{"id":2,"name":"Beverley Phelps"},{"id":3,"name":"Aguilar Valdez"},{"id":4,"name":"Sweeney Sandoval"},{"id":5,"name":"Potts Allison"},{"id":6,"name":"Chavez Norman"},{"id":7,"name":"Katherine Torres"},{"id":8,"name":"Kelli Russo"},{"id":9,"name":"Joni Rojas"}],"greeting":"Hello, Cora! You have 9 unread messages.","favoriteFruit":"strawberry"},{"_id":"5ae0aa34a34dd832ac2eda38","index":26,"guid":"3ac7f2d1-cbc7-404b-915b-9984bc0947bb","isActive":true,"balance":"$3,667.55","picture":"http://placehold.it/32x32","age":29,"eyeColor":"green","name":{"first":"Faye","last":"Guzman"},"company":"CIPROMOX","email":"faye.guzman@cipromox.biz","phone":"+1 (912) 528-3635","address":"633 Livonia Avenue, Gracey, Mississippi, 9805","about":"Id ea duis velit enim qui esse non sunt. Est ad laboris proident et consequat exercitation. Aliquip do exercitation ea officia mollit dolore. Sit ullamco anim anim minim cillum pariatur officia. Exercitation velit ullamco ullamco sint amet voluptate commodo nulla ipsum qui. Elit cillum mollit ut nulla aute aliquip anim consequat ipsum Lorem sunt nisi nisi est.","registered":"Thursday, February 4, 2016 12:04 AM","latitude":"-23.278189","longitude":"-136.239491","tags":["culpa","consequat","Lorem","eiusmod","deserunt","officia","elit","cillum","aliquip","ex"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Cassandra Forbes"},{"id":1,"name":"Poole Mueller"},{"id":2,"name":"Danielle Espinoza"},{"id":3,"name":"Gentry Leach"},{"id":4,"name":"Allen Berg"},{"id":5,"name":"Rhea Fitzgerald"},{"id":6,"name":"Blair Arnold"},{"id":7,"name":"Felicia Fulton"},{"id":8,"name":"Karla Rodgers"},{"id":9,"name":"Alyssa Mcfadden"}],"greeting":"Hello, Faye! You have 5 unread messages.","favoriteFruit":"apple"},{"_id":"5ae0aa34e0f6838816113db8","index":27,"guid":"ca5a4646-8edc-4443-91cf-c5dc73927e7a","isActive":false,"balance":"$3,344.58","picture":"http://placehold.it/32x32","age":32,"eyeColor":"brown","name":{"first":"Ryan","last":"Mcintyre"},"company":"STOCKPOST","email":"ryan.mcintyre@stockpost.us","phone":"+1 (872) 474-3518","address":"272 Barwell Terrace, Tedrow, Hawaii, 5053","about":"Incididunt incididunt commodo nulla eiusmod ullamco velit ipsum ad minim non magna culpa laborum. Exercitation ipsum culpa sint in ipsum qui exercitation eiusmod nulla ex. Magna ex eu officia magna esse. Irure dolore anim aute anim tempor nulla adipisicing culpa culpa voluptate ullamco commodo. Adipisicing pariatur excepteur ad reprehenderit reprehenderit dolor magna velit est. Fugiat laboris laborum in et consectetur laborum elit adipisicing.","registered":"Saturday, April 11, 2015 11:23 PM","latitude":"76.874677","longitude":"94.777546","tags":["id","veniam","ad","do","commodo","amet","tempor","sunt","ullamco","laborum"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Vera Olson"},{"id":1,"name":"West Daniel"},{"id":2,"name":"Socorro Manning"},{"id":3,"name":"Louise Haynes"},{"id":4,"name":"Lela Mann"},{"id":5,"name":"Spence Lyons"},{"id":6,"name":"Wynn Gillespie"},{"id":7,"name":"Lynch Jones"},{"id":8,"name":"Patrica Carlson"},{"id":9,"name":"Marietta Hickman"}],"greeting":"Hello, Ryan! You have 9 unread messages.","favoriteFruit":"apple"},{"_id":"5ae0aa344299323300eee507","index":28,"guid":"58a7ace0-c6be-4581-bee8-74a5d60398f7","isActive":false,"balance":"$3,743.38","picture":"http://placehold.it/32x32","age":27,"eyeColor":"blue","name":{"first":"Mcgowan","last":"Mcgowan"},"company":"QUINTITY","email":"mcgowan.mcgowan@quintity.biz","phone":"+1 (881) 485-2266","address":"822 Ridge Court, Somerset, Rhode Island, 458","about":"Laboris in velit voluptate amet cupidatat sit. Ea culpa cupidatat in dolor aute ex dolor ut dolore aliqua aliquip cillum. Ipsum laboris sint dolor esse deserunt amet exercitation sunt mollit.","registered":"Tuesday, June 28, 2016 6:36 AM","latitude":"-25.599677","longitude":"84.225442","tags":["nostrud","consectetur","labore","ex","anim","fugiat","nostrud","sunt","deserunt","in"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Steele Riggs"},{"id":1,"name":"Carmella Moran"},{"id":2,"name":"Allyson Blake"},{"id":3,"name":"Carol Cooke"},{"id":4,"name":"Heath Knight"},{"id":5,"name":"Ware Oconnor"},{"id":6,"name":"Osborne Jackson"},{"id":7,"name":"Lilian Burns"},{"id":8,"name":"Mallory Chen"},{"id":9,"name":"Liza Nunez"}],"greeting":"Hello, Mcgowan! You have 10 unread messages.","favoriteFruit":"apple"},{"_id":"5ae0aa34ad8457b4672e26a6","index":29,"guid":"af06ab7e-2514-4610-805b-9ef1ab10d1b6","isActive":false,"balance":"$2,641.66","picture":"http://placehold.it/32x32","age":40,"eyeColor":"green","name":{"first":"Cathryn","last":"Norris"},"company":"CUIZINE","email":"cathryn.norris@cuizine.com","phone":"+1 (886) 433-3723","address":"631 Hinsdale Street, Edmund, Texas, 818","about":"Enim consectetur eu nulla aliqua enim deserunt veniam deserunt. Commodo dolor anim aliqua anim cupidatat incididunt ex. Consequat qui ex dolor do do ullamco officia laboris ipsum. Sit sunt officia dolore ut dolore veniam dolor Lorem dolor do et qui velit pariatur. Nisi eiusmod ut do duis Lorem cillum do cillum amet dolore. Qui esse officia veniam incididunt consectetur id nostrud deserunt dolor adipisicing ipsum aliquip occaecat. Cillum aliquip sint eu minim aliquip nostrud eiusmod ullamco esse mollit sit amet amet ipsum.","registered":"Wednesday, July 5, 2017 9:58 PM","latitude":"-54.903831","longitude":"-75.383676","tags":["non","minim","fugiat","sint","deserunt","quis","fugiat","do","deserunt","occaecat"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Hale Wiley"},{"id":1,"name":"Clarice Holcomb"},{"id":2,"name":"Arlene Rowland"},{"id":3,"name":"Mckee Craig"},{"id":4,"name":"Leann Mullins"},{"id":5,"name":"Lydia Carpenter"},{"id":6,"name":"Franklin Bishop"},{"id":7,"name":"Meadows Rice"},{"id":8,"name":"Lee Bullock"},{"id":9,"name":"Moon Vance"}],"greeting":"Hello, Cathryn! You have 8 unread messages.","favoriteFruit":"strawberry"},{"_id":"5ae0aa3438902775803a72d7","index":30,"guid":"0433ad5c-c75b-4e57-b81b-d6f7b4c6ce23","isActive":true,"balance":"$1,413.96","picture":"http://placehold.it/32x32","age":35,"eyeColor":"green","name":{"first":"Gladys","last":"Luna"},"company":"VORTEXACO","email":"gladys.luna@vortexaco.co.uk","phone":"+1 (841) 419-2692","address":"710 Grove Place, Gouglersville, North Dakota, 7911","about":"Adipisicing excepteur incididunt eu ullamco in tempor. Fugiat aute ex veniam culpa labore quis anim magna. Aliqua exercitation culpa cupidatat do ullamco nostrud id. Dolore nisi laborum ullamco fugiat eiusmod laboris exercitation dolore magna id ipsum. Non velit commodo amet fugiat esse cillum tempor ex esse et aliquip ipsum nostrud. Consectetur adipisicing nisi voluptate exercitation amet. Incididunt fugiat dolore elit consequat laboris sunt culpa quis ullamco occaecat consequat enim.","registered":"Thursday, February 25, 2016 1:35 AM","latitude":"-17.405117","longitude":"-94.797563","tags":["ullamco","cupidatat","consectetur","sit","sit","incididunt","voluptate","eu","occaecat","commodo"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Jill Black"},{"id":1,"name":"Myra Wilcox"},{"id":2,"name":"Rojas Mcintosh"},{"id":3,"name":"Duffy Tyson"},{"id":4,"name":"Nettie Payne"},{"id":5,"name":"Pam Avery"},{"id":6,"name":"Guerrero Drake"},{"id":7,"name":"Dunlap Fuller"},{"id":8,"name":"Mercer Hopper"},{"id":9,"name":"Mayra Ramos"}],"greeting":"Hello, Gladys! You have 9 unread messages.","favoriteFruit":"apple"},{"_id":"5ae0aa34fab19bc37260045d","index":31,"guid":"fbd1f589-514f-4124-a008-6fafdfceebca","isActive":false,"balance":"$2,778.78","picture":"http://placehold.it/32x32","age":29,"eyeColor":"blue","name":{"first":"Mildred","last":"Farrell"},"company":"GLEAMINK","email":"mildred.farrell@gleamink.ca","phone":"+1 (979) 492-2298","address":"285 Lewis Avenue, Toftrees, West Virginia, 2663","about":"Lorem ut incididunt Lorem commodo ipsum nisi sunt consequat officia sit nulla labore nostrud labore. Culpa cupidatat id proident nostrud dolor officia ullamco non veniam. Laborum exercitation amet amet sint. Excepteur excepteur dolor irure laborum. Commodo ut minim incididunt tempor officia consequat occaecat.","registered":"Monday, July 11, 2016 4:58 PM","latitude":"-22.275242","longitude":"138.432225","tags":["dolor","officia","dolor","sit","est","ea","enim","eiusmod","magna","incididunt"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Rodgers Thornton"},{"id":1,"name":"Samantha Stanley"},{"id":2,"name":"Lelia Bowen"},{"id":3,"name":"Herminia Hutchinson"},{"id":4,"name":"Wright Holloway"},{"id":5,"name":"Ava Vinson"},{"id":6,"name":"Francine Schmidt"},{"id":7,"name":"Frederick Bowman"},{"id":8,"name":"Daniel Brown"},{"id":9,"name":"Renee Gill"}],"greeting":"Hello, Mildred! You have 5 unread messages.","favoriteFruit":"apple"},{"_id":"5ae0aa34a398b521eb072327","index":32,"guid":"045c43e8-f6f2-4490-8d26-3fb124098c34","isActive":false,"balance":"$3,371.61","picture":"http://placehold.it/32x32","age":23,"eyeColor":"blue","name":{"first":"Angelina","last":"Hahn"},"company":"QUANTALIA","email":"angelina.hahn@quantalia.info","phone":"+1 (813) 431-3542","address":"894 Louisa Street, Cochranville, Maine, 2019","about":"Tempor Lorem labore et officia id. Laboris aliqua Lorem do aliquip dolore officia quis Lorem quis et qui voluptate sunt. Id Lorem voluptate elit ea dolor occaecat magna occaecat et ad sint. Cillum mollit incididunt nisi reprehenderit Lorem duis reprehenderit minim voluptate exercitation incididunt est nisi esse.","registered":"Monday, December 12, 2016 7:40 AM","latitude":"28.524161","longitude":"97.447644","tags":["pariatur","incididunt","est","ut","pariatur","laboris","duis","nulla","eu","fugiat"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Sylvia Bradley"},{"id":1,"name":"Jamie Wells"},{"id":2,"name":"Jodie Mccormick"},{"id":3,"name":"Calderon Hansen"},{"id":4,"name":"Fuentes Walton"},{"id":5,"name":"Lacy Goff"},{"id":6,"name":"Marguerite Cantrell"},{"id":7,"name":"Goldie Best"},{"id":8,"name":"Melva Ware"},{"id":9,"name":"Lina Stuart"}],"greeting":"Hello, Angelina! You have 6 unread messages.","favoriteFruit":"strawberry"},{"_id":"5ae0aa3415690121e1d31dc8","index":33,"guid":"1c4d9d82-a2ed-4137-bbb2-1e31c25c3845","isActive":true,"balance":"$1,955.48","picture":"http://placehold.it/32x32","age":38,"eyeColor":"green","name":{"first":"Rosanna","last":"Morgan"},"company":"AQUOAVO","email":"rosanna.morgan@aquoavo.me","phone":"+1 (972) 555-2633","address":"125 Downing Street, Lutsen, Missouri, 7484","about":"Officia officia magna exercitation do culpa nostrud anim minim reprehenderit velit exercitation exercitation excepteur. Cillum est occaecat id nisi. Ipsum dolor quis incididunt non anim. Officia in esse nulla do commodo eu non commodo consequat. Et proident et id officia fugiat consectetur do non. Est ea eu velit fugiat ea nostrud id consequat cillum sit consectetur. Proident ut Lorem incididunt Lorem aliqua mollit Lorem dolore esse mollit elit eu.","registered":"Saturday, November 11, 2017 6:19 AM","latitude":"-26.09161","longitude":"-142.715019","tags":["veniam","exercitation","proident","minim","aliquip","labore","sint","quis","laboris","ut"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Durham Mayo"},{"id":1,"name":"Puckett Clayton"},{"id":2,"name":"Guzman Franks"},{"id":3,"name":"Marion Riddle"},{"id":4,"name":"Bowman Clemons"},{"id":5,"name":"Church Dudley"},{"id":6,"name":"Adeline Gross"},{"id":7,"name":"Wolf Boyle"},{"id":8,"name":"Maggie Fry"},{"id":9,"name":"Molly Willis"}],"greeting":"Hello, Rosanna! You have 9 unread messages.","favoriteFruit":"strawberry"},{"_id":"5ae0aa3473b934c37662ee87","index":34,"guid":"9e975a37-6c04-4476-8af8-a8da71f95bce","isActive":false,"balance":"$2,374.83","picture":"http://placehold.it/32x32","age":26,"eyeColor":"green","name":{"first":"Alexis","last":"Stephens"},"company":"VIDTO","email":"alexis.stephens@vidto.org","phone":"+1 (823) 415-2326","address":"129 Cove Lane, Loretto, Federated States Of Micronesia, 4132","about":"Ipsum laboris elit irure sunt velit minim et aliquip qui labore. Cupidatat pariatur cupidatat labore dolore ullamco nisi ipsum aute. Aute fugiat pariatur cupidatat fugiat irure do proident voluptate pariatur duis quis sit est. Ex ipsum ea ipsum nulla nostrud ad veniam sunt cupidatat aliquip occaecat elit est voluptate. Nostrud non enim quis consequat cupidatat voluptate occaecat veniam ad sunt sint Lorem tempor minim.","registered":"Tuesday, February 9, 2016 6:58 AM","latitude":"30.265793","longitude":"133.438276","tags":["veniam","eiusmod","Lorem","officia","consequat","dolor","tempor","sunt","ex","sit"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Fanny Atkinson"},{"id":1,"name":"Maryanne Conrad"},{"id":2,"name":"Catalina Harmon"},{"id":3,"name":"Cooke Rowe"},{"id":4,"name":"Angie Mcgee"},{"id":5,"name":"Stokes Gibson"},{"id":6,"name":"Jones Vega"},{"id":7,"name":"Leah Butler"},{"id":8,"name":"Taylor Peters"},{"id":9,"name":"Cochran Neal"}],"greeting":"Hello, Alexis! You have 10 unread messages.","favoriteFruit":"strawberry"},{"_id":"5ae0aa34629b7e6a2d9be431","index":35,"guid":"3c9958e7-c013-4af9-8fcd-b6956105558b","isActive":false,"balance":"$3,908.65","picture":"http://placehold.it/32x32","age":32,"eyeColor":"green","name":{"first":"Aguirre","last":"Mckinney"},"company":"NETPLODE","email":"aguirre.mckinney@netplode.tv","phone":"+1 (894) 423-3860","address":"403 Glendale Court, Wildwood, Massachusetts, 8080","about":"Aliqua elit sunt anim magna aliquip ullamco non incididunt. Proident irure Lorem excepteur aliqua deserunt nisi in ea reprehenderit. Ad laboris aute cillum ut exercitation culpa dolor.","registered":"Wednesday, May 7, 2014 2:56 PM","latitude":"-76.365309","longitude":"-157.854137","tags":["proident","deserunt","est","sint","sint","magna","aliquip","in","commodo","velit"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Dorsey Cote"},{"id":1,"name":"Levy Castro"},{"id":2,"name":"Anne Love"},{"id":3,"name":"Neva Suarez"},{"id":4,"name":"Tammie Gallagher"},{"id":5,"name":"Kristi Parker"},{"id":6,"name":"Waller Miller"},{"id":7,"name":"Nadia Wood"},{"id":8,"name":"Powell Lott"},{"id":9,"name":"Chris Hoffman"}],"greeting":"Hello, Aguirre! You have 7 unread messages.","favoriteFruit":"strawberry"},{"_id":"5ae0aa3426ab5d444b6a10f9","index":36,"guid":"8b4fdf0f-9db7-41d4-b39d-139cd785d7b5","isActive":true,"balance":"$2,280.91","picture":"http://placehold.it/32x32","age":30,"eyeColor":"green","name":{"first":"Richards","last":"Snow"},"company":"BLURRYBUS","email":"richards.snow@blurrybus.io","phone":"+1 (905) 463-2264","address":"494 Brightwater Court, Marenisco, Colorado, 1830","about":"Mollit duis nisi eu aliquip tempor irure laborum ad minim aliqua. Ullamco voluptate enim adipisicing consectetur adipisicing occaecat tempor voluptate dolor incididunt tempor fugiat. Adipisicing cupidatat esse ex commodo nostrud officia cupidatat ullamco magna proident incididunt. Adipisicing sint Lorem qui duis laborum non consequat minim incididunt sint quis in voluptate. Minim ea ex ex cupidatat consectetur aliqua proident exercitation cupidatat duis sint ut. Magna enim eu aute aliquip est minim nulla mollit. Occaecat magna ad nisi eiusmod aliqua deserunt consectetur exercitation adipisicing commodo voluptate.","registered":"Thursday, October 13, 2016 7:34 PM","latitude":"-84.839295","longitude":"-173.645597","tags":["ut","veniam","ullamco","culpa","exercitation","fugiat","quis","consequat","amet","nostrud"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Lakeisha Banks"},{"id":1,"name":"Anderson Wooten"},{"id":2,"name":"Marie Hopkins"},{"id":3,"name":"Parks Jensen"},{"id":4,"name":"Wendi Page"},{"id":5,"name":"Hart Andrews"},{"id":6,"name":"Margery Irwin"},{"id":7,"name":"Harrison Kirby"},{"id":8,"name":"Ivy Santiago"},{"id":9,"name":"Tamra Witt"}],"greeting":"Hello, Richards! You have 5 unread messages.","favoriteFruit":"apple"},{"_id":"5ae0aa345399675c8e97e073","index":37,"guid":"8c6ce068-927a-4c5a-ba09-bc264d9fd709","isActive":true,"balance":"$2,124.25","picture":"http://placehold.it/32x32","age":25,"eyeColor":"brown","name":{"first":"Penny","last":"Swanson"},"company":"PLAYCE","email":"penny.swanson@playce.name","phone":"+1 (874) 539-3904","address":"797 Pershing Loop, Gorst, New York, 2236","about":"Cillum ad irure consequat do cillum culpa aliquip exercitation est cupidatat ex do consectetur aute. Enim proident ea velit dolore ex ea tempor elit consequat laboris. Laborum velit eu minim irure nostrud cupidatat mollit minim nostrud. Tempor ea laboris nostrud tempor veniam. Cillum ex in cupidatat ut laboris. Fugiat nisi in quis velit eu fugiat veniam aliquip aliqua anim est.","registered":"Tuesday, September 13, 2016 10:26 AM","latitude":"46.06826","longitude":"-179.707892","tags":["ea","proident","ea","culpa","consequat","incididunt","incididunt","non","ea","duis"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Webster Alford"},{"id":1,"name":"Stacey Boyd"},{"id":2,"name":"Nellie Delgado"},{"id":3,"name":"Baker Slater"},{"id":4,"name":"Moody Underwood"},{"id":5,"name":"Rosales Buck"},{"id":6,"name":"Hess Todd"},{"id":7,"name":"Lane Farley"},{"id":8,"name":"Smith Sears"},{"id":9,"name":"Newman Caldwell"}],"greeting":"Hello, Penny! You have 10 unread messages.","favoriteFruit":"banana"},{"_id":"5ae0aa344f3de72033606554","index":38,"guid":"78c70b05-ba42-4c24-a841-3cb06a29112c","isActive":false,"balance":"$2,845.79","picture":"http://placehold.it/32x32","age":26,"eyeColor":"green","name":{"first":"Christi","last":"French"},"company":"SKYPLEX","email":"christi.french@skyplex.biz","phone":"+1 (831) 500-2423","address":"381 Colby Court, Haena, Marshall Islands, 839","about":"Consectetur magna in fugiat quis aute anim elit officia ex sint exercitation. Sunt exercitation pariatur enim laborum mollit labore. Minim enim pariatur enim ex ipsum eiusmod est quis irure.","registered":"Wednesday, August 17, 2016 9:32 PM","latitude":"-70.652626","longitude":"-160.945222","tags":["dolor","reprehenderit","nulla","laborum","esse","est","nostrud","velit","cupidatat","id"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Haley Mcneil"},{"id":1,"name":"Bernadine Bean"},{"id":2,"name":"Agnes Kirk"},{"id":3,"name":"Margie Aguirre"},{"id":4,"name":"Mitzi Harper"},{"id":5,"name":"Hatfield Marks"},{"id":6,"name":"Rachael Harris"},{"id":7,"name":"Serena Sykes"},{"id":8,"name":"Davis Cline"},{"id":9,"name":"Blanca Cameron"}],"greeting":"Hello, Christi! You have 7 unread messages.","favoriteFruit":"strawberry"},{"_id":"5ae0aa346c214ead2d9abdf1","index":39,"guid":"eae6507c-4bae-4729-9212-aa5a8c48006b","isActive":false,"balance":"$3,360.83","picture":"http://placehold.it/32x32","age":26,"eyeColor":"green","name":{"first":"Kidd","last":"Stark"},"company":"CENTREXIN","email":"kidd.stark@centrexin.us","phone":"+1 (832) 581-2526","address":"759 Blake Court, Turpin, Oklahoma, 7285","about":"Dolore dolor labore ut aliquip ad enim do enim do. Fugiat voluptate culpa qui ut officia consequat ipsum laboris ullamco exercitation voluptate consectetur proident minim. Laboris nisi amet quis aliquip adipisicing eiusmod pariatur in ut quis.","registered":"Saturday, April 30, 2016 2:38 AM","latitude":"-66.122491","longitude":"178.936101","tags":["pariatur","duis","qui","qui","deserunt","minim","in","aliqua","voluptate","laboris"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Petty Ferrell"},{"id":1,"name":"Jacqueline Reilly"},{"id":2,"name":"Robles Frye"},{"id":3,"name":"Woods Weeks"},{"id":4,"name":"Louisa Levine"},{"id":5,"name":"Hardin Farmer"},{"id":6,"name":"Roach Glass"},{"id":7,"name":"Jacobs Faulkner"},{"id":8,"name":"Oconnor Hurley"},{"id":9,"name":"Loretta Patel"}],"greeting":"Hello, Kidd! You have 9 unread messages.","favoriteFruit":"strawberry"},{"_id":"5ae0aa348ff4bb496eae2130","index":40,"guid":"77f38c4e-206a-41ad-b8ec-8c124da29d71","isActive":true,"balance":"$1,227.03","picture":"http://placehold.it/32x32","age":39,"eyeColor":"green","name":{"first":"Angelita","last":"Mills"},"company":"CABLAM","email":"angelita.mills@cablam.biz","phone":"+1 (977) 596-3341","address":"240 Court Street, Collins, South Carolina, 3358","about":"Quis velit labore adipisicing amet excepteur. Exercitation enim adipisicing adipisicing ut nostrud cillum. Est anim duis duis voluptate Lorem occaecat. Ut commodo laborum reprehenderit aliqua dolor eiusmod cupidatat duis. Deserunt et mollit velit excepteur qui non deserunt esse ut tempor ut voluptate. Sit culpa sit officia Lorem nisi ad nulla amet ipsum elit dolore. Mollit amet id Lorem magna esse consequat duis irure ad veniam ea duis.","registered":"Saturday, October 11, 2014 3:14 PM","latitude":"47.351007","longitude":"105.90419","tags":["sunt","non","voluptate","consectetur","ut","est","laborum","ad","cupidatat","esse"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Langley Powers"},{"id":1,"name":"Carson Woodard"},{"id":2,"name":"Peterson Pugh"},{"id":3,"name":"Lorie Perez"},{"id":4,"name":"Case Parks"},{"id":5,"name":"Elizabeth Boone"},{"id":6,"name":"Marian Maddox"},{"id":7,"name":"Mara Sargent"},{"id":8,"name":"Gilliam Garner"},{"id":9,"name":"Weeks Ross"}],"greeting":"Hello, Angelita! You have 10 unread messages.","favoriteFruit":"banana"},{"_id":"5ae0aa34e7a9b39ca8267980","index":41,"guid":"2a508c71-aa4d-4f9e-a96d-3f6495bf102c","isActive":true,"balance":"$2,610.88","picture":"http://placehold.it/32x32","age":34,"eyeColor":"green","name":{"first":"Kirk","last":"Conner"},"company":"ECRATER","email":"kirk.conner@ecrater.com","phone":"+1 (876) 408-2160","address":"984 Erasmus Street, Ronco, Tennessee, 2168","about":"Nisi sint velit reprehenderit ex anim in veniam elit laborum aliquip. Sit exercitation mollit magna veniam officia minim ipsum labore dolore nulla. Dolor ad adipisicing magna amet aliquip. Amet elit aliqua dolore dolore mollit ex veniam sit aliquip proident pariatur duis aliqua do. Eu et id aute in eu qui minim sunt ullamco quis ea ipsum. Elit ullamco dolor irure culpa proident tempor enim irure irure deserunt anim ex. Consequat proident Lorem enim est.","registered":"Tuesday, June 20, 2017 5:36 PM","latitude":"9.0713","longitude":"-13.535663","tags":["eu","duis","deserunt","sint","consequat","magna","laborum","aliquip","anim","ex"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Bridges Floyd"},{"id":1,"name":"Barnes Head"},{"id":2,"name":"Conley Barrett"},{"id":3,"name":"Waters Bennett"},{"id":4,"name":"Kaufman Morton"},{"id":5,"name":"Copeland Rose"},{"id":6,"name":"Kendra Daniels"},{"id":7,"name":"Julie Roth"},{"id":8,"name":"Tami Snider"},{"id":9,"name":"Margaret Mcmillan"}],"greeting":"Hello, Kirk! You have 10 unread messages.","favoriteFruit":"banana"},{"_id":"5ae0aa341e9858da323f3e8a","index":42,"guid":"8ae8c734-331c-4707-9225-f38615f7d87c","isActive":true,"balance":"$3,280.97","picture":"http://placehold.it/32x32","age":22,"eyeColor":"blue","name":{"first":"Hickman","last":"Langley"},"company":"VERAQ","email":"hickman.langley@veraq.co.uk","phone":"+1 (908) 439-3901","address":"469 Nova Court, Buxton, Wisconsin, 8132","about":"Voluptate et ipsum ea in voluptate. Amet incididunt cupidatat duis veniam ipsum veniam deserunt. Mollit duis exercitation cillum pariatur culpa enim. Excepteur nulla ea velit sunt aliqua. Est velit consectetur in aliqua. Duis pariatur ea sit laborum Lorem quis nisi dolore. Fugiat id ad consectetur irure ipsum adipisicing eu sit duis laboris tempor sunt irure minim.","registered":"Monday, December 19, 2016 6:40 PM","latitude":"38.352256","longitude":"-79.01819","tags":["commodo","laboris","in","esse","incididunt","eiusmod","aute","amet","qui","esse"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Snow Stafford"},{"id":1,"name":"Pollard Fischer"},{"id":2,"name":"Lillian Bowers"},{"id":3,"name":"Katy Lancaster"},{"id":4,"name":"Ana Tillman"},{"id":5,"name":"Tanisha Dean"},{"id":6,"name":"Verna Osborn"},{"id":7,"name":"Lorene Blackwell"},{"id":8,"name":"Klein Lindsay"},{"id":9,"name":"Kay Simpson"}],"greeting":"Hello, Hickman! You have 9 unread messages.","favoriteFruit":"banana"},{"_id":"5ae0aa34745076e981951cad","index":43,"guid":"cf66637a-0f87-43c1-8d3e-8a8eb32a3b82","isActive":true,"balance":"$3,777.58","picture":"http://placehold.it/32x32","age":27,"eyeColor":"blue","name":{"first":"Dorthy","last":"Dixon"},"company":"AVENETRO","email":"dorthy.dixon@avenetro.ca","phone":"+1 (865) 445-2658","address":"845 Hendrix Street, Mathews, Delaware, 8863","about":"Consectetur qui minim tempor proident sint esse est quis. Eiusmod aliqua ea ut officia quis deserunt cillum quis aliqua exercitation ipsum. Magna qui veniam enim ullamco nulla. Occaecat commodo excepteur qui laborum qui ea mollit Lorem non ex nostrud proident reprehenderit nulla. Ipsum laboris irure ullamco do consectetur laboris sunt id adipisicing ullamco duis quis cupidatat.","registered":"Wednesday, January 13, 2016 1:56 AM","latitude":"-45.361089","longitude":"118.758428","tags":["esse","aute","excepteur","esse","labore","ut","irure","ea","cupidatat","aliquip"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Dillon Holman"},{"id":1,"name":"Megan Joyner"},{"id":2,"name":"Casandra Branch"},{"id":3,"name":"Linda Livingston"},{"id":4,"name":"Pope Talley"},{"id":5,"name":"Vasquez Cox"},{"id":6,"name":"Skinner Cross"},{"id":7,"name":"Anna Beasley"},{"id":8,"name":"Dawson Gould"},{"id":9,"name":"Dean Shepard"}],"greeting":"Hello, Dorthy! You have 9 unread messages.","favoriteFruit":"apple"},{"_id":"5ae0aa34f7481ee6928ebadf","index":44,"guid":"d5e4825b-cbfe-4994-af1a-03a4a46fd290","isActive":false,"balance":"$2,976.38","picture":"http://placehold.it/32x32","age":33,"eyeColor":"brown","name":{"first":"Lynnette","last":"Rosales"},"company":"YURTURE","email":"lynnette.rosales@yurture.info","phone":"+1 (931) 424-3261","address":"293 Rose Street, Alderpoint, Alabama, 2523","about":"Veniam et et laboris irure elit consectetur ut exercitation fugiat. Cillum in eu cillum elit et. Ullamco mollit tempor consectetur excepteur enim laborum amet magna id. Cillum commodo mollit qui consequat eiusmod cupidatat amet. Do labore aute consequat aliqua nostrud ut officia qui elit id labore dolor laboris. Fugiat nostrud ea id officia adipisicing. Incididunt reprehenderit velit sunt elit ex officia nulla nostrud sit culpa pariatur ipsum.","registered":"Monday, June 22, 2015 9:25 PM","latitude":"83.236293","longitude":"-165.695513","tags":["sit","deserunt","non","ipsum","irure","laborum","ut","amet","dolore","Lorem"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Harriet Vasquez"},{"id":1,"name":"Warren Sherman"},{"id":2,"name":"Rita Downs"},{"id":3,"name":"Celia Mcclain"},{"id":4,"name":"Bethany Mason"},{"id":5,"name":"Barker Anderson"},{"id":6,"name":"Lucy Santana"},{"id":7,"name":"Aurelia Dickerson"},{"id":8,"name":"Bullock Durham"},{"id":9,"name":"Middleton Huffman"}],"greeting":"Hello, Lynnette! You have 7 unread messages.","favoriteFruit":"banana"},{"_id":"5ae0aa3447392bc6d49d3c6a","index":45,"guid":"93bf294e-6516-462c-affc-fefb2fc25cfb","isActive":false,"balance":"$1,251.30","picture":"http://placehold.it/32x32","age":26,"eyeColor":"brown","name":{"first":"Wagner","last":"Murphy"},"company":"NEXGENE","email":"wagner.murphy@nexgene.me","phone":"+1 (861) 488-2141","address":"567 Cleveland Street, Belva, Northern Mariana Islands, 1561","about":"Mollit ut ipsum id qui ex magna duis magna ipsum cillum proident. Veniam eu dolore deserunt nulla amet dolore quis aliqua. Est dolore anim aliqua ex exercitation cupidatat voluptate. Ea ipsum proident sunt non. Minim laborum nisi culpa ex sit excepteur reprehenderit aute exercitation officia veniam. Occaecat est tempor anim incididunt elit incididunt commodo fugiat magna laboris laboris irure. Culpa aliquip ipsum tempor cupidatat non laboris ad anim duis.","registered":"Saturday, June 20, 2015 8:46 AM","latitude":"26.957528","longitude":"-83.855732","tags":["officia","aliqua","et","cupidatat","labore","culpa","nisi","sit","mollit","cupidatat"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Gertrude Mclaughlin"},{"id":1,"name":"Esther Pope"},{"id":2,"name":"Jacquelyn Rush"},{"id":3,"name":"Ellen Keith"},{"id":4,"name":"Zimmerman Mejia"},{"id":5,"name":"Craft Franklin"},{"id":6,"name":"Hubbard Christian"},{"id":7,"name":"Bridgett Kline"},{"id":8,"name":"Gonzalez Douglas"},{"id":9,"name":"Larson Justice"}],"greeting":"Hello, Wagner! You have 10 unread messages.","favoriteFruit":"apple"},{"_id":"5ae0aa344a1963094ff64da5","index":46,"guid":"6b9e6342-267d-4844-b030-723bae27c4ce","isActive":true,"balance":"$1,943.18","picture":"http://placehold.it/32x32","age":35,"eyeColor":"green","name":{"first":"Tonya","last":"Lowery"},"company":"MYOPIUM","email":"tonya.lowery@myopium.org","phone":"+1 (881) 553-2631","address":"975 Amboy Street, Nipinnawasee, California, 9715","about":"Pariatur dolor exercitation dolore eiusmod et nisi in deserunt laborum nisi magna. Dolor laborum culpa quis aute pariatur labore est velit voluptate. Minim labore occaecat aute fugiat sint laborum voluptate tempor minim.","registered":"Wednesday, December 30, 2015 5:12 AM","latitude":"13.22295","longitude":"159.692667","tags":["eu","veniam","et","est","est","aliquip","sint","non","anim","ex"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Debra Stanton"},{"id":1,"name":"Harrington Carney"},{"id":2,"name":"Helena Rivas"},{"id":3,"name":"Pearl Wagner"},{"id":4,"name":"Bridgette Lawson"},{"id":5,"name":"Patel Sutton"},{"id":6,"name":"Horton Saunders"},{"id":7,"name":"Sherri Nelson"},{"id":8,"name":"Jacobson Hurst"},{"id":9,"name":"Shanna Cotton"}],"greeting":"Hello, Tonya! You have 5 unread messages.","favoriteFruit":"banana"},{"_id":"5ae0aa34b44b3eb5d573dfe4","index":47,"guid":"6fc32a51-1b79-4679-910e-5eb13aa223f8","isActive":true,"balance":"$1,559.68","picture":"http://placehold.it/32x32","age":38,"eyeColor":"green","name":{"first":"Johnnie","last":"Travis"},"company":"SURELOGIC","email":"johnnie.travis@surelogic.tv","phone":"+1 (855) 506-2479","address":"289 Anna Court, Denio, Virgin Islands, 7460","about":"Adipisicing ipsum enim ullamco laborum nostrud nulla esse quis ipsum voluptate eiusmod. Fugiat ut est consectetur nisi minim. Id sint nostrud eiusmod cupidatat fugiat.","registered":"Tuesday, August 1, 2017 5:05 PM","latitude":"-4.213212","longitude":"-93.529387","tags":["est","consequat","consectetur","enim","qui","et","ullamco","ipsum","reprehenderit","mollit"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Ester Harrell"},{"id":1,"name":"Shepard Shelton"},{"id":2,"name":"Enid Romero"},{"id":3,"name":"Effie Baxter"},{"id":4,"name":"Dona Huber"},{"id":5,"name":"Berger Heath"},{"id":6,"name":"Allison Lynn"},{"id":7,"name":"Blackwell Edwards"},{"id":8,"name":"Lottie Chambers"},{"id":9,"name":"Todd Holmes"}],"greeting":"Hello, Johnnie! You have 8 unread messages.","favoriteFruit":"apple"},{"_id":"5ae0aa349fe0e22b1f9fbdfd","index":48,"guid":"3ae5da74-591c-48c9-a328-6ec454619337","isActive":false,"balance":"$2,825.93","picture":"http://placehold.it/32x32","age":20,"eyeColor":"blue","name":{"first":"Deanna","last":"Adkins"},"company":"VIRXO","email":"deanna.adkins@virxo.io","phone":"+1 (981) 554-3807","address":"650 Bridge Street, Mappsville, Kansas, 4759","about":"Et aliqua laboris do excepteur veniam dolor cupidatat nisi eu voluptate sunt eiusmod non eiusmod. Minim eu dolor sunt aute. Nostrud laborum ad voluptate ipsum occaecat Lorem. Dolore sit amet cupidatat enim cillum ipsum cillum. Et laborum ut proident officia mollit. Eu velit commodo esse id sint aliquip Lorem et occaecat aliqua. Consequat eiusmod fugiat cillum consequat eiusmod eiusmod ex qui est duis consequat et officia.","registered":"Tuesday, December 13, 2016 7:50 AM","latitude":"33.223746","longitude":"15.22617","tags":["excepteur","ex","deserunt","qui","sit","aute","nostrud","do","labore","ipsum"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Riddle Reese"},{"id":1,"name":"Selma Henson"},{"id":2,"name":"Yesenia Middleton"},{"id":3,"name":"Nola White"},{"id":4,"name":"Sweet Guerra"},{"id":5,"name":"Terra Small"},{"id":6,"name":"Hamilton Hughes"},{"id":7,"name":"Hester Poole"},{"id":8,"name":"Calhoun Hubbard"},{"id":9,"name":"Bonita Hall"}],"greeting":"Hello, Deanna! You have 10 unread messages.","favoriteFruit":"strawberry"},{"_id":"5ae0aa34e364048f9370e15f","index":49,"guid":"bf614856-f78f-44c2-8abc-0f89064d6c7b","isActive":true,"balance":"$3,625.36","picture":"http://placehold.it/32x32","age":35,"eyeColor":"blue","name":{"first":"Rosella","last":"Zimmerman"},"company":"ZYTREX","email":"rosella.zimmerman@zytrex.name","phone":"+1 (928) 486-3626","address":"915 Dean Street, Valmy, Iowa, 2698","about":"Dolore cupidatat quis ex esse. Mollit labore exercitation exercitation Lorem elit pariatur incididunt culpa nostrud pariatur tempor ut id. Lorem nostrud mollit elit veniam dolore incididunt duis consectetur nostrud proident eu occaecat cupidatat incididunt. Dolor pariatur fugiat sunt do consequat officia sit aliquip officia quis consequat ipsum dolor ad. Occaecat mollit sunt sit occaecat. Quis in irure ipsum sunt. Dolor ullamco sunt dolor labore proident consectetur ex irure est dolor officia.","registered":"Sunday, September 24, 2017 5:22 AM","latitude":"-58.21478","longitude":"2.994387","tags":["id","irure","labore","anim","non","est","eiusmod","aliquip","enim","proident"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Ortiz Rivera"},{"id":1,"name":"Nina Mcclure"},{"id":2,"name":"Donna Whitley"},{"id":3,"name":"Ladonna Salazar"},{"id":4,"name":"Rosario Gay"},{"id":5,"name":"Nelda Rich"},{"id":6,"name":"Harriett Phillips"},{"id":7,"name":"Marianne Leblanc"},{"id":8,"name":"Anastasia Smith"},{"id":9,"name":"Stacy Cochran"}],"greeting":"Hello, Rosella! You have 8 unread messages.","favoriteFruit":"apple"}]} 2 | -------------------------------------------------------------------------------- /testdata/one.gron: -------------------------------------------------------------------------------- 1 | json = {}; 2 | json.abool = true; 3 | json.abool2 = false; 4 | json.five = {}; 5 | json.five.alpha = []; 6 | json.five.alpha[0] = "fo"; 7 | json.five.alpha[1] = "fum"; 8 | json.five.beta = {}; 9 | json.five.beta.hey = "How's tricks?"; 10 | json.four = []; 11 | json.four[0] = 1; 12 | json.four[1] = 2; 13 | json.four[2] = 3; 14 | json.four[3] = 4; 15 | json.id = 66912849; 16 | json.isnull = null; 17 | json.one = 1; 18 | json.two = 2.2; 19 | json["three-b"] = "3"; 20 | -------------------------------------------------------------------------------- /testdata/one.jgron: -------------------------------------------------------------------------------- 1 | [[],{}] 2 | [["abool"],true] 3 | [["abool2"],false] 4 | [["five"],{}] 5 | [["five","alpha"],[]] 6 | [["five","alpha",0],"fo"] 7 | [["five","alpha",1],"fum"] 8 | [["five","beta"],{}] 9 | [["five","beta","hey"],"How's tricks?"] 10 | [["four"],[]] 11 | [["four",0],1] 12 | [["four",1],2] 13 | [["four",2],3] 14 | [["four",3],4] 15 | [["id"],66912849] 16 | [["isnull"],null] 17 | [["one"],1] 18 | [["two"],2.2] 19 | [["three-b"],"3"] 20 | -------------------------------------------------------------------------------- /testdata/one.json: -------------------------------------------------------------------------------- 1 | { 2 | "one": 1, 3 | "two": 2.2, 4 | "three-b": "3", 5 | "four": [1,2,3,4], 6 | "five": { 7 | "alpha": ["fo", "fum"], 8 | "beta": { 9 | "hey": "How's tricks?" 10 | } 11 | }, 12 | "abool": true, 13 | "abool2": false, 14 | "isnull": null, 15 | "id": 66912849 16 | } 17 | -------------------------------------------------------------------------------- /testdata/scalar-stream.gron: -------------------------------------------------------------------------------- 1 | json = []; 2 | json[0] = true; 3 | json[1] = false; 4 | json[2] = null; 5 | json[3] = "hello"; 6 | json[4] = 4; 7 | json[5] = 4.4; 8 | -------------------------------------------------------------------------------- /testdata/scalar-stream.jgron: -------------------------------------------------------------------------------- 1 | [[],[]] 2 | [[0],true] 3 | [[1],false] 4 | [[2],null] 5 | [[3],"hello"] 6 | [[4],4] 7 | [[5],4.4] 8 | -------------------------------------------------------------------------------- /testdata/scalar-stream.json: -------------------------------------------------------------------------------- 1 | true 2 | false 3 | null 4 | "hello" 5 | 4 6 | 4.4 7 | -------------------------------------------------------------------------------- /testdata/stream.gron: -------------------------------------------------------------------------------- 1 | json = []; 2 | json[0] = {}; 3 | json[0].one = 1; 4 | json[0].three = []; 5 | json[0].three[0] = 1; 6 | json[0].three[1] = 2; 7 | json[0].three[2] = 3; 8 | json[0].two = 2; 9 | json[1] = {}; 10 | json[1].one = 1; 11 | json[1].three = []; 12 | json[1].three[0] = 1; 13 | json[1].three[1] = 2; 14 | json[1].three[2] = 3; 15 | json[1].two = 2; 16 | -------------------------------------------------------------------------------- /testdata/stream.jgron: -------------------------------------------------------------------------------- 1 | [[],[]] 2 | [[0],{}] 3 | [[0,"one"],1] 4 | [[0,"three"],[]] 5 | [[0,"three",0],1] 6 | [[0,"three",1],2] 7 | [[0,"three",2],3] 8 | [[0,"two"],2] 9 | [[1],{}] 10 | [[1,"one"],1] 11 | [[1,"three"],[]] 12 | [[1,"three",0],1] 13 | [[1,"three",1],2] 14 | [[1,"three",2],3] 15 | [[1,"two"],2] 16 | -------------------------------------------------------------------------------- /testdata/stream.json: -------------------------------------------------------------------------------- 1 | {"one": 1, "two": 2, "three": [1, 2, 3]} 2 | {"one": 1, "two": 2, "three": [1, 2, 3]} 3 | -------------------------------------------------------------------------------- /testdata/three.gron: -------------------------------------------------------------------------------- 1 | json = {}; 2 | json.abool = true; 3 | json.abool2 = false; 4 | json.five = {}; 5 | json.five.alpha = []; 6 | json.five.alpha[0] = "fo"; 7 | json.five.alpha[1] = "fum"; 8 | json.five.beta = {}; 9 | json.five.beta.hey = "How's tricks?"; 10 | json.four = []; 11 | json.four[0] = 1; 12 | json.four[1] = 2; 13 | json.four[2] = 3; 14 | json.four[3] = 4; 15 | json.four[4] = 5; 16 | json.four[5] = 6; 17 | json.four[6] = 7; 18 | json.four[7] = 8; 19 | json.four[8] = 9; 20 | json.four[9] = 10; 21 | json.four[10] = 11; 22 | json.four[11] = 12; 23 | json.four[12] = 13; 24 | json.four[13] = 14; 25 | json.isnull = null; 26 | json.one = 1; 27 | json.two = 2.2; 28 | json.ಠ_ಠ = "yarly"; 29 | json["three-b"] = "3"; 30 | -------------------------------------------------------------------------------- /testdata/three.jgron: -------------------------------------------------------------------------------- 1 | [[],{}] 2 | [["abool"],true] 3 | [["abool2"],false] 4 | [["five"],{}] 5 | [["five","alpha"],[]] 6 | [["five","alpha",0],"fo"] 7 | [["five","alpha",1],"fum"] 8 | [["five","beta"],{}] 9 | [["five","beta","hey"],"How's tricks?"] 10 | [["four"],[]] 11 | [["four",0],1] 12 | [["four",1],2] 13 | [["four",2],3] 14 | [["four",3],4] 15 | [["four",4],5] 16 | [["four",5],6] 17 | [["four",6],7] 18 | [["four",7],8] 19 | [["four",8],9] 20 | [["four",9],10] 21 | [["four",10],11] 22 | [["four",11],12] 23 | [["four",12],13] 24 | [["four",13],14] 25 | [["isnull"],null] 26 | [["one"],1] 27 | [["two"],2.2] 28 | [["ಠ_ಠ"],"yarly"] 29 | [["three-b"],"3"] 30 | -------------------------------------------------------------------------------- /testdata/three.json: -------------------------------------------------------------------------------- 1 | { 2 | "one": 1, 3 | "two": 2.2, 4 | "three-b": "3", 5 | "four": [1,2,3,4,5,6,7,8,9,10,11,12,13,14], 6 | "five": { 7 | "alpha": ["fo", "fum"], 8 | "beta": { 9 | "hey": "How's tricks?" 10 | } 11 | }, 12 | "abool": true, 13 | "abool2": false, 14 | "isnull": null, 15 | "ಠ_ಠ": "yarly" 16 | } 17 | -------------------------------------------------------------------------------- /testdata/two-b.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Tom", 3 | "github": "https://github.com/tomnomnom/", 4 | "likes": ["code", "cheese", "meat"], 5 | "contact": { 6 | "email": "contact@tomnomnom.com", 7 | "twitter": "@TomNomNom" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /testdata/two.gron: -------------------------------------------------------------------------------- 1 | json = {}; 2 | json.contact = {}; 3 | json.contact.email = "mail@tomnomnom.com"; 4 | json.contact.twitter = "@TomNomNom"; 5 | json.github = "https://github.com/tomnomnom/"; 6 | json.likes = []; 7 | json.likes[0] = "code"; 8 | json.likes[1] = "cheese"; 9 | json.likes[2] = "meat"; 10 | json.name = "Tom"; 11 | -------------------------------------------------------------------------------- /testdata/two.jgron: -------------------------------------------------------------------------------- 1 | [[],{}] 2 | [["contact"],{}] 3 | [["contact","email"],"mail@tomnomnom.com"] 4 | [["contact","twitter"],"@TomNomNom"] 5 | [["github"],"https://github.com/tomnomnom/"] 6 | [["likes"],[]] 7 | [["likes",0],"code"] 8 | [["likes",1],"cheese"] 9 | [["likes",2],"meat"] 10 | [["name"],"Tom"] 11 | -------------------------------------------------------------------------------- /testdata/two.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Tom", 3 | "github": "https://github.com/tomnomnom/", 4 | "likes": ["code", "cheese", "meat"], 5 | "contact": { 6 | "email": "mail@tomnomnom.com", 7 | "twitter": "@TomNomNom" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /token.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "unicode" 8 | ) 9 | 10 | // A token is a chunk of text from a statement with a type 11 | type token struct { 12 | text string 13 | typ tokenTyp 14 | } 15 | 16 | // A tokenTyp identifies what kind of token something is 17 | type tokenTyp int 18 | 19 | const ( 20 | // A bare word is a unquoted key; like 'foo' in json.foo = 1; 21 | typBare tokenTyp = iota 22 | 23 | // Numeric key; like '2' in json[2] = "foo"; 24 | typNumericKey 25 | 26 | // A quoted key; like 'foo bar' in json["foo bar"] = 2; 27 | typQuotedKey 28 | 29 | // Punctuation types 30 | typDot // . 31 | typLBrace // [ 32 | typRBrace // ] 33 | typEquals // = 34 | typSemi // ; 35 | typComma // , 36 | 37 | // Value types 38 | typString // "foo" 39 | typNumber // 4 40 | typTrue // true 41 | typFalse // false 42 | typNull // null 43 | typEmptyArray // [] 44 | typEmptyObject // {} 45 | 46 | // Ignored token 47 | typIgnored 48 | 49 | // Error token 50 | typError 51 | ) 52 | 53 | // a sprintFn adds color to its input 54 | type sprintFn func(...interface{}) string 55 | 56 | // mapping of token types to the appropriate color sprintFn 57 | var sprintFns = map[tokenTyp]sprintFn{ 58 | typBare: bareColor.SprintFunc(), 59 | typNumericKey: numColor.SprintFunc(), 60 | typQuotedKey: strColor.SprintFunc(), 61 | typLBrace: braceColor.SprintFunc(), 62 | typRBrace: braceColor.SprintFunc(), 63 | typString: strColor.SprintFunc(), 64 | typNumber: numColor.SprintFunc(), 65 | typTrue: boolColor.SprintFunc(), 66 | typFalse: boolColor.SprintFunc(), 67 | typNull: boolColor.SprintFunc(), 68 | typEmptyArray: braceColor.SprintFunc(), 69 | typEmptyObject: braceColor.SprintFunc(), 70 | } 71 | 72 | // isValue returns true if the token is a valid value type 73 | func (t token) isValue() bool { 74 | switch t.typ { 75 | case typString, typNumber, typTrue, typFalse, typNull, typEmptyArray, typEmptyObject: 76 | return true 77 | default: 78 | return false 79 | } 80 | } 81 | 82 | // isPunct returns true if the token is a punctuation type 83 | func (t token) isPunct() bool { 84 | switch t.typ { 85 | case typDot, typLBrace, typRBrace, typEquals, typSemi, typComma: 86 | return true 87 | default: 88 | return false 89 | } 90 | } 91 | 92 | // format returns the formatted version of the token text 93 | func (t token) format() string { 94 | if t.typ == typEquals { 95 | return " " + t.text + " " 96 | } 97 | return t.text 98 | } 99 | 100 | // formatColor returns the colored formatted version of the token text 101 | func (t token) formatColor() string { 102 | text := t.text 103 | if t.typ == typEquals { 104 | text = " " + text + " " 105 | } 106 | fn, ok := sprintFns[t.typ] 107 | if ok { 108 | return fn(text) 109 | } 110 | return text 111 | 112 | } 113 | 114 | // valueTokenFromInterface takes any valid value and 115 | // returns a value token to represent it 116 | func valueTokenFromInterface(v interface{}) token { 117 | switch vv := v.(type) { 118 | 119 | case map[string]interface{}: 120 | return token{"{}", typEmptyObject} 121 | case []interface{}: 122 | return token{"[]", typEmptyArray} 123 | case json.Number: 124 | return token{vv.String(), typNumber} 125 | case string: 126 | return token{quoteString(vv), typString} 127 | case bool: 128 | if vv { 129 | return token{"true", typTrue} 130 | } 131 | return token{"false", typFalse} 132 | case nil: 133 | return token{"null", typNull} 134 | default: 135 | return token{"", typError} 136 | } 137 | } 138 | 139 | // quoteString takes a string and returns a quoted and 140 | // escaped string valid for use in gron output 141 | func quoteString(s string) string { 142 | 143 | out := &bytes.Buffer{} 144 | // bytes.Buffer never returns errors on these methods. 145 | // errors are explicitly ignored to keep the linter 146 | // happy. A price worth paying so that the linter 147 | // remains useful. 148 | _ = out.WriteByte('"') 149 | 150 | for _, r := range s { 151 | switch r { 152 | case '\\': 153 | _, _ = out.WriteString(`\\`) 154 | case '"': 155 | _, _ = out.WriteString(`\"`) 156 | case '\b': 157 | _, _ = out.WriteString(`\b`) 158 | case '\f': 159 | _, _ = out.WriteString(`\f`) 160 | case '\n': 161 | _, _ = out.WriteString(`\n`) 162 | case '\r': 163 | _, _ = out.WriteString(`\r`) 164 | case '\t': 165 | _, _ = out.WriteString(`\t`) 166 | // \u2028 and \u2029 are separator runes that are not valid 167 | // in javascript strings so they must be escaped. 168 | // See http://timelessrepo.com/json-isnt-a-javascript-subset 169 | case '\u2028': 170 | _, _ = out.WriteString(`\u2028`) 171 | case '\u2029': 172 | _, _ = out.WriteString(`\u2029`) 173 | default: 174 | // Any other control runes must be escaped 175 | if unicode.IsControl(r) { 176 | _, _ = fmt.Fprintf(out, `\u%04X`, r) 177 | } else { 178 | // Unescaped rune 179 | _, _ = out.WriteRune(r) 180 | } 181 | } 182 | } 183 | 184 | _ = out.WriteByte('"') 185 | return out.String() 186 | 187 | } 188 | -------------------------------------------------------------------------------- /token_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | ) 7 | 8 | var cases = []struct { 9 | in interface{} 10 | want token 11 | }{ 12 | {make(map[string]interface{}), token{"{}", typEmptyObject}}, 13 | {make([]interface{}, 0), token{"[]", typEmptyArray}}, 14 | {json.Number("1.2"), token{"1.2", typNumber}}, 15 | {"foo", token{`"foo"`, typString}}, 16 | {"<3", token{`"<3"`, typString}}, 17 | {"&", token{`"&"`, typString}}, 18 | {"\b", token{`"\b"`, typString}}, 19 | {"\f", token{`"\f"`, typString}}, 20 | {"\n", token{`"\n"`, typString}}, 21 | {"\r", token{`"\r"`, typString}}, 22 | {"\t", token{`"\t"`, typString}}, 23 | {"wat \u001e", token{`"wat \u001E"`, typString}}, 24 | {"Hello, 世界", token{`"Hello, 世界"`, typString}}, 25 | {true, token{"true", typTrue}}, 26 | {false, token{"false", typFalse}}, 27 | {nil, token{"null", typNull}}, 28 | {struct{}{}, token{"", typError}}, 29 | } 30 | 31 | func TestValueTokenFromInterface(t *testing.T) { 32 | 33 | for _, c := range cases { 34 | have := valueTokenFromInterface(c.in) 35 | 36 | if have != c.want { 37 | t.Logf("input: %#v", have) 38 | t.Logf("have: %#v", have) 39 | t.Logf("want: %#v", c.want) 40 | t.Errorf("have != want") 41 | } 42 | } 43 | } 44 | 45 | func BenchmarkValueTokenFromInterface(b *testing.B) { 46 | 47 | for i := 0; i < b.N; i++ { 48 | for _, c := range cases { 49 | _ = valueTokenFromInterface(c.in) 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /ungron.go: -------------------------------------------------------------------------------- 1 | // Ungronning is the reverse of gronning: turn statements 2 | // back into JSON. The expected input grammar is: 3 | // 4 | // Input ::= '--'* Statement (Statement | '--')* 5 | // Statement ::= Path Space* "=" Space* Value ";" "\n" 6 | // Path ::= (BareWord) ("." BareWord | ("[" Key "]"))* 7 | // Value ::= String | Number | "true" | "false" | "null" | "[]" | "{}" 8 | // BareWord ::= (UnicodeLu | UnicodeLl | UnicodeLm | UnicodeLo | UnicodeNl | '$' | '_') (UnicodeLu | UnicodeLl | UnicodeLm | UnicodeLo | UnicodeNl | UnicodeMn | UnicodeMc | UnicodeNd | UnicodePc | '$' | '_')* 9 | // Key ::= [0-9]+ | String 10 | // String ::= '"' (UnescapedRune | ("\" (["\/bfnrt] | ('u' Hex))))* '"' 11 | // UnescapedRune ::= [^#x0-#x1f"\] 12 | 13 | package main 14 | 15 | import ( 16 | "encoding/json" 17 | "fmt" 18 | "reflect" 19 | "strconv" 20 | "strings" 21 | "unicode" 22 | "unicode/utf8" 23 | 24 | "github.com/pkg/errors" 25 | ) 26 | 27 | // errRecoverable is an error type to represent errors that 28 | // can be recovered from; e.g. an empty line in the input 29 | type errRecoverable struct { 30 | msg string 31 | } 32 | 33 | func (e errRecoverable) Error() string { 34 | return e.msg 35 | } 36 | 37 | // A lexer holds the state for lexing statements 38 | type lexer struct { 39 | text string // The raw input text 40 | pos int // The current byte offset in the text 41 | width int // The width of the current rune in bytes 42 | cur rune // The rune at the current position 43 | prev rune // The rune at the previous position 44 | tokens []token // The tokens that have been emitted 45 | tokenStart int // The starting position of the current token 46 | } 47 | 48 | // newLexer returns a new lexer for the provided input string 49 | func newLexer(text string) *lexer { 50 | return &lexer{ 51 | text: text, 52 | pos: 0, 53 | tokenStart: 0, 54 | tokens: make([]token, 0), 55 | } 56 | } 57 | 58 | // lex runs the lexer and returns the lexed statement 59 | func (l *lexer) lex() statement { 60 | 61 | for lexfn := lexStatement; lexfn != nil; { 62 | lexfn = lexfn(l) 63 | } 64 | return l.tokens 65 | } 66 | 67 | // next gets the next rune in the input and updates the lexer state 68 | func (l *lexer) next() rune { 69 | r, w := utf8.DecodeRuneInString(l.text[l.pos:]) 70 | 71 | l.pos += w 72 | l.width = w 73 | 74 | l.prev = l.cur 75 | l.cur = r 76 | 77 | return r 78 | } 79 | 80 | // backup moves the lexer back one rune 81 | // can only be used once per call of next() 82 | func (l *lexer) backup() { 83 | l.pos -= l.width 84 | } 85 | 86 | // peek returns the next rune in the input 87 | // without moving the internal pointer 88 | func (l *lexer) peek() rune { 89 | r := l.next() 90 | l.backup() 91 | return r 92 | } 93 | 94 | // ignore skips the current token 95 | func (l *lexer) ignore() { 96 | l.tokenStart = l.pos 97 | } 98 | 99 | // emit adds the current token to the token slice and 100 | // moves the tokenStart pointer to the current position 101 | func (l *lexer) emit(typ tokenTyp) { 102 | t := token{ 103 | text: l.text[l.tokenStart:l.pos], 104 | typ: typ, 105 | } 106 | l.tokenStart = l.pos 107 | 108 | l.tokens = append(l.tokens, t) 109 | } 110 | 111 | // accept moves the pointer if the next rune is in 112 | // the set of valid runes 113 | func (l *lexer) accept(valid string) bool { 114 | if strings.ContainsRune(valid, l.next()) { 115 | return true 116 | } 117 | l.backup() 118 | return false 119 | } 120 | 121 | // acceptRun continually accepts runes from the 122 | // set of valid runes 123 | func (l *lexer) acceptRun(valid string) { 124 | for strings.ContainsRune(valid, l.next()) { 125 | } 126 | l.backup() 127 | } 128 | 129 | // a runeCheck is a function that determines if a rune is valid 130 | // or not so that we can do complex checks against runes 131 | type runeCheck func(rune) bool 132 | 133 | // acceptFunc accepts a rune if the provided runeCheck 134 | // function returns true 135 | func (l *lexer) acceptFunc(fn runeCheck) bool { 136 | if fn(l.next()) { 137 | return true 138 | } 139 | l.backup() 140 | return false 141 | } 142 | 143 | // acceptRunFunc continually accepts runes for as long 144 | // as the runeCheck function returns true 145 | func (l *lexer) acceptRunFunc(fn runeCheck) { 146 | for fn(l.next()) { 147 | } 148 | l.backup() 149 | } 150 | 151 | // acceptUntil accepts runes until it hits a delimiter 152 | // rune contained in the provided string 153 | func (l *lexer) acceptUntil(delims string) { 154 | for !strings.ContainsRune(delims, l.next()) { 155 | if l.cur == utf8.RuneError { 156 | return 157 | } 158 | } 159 | l.backup() 160 | } 161 | 162 | // acceptUntilUnescaped accepts runes until it hits a delimiter 163 | // rune contained in the provided string, unless that rune was 164 | // escaped with a backslash 165 | func (l *lexer) acceptUntilUnescaped(delims string) { 166 | 167 | // Read until we hit an unescaped rune or the end of the input 168 | inEscape := false 169 | for { 170 | r := l.next() 171 | if r == '\\' && !inEscape { 172 | inEscape = true 173 | continue 174 | } 175 | if strings.ContainsRune(delims, r) && !inEscape { 176 | l.backup() 177 | return 178 | } 179 | if l.cur == utf8.RuneError { 180 | return 181 | } 182 | inEscape = false 183 | } 184 | } 185 | 186 | // a lexFn accepts a lexer, performs some action on it and 187 | // then returns an appropriate lexFn for the next stage 188 | type lexFn func(*lexer) lexFn 189 | 190 | // lexStatement is the highest level lexFn. Its only job 191 | // is to determine which more specific lexFn to use 192 | func lexStatement(l *lexer) lexFn { 193 | r := l.peek() 194 | 195 | switch { 196 | case r == '.' || validFirstRune(r): 197 | return lexBareWord 198 | case r == '[': 199 | return lexBraces 200 | case r == ' ', r == '=': 201 | return lexValue 202 | case r == '-': 203 | // grep -A etc can add '--' lines to output 204 | // we'll save the text but not actually do 205 | // anything with them 206 | return lexIgnore 207 | case r == utf8.RuneError: 208 | return nil 209 | default: 210 | l.emit(typError) 211 | return nil 212 | } 213 | 214 | } 215 | 216 | // lexBareWord lexes for bare identifiers. 217 | // E.g: the 'foo' in 'foo.bar' or 'foo[0]' is a bare identifier 218 | func lexBareWord(l *lexer) lexFn { 219 | if l.accept(".") { 220 | l.emit(typDot) 221 | } 222 | 223 | if !l.acceptFunc(validFirstRune) { 224 | l.emit(typError) 225 | return nil 226 | } 227 | l.acceptRunFunc(validSecondaryRune) 228 | l.emit(typBare) 229 | 230 | return lexStatement 231 | } 232 | 233 | // lexBraces lexes keys contained within square braces 234 | func lexBraces(l *lexer) lexFn { 235 | l.accept("[") 236 | l.emit(typLBrace) 237 | 238 | switch { 239 | case unicode.IsNumber(l.peek()): 240 | return lexNumericKey 241 | case l.peek() == '"': 242 | return lexQuotedKey 243 | default: 244 | l.emit(typError) 245 | return nil 246 | } 247 | } 248 | 249 | // lexNumericKey lexes numeric keys between square braces 250 | func lexNumericKey(l *lexer) lexFn { 251 | l.accept("[") 252 | l.ignore() 253 | 254 | l.acceptRunFunc(unicode.IsNumber) 255 | l.emit(typNumericKey) 256 | 257 | if l.accept("]") { 258 | l.emit(typRBrace) 259 | } else { 260 | l.emit(typError) 261 | return nil 262 | } 263 | l.ignore() 264 | return lexStatement 265 | } 266 | 267 | // lexQuotedKey lexes quoted keys between square braces 268 | func lexQuotedKey(l *lexer) lexFn { 269 | l.accept("[") 270 | l.ignore() 271 | 272 | l.accept(`"`) 273 | 274 | l.acceptUntilUnescaped(`"`) 275 | l.accept(`"`) 276 | l.emit(typQuotedKey) 277 | 278 | if l.accept("]") { 279 | l.emit(typRBrace) 280 | } else { 281 | l.emit(typError) 282 | return nil 283 | } 284 | l.ignore() 285 | return lexStatement 286 | } 287 | 288 | // lexValue lexes a value at the end of a statement 289 | func lexValue(l *lexer) lexFn { 290 | l.acceptRun(" ") 291 | l.ignore() 292 | 293 | if l.accept("=") { 294 | l.emit(typEquals) 295 | } else { 296 | return nil 297 | } 298 | l.acceptRun(" ") 299 | l.ignore() 300 | 301 | switch { 302 | 303 | case l.accept(`"`): 304 | l.acceptUntilUnescaped(`"`) 305 | l.accept(`"`) 306 | l.emit(typString) 307 | 308 | case l.accept("t"): 309 | l.acceptRun("rue") 310 | l.emit(typTrue) 311 | 312 | case l.accept("f"): 313 | l.acceptRun("alse") 314 | l.emit(typFalse) 315 | 316 | case l.accept("n"): 317 | l.acceptRun("ul") 318 | l.emit(typNull) 319 | 320 | case l.accept("["): 321 | l.accept("]") 322 | l.emit(typEmptyArray) 323 | 324 | case l.accept("{"): 325 | l.accept("}") 326 | l.emit(typEmptyObject) 327 | 328 | default: 329 | // Assume number 330 | l.acceptUntil(";") 331 | l.emit(typNumber) 332 | } 333 | 334 | l.acceptRun(" ") 335 | l.ignore() 336 | 337 | if l.accept(";") { 338 | l.emit(typSemi) 339 | } 340 | 341 | // The value should always be the last thing 342 | // in the statement 343 | return nil 344 | } 345 | 346 | // lexIgnore accepts runes until the end of the input 347 | // and emits them as a typIgnored token 348 | func lexIgnore(l *lexer) lexFn { 349 | l.acceptRunFunc(func(r rune) bool { 350 | return r != utf8.RuneError 351 | }) 352 | l.emit(typIgnored) 353 | return nil 354 | } 355 | 356 | // ungronTokens turns a slice of tokens into an actual datastructure 357 | func ungronTokens(ts []token) (interface{}, error) { 358 | if len(ts) == 0 { 359 | return nil, errRecoverable{"empty input"} 360 | } 361 | 362 | if ts[0].typ == typIgnored { 363 | return nil, errRecoverable{"ignored token"} 364 | } 365 | 366 | if ts[len(ts)-1].typ == typError { 367 | return nil, errors.New("invalid statement") 368 | } 369 | 370 | // The last token should be typSemi so we need to check 371 | // the second to last token is a value rather than the 372 | // last one 373 | if len(ts) > 1 && !ts[len(ts)-2].isValue() { 374 | return nil, errors.New("statement has no value") 375 | } 376 | 377 | t := ts[0] 378 | switch { 379 | case t.isPunct(): 380 | // Skip the token 381 | val, err := ungronTokens(ts[1:]) 382 | if err != nil { 383 | return nil, err 384 | } 385 | return val, nil 386 | 387 | case t.isValue(): 388 | var val interface{} 389 | d := json.NewDecoder(strings.NewReader(t.text)) 390 | d.UseNumber() 391 | err := d.Decode(&val) 392 | if err != nil { 393 | return nil, fmt.Errorf("invalid value `%s`", t.text) 394 | } 395 | return val, nil 396 | 397 | case t.typ == typBare: 398 | val, err := ungronTokens(ts[1:]) 399 | if err != nil { 400 | return nil, err 401 | } 402 | out := make(map[string]interface{}) 403 | out[t.text] = val 404 | return out, nil 405 | 406 | case t.typ == typQuotedKey: 407 | val, err := ungronTokens(ts[1:]) 408 | if err != nil { 409 | return nil, err 410 | } 411 | key := "" 412 | err = json.Unmarshal([]byte(t.text), &key) 413 | if err != nil { 414 | return nil, fmt.Errorf("invalid quoted key `%s`", t.text) 415 | } 416 | 417 | out := make(map[string]interface{}) 418 | out[key] = val 419 | return out, nil 420 | 421 | case t.typ == typNumericKey: 422 | key, err := strconv.Atoi(t.text) 423 | if err != nil { 424 | return nil, fmt.Errorf("invalid integer key `%s`", t.text) 425 | } 426 | 427 | val, err := ungronTokens(ts[1:]) 428 | if err != nil { 429 | return nil, err 430 | } 431 | 432 | // There needs to be at least key + 1 space in the array 433 | out := make([]interface{}, key+1) 434 | out[key] = val 435 | return out, nil 436 | 437 | default: 438 | return nil, fmt.Errorf("unexpected token `%s`", t.text) 439 | } 440 | } 441 | 442 | // recursiveMerge merges maps and slices, or returns b for scalars 443 | func recursiveMerge(a, b interface{}) (interface{}, error) { 444 | switch a.(type) { 445 | 446 | case map[string]interface{}: 447 | bMap, ok := b.(map[string]interface{}) 448 | if !ok { 449 | return nil, fmt.Errorf("cannot merge object with non-object") 450 | } 451 | return recursiveMapMerge(a.(map[string]interface{}), bMap) 452 | 453 | case []interface{}: 454 | bSlice, ok := b.([]interface{}) 455 | if !ok { 456 | return nil, fmt.Errorf("cannot merge array with non-array") 457 | } 458 | return recursiveSliceMerge(a.([]interface{}), bSlice) 459 | 460 | case string, int, float64, bool, nil, json.Number: 461 | // Can't merge them, second one wins 462 | return b, nil 463 | 464 | default: 465 | return nil, fmt.Errorf("unexpected data type for merge: `%s`", reflect.TypeOf(a)) 466 | } 467 | } 468 | 469 | // recursiveMapMerge recursively merges map[string]interface{} values 470 | func recursiveMapMerge(a, b map[string]interface{}) (map[string]interface{}, error) { 471 | // Merge keys from b into a 472 | for k, v := range b { 473 | _, exists := a[k] 474 | if !exists { 475 | // Doesn't exist in a, just add it in 476 | a[k] = v 477 | } else { 478 | // Does exist, merge the values 479 | merged, err := recursiveMerge(a[k], b[k]) 480 | if err != nil { 481 | return nil, err 482 | } 483 | 484 | a[k] = merged 485 | } 486 | } 487 | return a, nil 488 | } 489 | 490 | // recursiveSliceMerge recursively merged []interface{} values 491 | func recursiveSliceMerge(a, b []interface{}) ([]interface{}, error) { 492 | // We need a new slice with the capacity of whichever 493 | // slive is biggest 494 | outLen := len(a) 495 | if len(b) > outLen { 496 | outLen = len(b) 497 | } 498 | out := make([]interface{}, outLen) 499 | 500 | // Copy the values from 'a' into the output slice 501 | copy(out, a) 502 | 503 | // Add the values from 'b'; merging existing keys 504 | for k, v := range b { 505 | if out[k] == nil { 506 | out[k] = v 507 | } else if v != nil { 508 | merged, err := recursiveMerge(out[k], b[k]) 509 | if err != nil { 510 | return nil, err 511 | } 512 | out[k] = merged 513 | } 514 | } 515 | return out, nil 516 | } 517 | -------------------------------------------------------------------------------- /ungron_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestLex(t *testing.T) { 9 | cases := []struct { 10 | in string 11 | want []token 12 | }{ 13 | {`json.foo = 1;`, []token{ 14 | {`json`, typBare}, 15 | {`.`, typDot}, 16 | {`foo`, typBare}, 17 | {`=`, typEquals}, 18 | {`1`, typNumber}, 19 | {`;`, typSemi}, 20 | }}, 21 | 22 | {`json.foo = "bar";`, []token{ 23 | {`json`, typBare}, 24 | {`.`, typDot}, 25 | {`foo`, typBare}, 26 | {`=`, typEquals}, 27 | {`"bar"`, typString}, 28 | {`;`, typSemi}, 29 | }}, 30 | 31 | {`json.foo = "ba;r";`, []token{ 32 | {`json`, typBare}, 33 | {`.`, typDot}, 34 | {`foo`, typBare}, 35 | {`=`, typEquals}, 36 | {`"ba;r"`, typString}, 37 | {`;`, typSemi}, 38 | }}, 39 | 40 | {`json.foo = "ba\"r ;";`, []token{ 41 | {`json`, typBare}, 42 | {`.`, typDot}, 43 | {`foo`, typBare}, 44 | {`=`, typEquals}, 45 | {`"ba\"r ;"`, typString}, 46 | {`;`, typSemi}, 47 | }}, 48 | 49 | {`json = "\\";`, []token{ 50 | {`json`, typBare}, 51 | {`=`, typEquals}, 52 | {`"\\"`, typString}, 53 | {`;`, typSemi}, 54 | }}, 55 | 56 | {`json = "\\\\";`, []token{ 57 | {`json`, typBare}, 58 | {`=`, typEquals}, 59 | {`"\\\\"`, typString}, 60 | {`;`, typSemi}, 61 | }}, 62 | 63 | {`json = "f\oo\\";`, []token{ 64 | {`json`, typBare}, 65 | {`=`, typEquals}, 66 | {`"f\oo\\"`, typString}, 67 | {`;`, typSemi}, 68 | }}, 69 | 70 | {`json.value = "\u003c ;";`, []token{ 71 | {`json`, typBare}, 72 | {`.`, typDot}, 73 | {`value`, typBare}, 74 | {`=`, typEquals}, 75 | {`"\u003c ;"`, typString}, 76 | {`;`, typSemi}, 77 | }}, 78 | 79 | {`json[0] = "bar";`, []token{ 80 | {`json`, typBare}, 81 | {`[`, typLBrace}, 82 | {`0`, typNumericKey}, 83 | {`]`, typRBrace}, 84 | {`=`, typEquals}, 85 | {`"bar"`, typString}, 86 | {`;`, typSemi}, 87 | }}, 88 | 89 | {`json["foo"] = "bar";`, []token{ 90 | {`json`, typBare}, 91 | {`[`, typLBrace}, 92 | {`"foo"`, typQuotedKey}, 93 | {`]`, typRBrace}, 94 | {`=`, typEquals}, 95 | {`"bar"`, typString}, 96 | {`;`, typSemi}, 97 | }}, 98 | 99 | {`json.foo["bar"][0] = "bar";`, []token{ 100 | {`json`, typBare}, 101 | {`.`, typDot}, 102 | {`foo`, typBare}, 103 | {`[`, typLBrace}, 104 | {`"bar"`, typQuotedKey}, 105 | {`]`, typRBrace}, 106 | {`[`, typLBrace}, 107 | {`0`, typNumericKey}, 108 | {`]`, typRBrace}, 109 | {`=`, typEquals}, 110 | {`"bar"`, typString}, 111 | {`;`, typSemi}, 112 | }}, 113 | 114 | {`not an identifier at all`, []token{ 115 | {`not`, typBare}, 116 | }}, 117 | 118 | {`alsonotanidentifier`, []token{ 119 | {`alsonotanidentifier`, typBare}, 120 | }}, 121 | 122 | {`wat!`, []token{ 123 | {`wat`, typBare}, 124 | {``, typError}, 125 | }}, 126 | 127 | {`json[ = 1;`, []token{ 128 | {`json`, typBare}, 129 | {`[`, typLBrace}, 130 | {``, typError}, 131 | }}, 132 | 133 | {`json.[2] = 1;`, []token{ 134 | {`json`, typBare}, 135 | {`.`, typDot}, 136 | {``, typError}, 137 | }}, 138 | 139 | {`json[1 = 1;`, []token{ 140 | {`json`, typBare}, 141 | {`[`, typLBrace}, 142 | {`1`, typNumericKey}, 143 | {``, typError}, 144 | }}, 145 | 146 | {`json["foo] = 1;`, []token{ 147 | {`json`, typBare}, 148 | {`[`, typLBrace}, 149 | {`"foo] = 1;`, typQuotedKey}, 150 | {``, typError}, 151 | }}, 152 | 153 | {`--`, []token{ 154 | {`--`, typIgnored}, 155 | }}, 156 | 157 | {`json = 1;`, []token{ 158 | {`json`, typBare}, 159 | {`=`, typEquals}, 160 | {`1`, typNumber}, 161 | {`;`, typSemi}, 162 | }}, 163 | 164 | {`json=1;`, []token{ 165 | {`json`, typBare}, 166 | {`=`, typEquals}, 167 | {`1`, typNumber}, 168 | {`;`, typSemi}, 169 | }}, 170 | } 171 | 172 | for _, c := range cases { 173 | l := newLexer(c.in) 174 | have := l.lex() 175 | 176 | if len(have) != len(c.want) { 177 | t.Logf("Input: %#v", c.in) 178 | t.Logf("Want: %#v", c.want) 179 | t.Logf("Have: %#v", have) 180 | t.Fatalf("want %d tokens, have %d", len(c.want), len(have)) 181 | } 182 | 183 | for i := range have { 184 | if have[i] != c.want[i] { 185 | t.Logf("Input: %#v", c.in) 186 | t.Logf("Want: %#v", c.want) 187 | t.Logf("Have: %#v", have) 188 | t.Errorf("Want `%#v` in position %d, have `%#v`", c.want[i], i, have[i]) 189 | } 190 | } 191 | } 192 | } 193 | 194 | func TestUngronTokensSimple(t *testing.T) { 195 | in := `json.contact["e-mail"][0] = "mail@tomnomnom.com";` 196 | want := map[string]interface{}{ 197 | "json": map[string]interface{}{ 198 | "contact": map[string]interface{}{ 199 | "e-mail": []interface{}{ 200 | "mail@tomnomnom.com", 201 | }, 202 | }, 203 | }, 204 | } 205 | 206 | l := newLexer(in) 207 | tokens := l.lex() 208 | have, err := ungronTokens(tokens) 209 | 210 | if err != nil { 211 | t.Fatalf("failed to ungron statement: %s", err) 212 | } 213 | 214 | t.Logf("Have: %#v", have) 215 | t.Logf("Want: %#v", want) 216 | 217 | eq := reflect.DeepEqual(have, want) 218 | if !eq { 219 | t.Errorf("Have and want datastructures are unequal") 220 | } 221 | } 222 | 223 | func TestUngronTokensInvalid(t *testing.T) { 224 | cases := []struct { 225 | in []token 226 | }{ 227 | {[]token{{``, typError}}}, // Error token 228 | {[]token{{`foo`, typString}}}, // Invalid value 229 | {[]token{{`"foo`, typQuotedKey}, {"1", typNumber}}}, // Invalid quoted key 230 | {[]token{{`foo`, typNumericKey}, {"1", typNumber}}}, // Invalid numeric key 231 | {[]token{{``, -255}, {"1", typNumber}}}, // Invalid token type 232 | } 233 | 234 | for _, c := range cases { 235 | _, err := ungronTokens(c.in) 236 | if err == nil { 237 | t.Errorf("want non-nil error for %#v; have nil", c.in) 238 | } 239 | } 240 | } 241 | 242 | func TestMerge(t *testing.T) { 243 | a := map[string]interface{}{ 244 | "json": map[string]interface{}{ 245 | "contact": map[string]interface{}{ 246 | "e-mail": []interface{}{ 247 | 0: "mail@tomnomnom.com", 248 | }, 249 | }, 250 | }, 251 | } 252 | 253 | b := map[string]interface{}{ 254 | "json": map[string]interface{}{ 255 | "contact": map[string]interface{}{ 256 | "e-mail": []interface{}{ 257 | 1: "test@tomnomnom.com", 258 | 3: "foo@tomnomnom.com", 259 | }, 260 | "twitter": "@TomNomNom", 261 | }, 262 | }, 263 | } 264 | 265 | want := map[string]interface{}{ 266 | "json": map[string]interface{}{ 267 | "contact": map[string]interface{}{ 268 | "e-mail": []interface{}{ 269 | 0: "mail@tomnomnom.com", 270 | 1: "test@tomnomnom.com", 271 | 3: "foo@tomnomnom.com", 272 | }, 273 | "twitter": "@TomNomNom", 274 | }, 275 | }, 276 | } 277 | 278 | t.Logf("A: %#v", a) 279 | t.Logf("B: %#v", b) 280 | have, err := recursiveMerge(a, b) 281 | if err != nil { 282 | t.Fatalf("failed to merge datastructures: %s", err) 283 | } 284 | 285 | t.Logf("Have: %#v", have) 286 | t.Logf("Want: %#v", want) 287 | eq := reflect.DeepEqual(have, want) 288 | if !eq { 289 | t.Errorf("Have and want datastructures are unequal") 290 | } 291 | 292 | } 293 | -------------------------------------------------------------------------------- /url.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "crypto/tls" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | neturl "net/url" 10 | "os" 11 | "regexp" 12 | "strings" 13 | "time" 14 | ) 15 | 16 | func validURL(url string) bool { 17 | r := regexp.MustCompile("(?i)^http(?:s)?://") 18 | return r.MatchString(url) 19 | } 20 | 21 | func configureProxy(url string, proxy string, noProxy string) func(*http.Request) (*neturl.URL, error) { 22 | cURL, err := neturl.Parse(url) 23 | if err != nil { 24 | return nil 25 | } 26 | 27 | // Direct arguments are superior to environment variables. 28 | if proxy == undefinedProxy { 29 | proxy = os.Getenv(fmt.Sprintf("%s_proxy", cURL.Scheme)) 30 | } 31 | if noProxy == undefinedProxy { 32 | noProxy = os.Getenv("no_proxy") 33 | } 34 | 35 | // Skip setting a proxy if no proxy has been set through env variable or 36 | // argument. 37 | if proxy == "" { 38 | return nil 39 | } 40 | 41 | // Test if any of the hosts mentioned in the noProxy variable or the 42 | // no_proxy env variable. Skip setting up the proxy if a match is found. 43 | noProxyHosts := strings.Split(noProxy, ",") 44 | if len(noProxyHosts) > 0 { 45 | for _, noProxyHost := range noProxyHosts { 46 | if len(noProxyHost) == 0 { 47 | continue 48 | } 49 | // Test for direct matches of the hostname. 50 | if cURL.Host == noProxyHost { 51 | return nil 52 | } 53 | // Match through wildcard-like pattern, e.g. ".foobar.com" should 54 | // match all subdomains of foobar.com. 55 | if strings.HasPrefix(noProxyHost, ".") && strings.HasSuffix(cURL.Host, noProxyHost) { 56 | return nil 57 | } 58 | } 59 | } 60 | 61 | proxyURL, err := neturl.Parse(proxy) 62 | if err != nil { 63 | return nil 64 | } 65 | 66 | return http.ProxyURL(proxyURL) 67 | } 68 | 69 | func getURL(url string, insecure bool, proxyURL string, noProxy string) (io.Reader, error) { 70 | tr := &http.Transport{ 71 | TLSClientConfig: &tls.Config{InsecureSkipVerify: insecure}, 72 | } 73 | // Set proxy if defined. 74 | proxy := configureProxy(url, proxyURL, noProxy) 75 | if proxy != nil { 76 | tr.Proxy = proxy 77 | } 78 | client := http.Client{ 79 | Transport: tr, 80 | Timeout: 20 * time.Second, 81 | } 82 | 83 | req, err := http.NewRequest("GET", url, nil) 84 | if err != nil { 85 | return nil, err 86 | } 87 | req.Header.Set("User-Agent", fmt.Sprintf("gron/%s", gronVersion)) 88 | req.Header.Set("Accept", "application/json") 89 | 90 | resp, err := client.Do(req) 91 | 92 | if err != nil { 93 | return nil, err 94 | } 95 | 96 | return bufio.NewReader(resp.Body), err 97 | } 98 | -------------------------------------------------------------------------------- /url_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | ) 7 | 8 | func TestValidURL(t *testing.T) { 9 | tests := []struct { 10 | url string 11 | want bool 12 | }{ 13 | {"http://test.com", true}, 14 | {"https://test.com", true}, 15 | {"HttPs://test.com", true}, 16 | {"/test/test.com", false}, 17 | {"", false}, 18 | } 19 | 20 | for _, test := range tests { 21 | have := validURL(test.url) 22 | if have != test.want { 23 | t.Errorf("Want %t for validURL(%s); have %t", test.want, test.url, have) 24 | } 25 | } 26 | } 27 | 28 | func TestConfigureProxyHttp(t *testing.T) { 29 | tests := []struct { 30 | url string 31 | httpProxy string 32 | envHttpProxy string 33 | noProxy string 34 | envNoProxy string 35 | hasProxy bool 36 | }{ 37 | // http proxy via env variables 38 | {"http://test1.com", undefinedProxy, "http://localhost:1234", undefinedProxy, "", true}, 39 | {"https://test1.com", undefinedProxy, "http://localhost:1234", undefinedProxy, "", false}, 40 | {"schema://test1.com", undefinedProxy, "http://localhost:1234", undefinedProxy, "", false}, 41 | 42 | // http proxy with env variables, overwritten by argument 43 | {"http://test2.com", "", "http://localhost:1234", undefinedProxy, "", false}, 44 | {"https://test2.com", "", "http://localhost:1234", undefinedProxy, "", false}, 45 | {"schema://test2.com", "", "http://localhost:1234", undefinedProxy, "", false}, 46 | 47 | // http proxy with env variables, domain excluded by no_proxy 48 | {"http://test3.com", undefinedProxy, "http://localhost:1234", undefinedProxy, "test3.com,foobar3.com", false}, 49 | {"http://foobar3.com", undefinedProxy, "http://localhost:1234", undefinedProxy, "test3.com,foobar3.com", false}, 50 | {"http://test.foobar3.com", undefinedProxy, "http://localhost:1234", undefinedProxy, ".foobar3.com", false}, 51 | {"https://test3.com", undefinedProxy, "http://localhost:1234", undefinedProxy, "test3.com,foobar3.com", false}, 52 | {"schema://test3.com", undefinedProxy, "http://localhost:1234", undefinedProxy, "test3.com,foobar3.com", false}, 53 | 54 | // http proxy with env variables, domain excluded by no_proxy, overwritten by argument 55 | {"http://test4.com", undefinedProxy, "http://localhost:1234", "", "test4.com,foobar4.com", true}, 56 | {"http://foobar4.com", undefinedProxy, "http://localhost:1234", "", "test4.com,foobar4.com", true}, 57 | {"http://test.foobar4.com", undefinedProxy, "http://localhost:1234", "", ".foobar4.com", true}, 58 | {"https://test4.com", undefinedProxy, "http://localhost:1234", "", "test4.com,foobar4.com", false}, 59 | {"schema://test4.com", undefinedProxy, "http://localhost:1234", "", "test4.com,foobar4.com", false}, 60 | } 61 | 62 | for _, test := range tests { 63 | os.Setenv("http_proxy", test.envHttpProxy) 64 | os.Setenv("no_proxy", test.envNoProxy) 65 | proxy := configureProxy(test.url, test.httpProxy, test.noProxy) 66 | hasProxy := proxy != nil 67 | if hasProxy != test.hasProxy { 68 | t.Errorf("Want %t for configureProxy; have %t; %v", test.hasProxy, hasProxy, test) 69 | } 70 | os.Unsetenv("http_proxy") 71 | os.Unsetenv("no_proxy") 72 | } 73 | } 74 | 75 | func TestConfigureProxyHttps(t *testing.T) { 76 | tests := []struct { 77 | url string 78 | httpsProxy string 79 | envHttpsProxy string 80 | noProxy string 81 | envNoProxy string 82 | hasProxy bool 83 | }{ 84 | // https proxy via env variables 85 | {"http://test1.com", undefinedProxy, "http://localhost:1234", undefinedProxy, "", false}, 86 | {"https://test1.com", undefinedProxy, "http://localhost:1234", undefinedProxy, "", true}, 87 | {"schema://test1.com", undefinedProxy, "http://localhost:1234", undefinedProxy, "", false}, 88 | 89 | // https proxy with env variables, overwritten by argument 90 | {"http://test2.com", "", "http://localhost:1234", undefinedProxy, "", false}, 91 | {"https://test2.com", "", "http://localhost:1234", undefinedProxy, "", false}, 92 | {"schema://test2.com", "", "http://localhost:1234", undefinedProxy, "", false}, 93 | 94 | // https proxy with env variables, domain excluded by no_proxy 95 | {"http://test3.com", undefinedProxy, "http://localhost:1234", undefinedProxy, "test3.com,foobar3.com", false}, 96 | {"http://foobar3.com", undefinedProxy, "http://localhost:1234", undefinedProxy, "test3.com,foobar3.com", false}, 97 | {"http://test.foobar3.com", undefinedProxy, "http://localhost:1234", undefinedProxy, ".foobar3.com", false}, 98 | {"https://test3.com", undefinedProxy, "http://localhost:1234", undefinedProxy, "test3.com,foobar3.com", false}, 99 | {"schema://test3.com", undefinedProxy, "http://localhost:1234", undefinedProxy, "test3.com,foobar3.com", false}, 100 | 101 | // https proxy with env variables, domain excluded by no_proxy, overwritten by argument 102 | {"http://test4.com", undefinedProxy, "http://localhost:1234", "", "test4.com,foobar4.com", false}, 103 | {"http://foobar4.com", undefinedProxy, "http://localhost:1234", "", "test4.com,foobar4.com", false}, 104 | {"http://test.foobar4.com", undefinedProxy, "http://localhost:1234", "", ".foobar4.com", false}, 105 | {"https://test4.com", undefinedProxy, "http://localhost:1234", "", "test4.com,foobar4.com", true}, 106 | {"schema://test4.com", undefinedProxy, "http://localhost:1234", "", "test4.com,foobar4.com", false}, 107 | } 108 | 109 | for _, test := range tests { 110 | os.Setenv("https_proxy", test.envHttpsProxy) 111 | os.Setenv("no_proxy", test.envNoProxy) 112 | proxy := configureProxy(test.url, test.httpsProxy, test.noProxy) 113 | hasProxy := proxy != nil 114 | if hasProxy != test.hasProxy { 115 | t.Errorf("Want %t for configureProxy; have %t; %v", test.hasProxy, hasProxy, test) 116 | } 117 | os.Unsetenv("https_proxy") 118 | os.Unsetenv("no_proxy") 119 | } 120 | } 121 | --------------------------------------------------------------------------------