├── LICENSE ├── README.md └── sources ├── ljt └── ljt.min /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Joel Bruner 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.md: -------------------------------------------------------------------------------- 1 | # ljt - Little JSON Tool 2 | 3 | `ljt` is a shell utility written in JavaScript that can quickly extract values from JSON data. It can be used standalone or embedded as a function into _your_ shell scripts and only requires `jsc` the [JavaScriptCore](https://trac.webkit.org/wiki/JavaScriptCore) binary, which is standard on every Mac and available on many \*nix distributions. If you need advanced JSON manipulation and querying, see my other project [jpt](https://github.com/brunerd/jpt) the JSON Power Tool. 4 | 5 | ### Usage: 6 | `ljt [query] [filepath]` 7 | 8 | `[query]` is optional and may be either [JSON Pointer](https://datatracker.ietf.org/doc/html/rfc6901), [JSONPath](https://datatracker.ietf.org/doc/draft-ietf-jsonpath-base/) ("canonical" only, no filters, unions, recursive descenders, etc) or `plutil`-style "keypath" notation. An empty query will output the entire JSON document with the default of 2 spaces per level of indent. 9 | 10 | **Note:** If a `keypath` resembles a JSON Pointer or JSONPath/jq-style expression by **beginning** with the character dollar sign ($), period(.), left square bracket ([), or solidus (/) it will **not** be resolved. It will be treated as if it were JSONPath and JSON Pointer. Standards win. 11 | 12 | `[filepath]` can be any valid file path. Input via file redirection, here docs, here texts and Unix pipe (via cat) are all accepted. 13 | 14 | String values are output as regular text and are _not_ double quoted JSON. All other values are output as JSON (arrays, objects, booleans, numbers and null). If the query path is not found an error is sent to `/dev/stderr` and ljt will exit with a status of `1`. 15 | 16 | There are no other input or output options. 17 | 18 | See my blog for articles, examples, and musing on the ljt: https://www.brunerd.com/blog/category/projects/ljt/ 19 | 20 | ## Examples 21 | ``` 22 | #JSON Pointer example, strings are output as text not JSON 23 | % ljt /obj/0 <<< '{"obj":["string",42,true]}' 24 | string 25 | 26 | #keypath example: No leading characters before propery name, both array elements and property names are period delimited 27 | % ljt obj.0 <<< '{"obj":["string",42,true]}' 28 | string 29 | 30 | #keypath example if property name contains periods escape them with \ 31 | % ljt 'com\.keypaths\.are kinda\.wonky' <<< '{"com.keypaths.are kinda.wonky":true}' 32 | true 33 | 34 | #if you don't single quote you must escape your escape (leaning toothpick syndrome? :) 35 | % ljt "com\\.keypaths\\.are kinda\\.wonky" <<< '{"com.keypaths.are kinda.wonky":true}' 36 | true 37 | 38 | #use JSON Pointer to avoid escaping periods, if you have space you still have to quote 39 | % ljt '/com.json pointer.is.easier' <<< '{"com.json pointer.is.easier":true}' 40 | true 41 | 42 | #JSONPath example 43 | % ljt '$["obj"][2]' <<< '{"obj":["string",42,true]}' 44 | true 45 | 46 | #JSONPath without leading $ is also allowed 47 | % ljt '.obj[1]' <<< '{"obj":["string",42,true]}' 48 | 42 49 | 50 | #objects are output as JSON 51 | % ljt <<< '{"obj":["string",42,true]}' 52 | { 53 | "obj": [ 54 | "string", 55 | 42, 56 | true 57 | ] 58 | } 59 | 60 | #arrays are output as JSON 61 | % ljt /obj <<< '{"obj":["string",42,true]}' 62 | [ 63 | "string", 64 | 42, 65 | true 66 | ] 67 | 68 | #unlike jpt there are no output options, rely on standard tools to modify output 69 | % ljt /obj <<< '{"obj":["string",42,true]}' | sed -e 's/^ *//g' | tr -d $'\n' 70 | ["string",42,true] 71 | 72 | #example of piped input 73 | % ljt /obj <<< '{"obj":["string",42,true]}' | ljt '/0' 74 | string 75 | ``` 76 | 77 | ### Requirements: 78 | * macOS 10.11+ or \*nix with jsc installed 79 | 80 | ### Limitations: 81 | * Max JSON input size is 2GB 82 | * Max output is 720MB 83 | 84 | ### Installation 85 | A macOS pkg is available in [Releases](https://github.com/brunerd/ljt/releases) it will install both the commented `ljt` and the minified version `ljt.min` into `/usr/local/ljt` and create a symlink to `ljt` in `/usr/local/bin`. 86 | -------------------------------------------------------------------------------- /sources/ljt: -------------------------------------------------------------------------------- 1 | #!/bin/zsh 2 | #!/bin/bash 3 | #choose your shell with the first line, works in either 4 | 5 | : <<-LICENSE_BLOCK 6 | ljt - Little JSON Tool (https://github.com/brunerd/ljt) Copyright (c) 2022 Joel Bruner (https://github.com/brunerd) 7 | Licensed under the MIT License. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | LICENSE_BLOCK 9 | 10 | #BEGIN FUNCTION - copy the function below to embed ljt into your shell script, ljt.min a minified version is also available 11 | function ljt () ( #v1.0.10 ljt [query] [file] 12 | #xtrace will interfere with stderr output capture and exit non-zero (done because jsc lacks ability to exit non-zero) 13 | { set +x; } &> /dev/null 14 | 15 | #no special escaping needed when using read and heredoc to set a variable 16 | read -r -d '' JSCode <<-'EOT' 17 | try { 18 | //get arguments preserving Unicode 19 | var query = decodeURIComponent(escape(arguments[0])); 20 | var file = decodeURIComponent(escape(arguments[1])); 21 | 22 | //jq expression '.' (full document) converts to "" (skips invocation check) 23 | //if it were treated as a keypath it would be $[''][''], edge case and not so useful 24 | if(query==="."){query=""} 25 | //jq expression .[ converts to $[ 26 | else if(query[0]==="." && query[1]==="["){query="$"+query.slice(1)} 27 | 28 | //if JSON Pointer, convert to bracket style JS notation 29 | if (query[0]==='/' || query===""){ 30 | //no naked tildes ~ allowed 31 | if (/~[^0-1]/g.test(query+' ')){ throw new SyntaxError("JSON Pointer allows ~0 and ~1 only: " + query) } 32 | //properly split on /, reconstitute ~ and / chars, then normalize to JS bracket-style path 33 | query = query.split('/').slice(1).map(function (f){return "["+JSON.stringify(f.replace(/~1/g,"/").replace(/~0/g,"~"))+"]"}).join('') 34 | } 35 | //treat as JS/JSONPath style path 36 | else if(query[0] === '$' || (query[0] === '.' && query[1] !== '.') || query[0] === '[' ){ 37 | //reverse query, toss quoted strings, look for any disallowed characters: function invocation is prohibited! 38 | if(/[^A-Za-z_$\d\.\[\]'"]/.test(query.split('').reverse().join('').replace(/(["'])(.*?)\1(?!\\)/g, ""))){ 39 | throw new Error("Invalid path: " + query); 40 | } 41 | } 42 | //else perhaps it is plutil style 'keypath' (shudder) 43 | else { 44 | //replace escaped periods \. with invalid surrogate pair \uDEAD (no chance of collision) 45 | //split on periods, then surround stringified keys with [" "] and rejoin as a string 46 | query=query.replace(/\\\./g,"\uDEAD").split(".").map(function(f){return "["+JSON.stringify(f.replace(/\uDEAD/g,"."))+"]"}).join('') 47 | } 48 | 49 | //if JSONPath query string, lop off leading $ for eval later 50 | if(query[0]==="$"){ 51 | query=query.slice(1) 52 | }; 53 | 54 | //get data with readFile() and parse JSON 55 | var data = JSON.parse(readFile(file)); 56 | 57 | //use eval to get the result, catch errors quietly 58 | try {var result = eval("(data)"+query)}catch(e){} 59 | } 60 | //catch any errors, print error and quit 61 | catch(e){ 62 | printErr(e); 63 | quit(); 64 | } 65 | 66 | //return value or alert via stderr 67 | if (result !== undefined) { 68 | result !== null && result.constructor === String ? print(result) : print(JSON.stringify(result,null,2)) 69 | } 70 | else { 71 | printErr("Path not found.") 72 | } 73 | EOT 74 | 75 | #our two optional arguments 76 | queryArg="${1}"; fileArg="${2}"; 77 | #jsc changed location in 10.15, or perhaps this some other *nix 78 | jsc=$(find "/System/Library/Frameworks/JavaScriptCore.framework/Versions/Current/" -name 'jsc');[ -z "${jsc}" ] && jsc=$(which jsc); 79 | #allow query to be optional, if it is a file path 80 | if [ -f "${queryArg}" ] && [ -z "${fileArg}" ]; then fileArg="${queryArg}"; unset queryArg; fi 81 | #get input via file or file redirection, capture stderr to a variable 82 | if [ -f "${fileArg:=/dev/stdin}" ]; then { errOut=$( { { "${jsc}" -e "${JSCode}" -- "${queryArg}" "${fileArg}"; } 1>&3 ; } 2>&1); } 3>&1; 83 | #otherwise use cat for piped input 84 | else 85 | #if file descriptor is 0 we do have no input, offer help 86 | [ -t '0' ] && echo -e "ljt (v1.0.10) - Little JSON Tool (https://github.com/brunerd/ljt)\nUsage: ljt [query] [filepath]\n [query] is optional and can be JSON Pointer, canonical JSONPath (with or without leading $), or plutil-style keypath\n [filepath] is optional, input can also be via file redirection, piped input, here doc, or here strings" >/dev/stderr && exit 0 87 | #otherwise process piped input 88 | { errOut=$( { { "${jsc}" -e "${JSCode}" -- "${queryArg}" "/dev/stdin" <<< "$(cat)"; } 1>&3 ; } 2>&1); } 3>&1; 89 | fi 90 | #print any error output and exit non-zero 91 | if [ -n "${errOut}" ]; then /bin/echo "$errOut" >&2; return 1; fi 92 | ) 93 | #END FUNCTION 94 | 95 | # Usage: ljt [query] [filepath] 96 | # [query] is optional and can be JSON Pointer, canonical JSONPath (with or without leading $), or plutil-style keypath 97 | # [filepath] is optional, input can also be via file redirection, piped input, here doc, or here strings 98 | 99 | ljt "$@" 100 | exit $? 101 | -------------------------------------------------------------------------------- /sources/ljt.min: -------------------------------------------------------------------------------- 1 | #!/bin/zsh 2 | #!/bin/bash 3 | #choose your shell with the first line, works in either 4 | 5 | : <<-LICENSE_BLOCK 6 | ljt.min - Little JSON Tool (https://github.com/brunerd/ljt) Copyright (c) 2022 Joel Bruner (https://github.com/brunerd). Licensed under the MIT License. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 7 | LICENSE_BLOCK 8 | 9 | ## BEGIN FUNCTION ## - use the minified function below to embed ljt into your shell script 10 | function ljt () ( #v1.0.10 ljt [query] [file] 11 | { set +x; } &> /dev/null; read -r -d '' JSCode <<-'EOT' 12 | try{var query=decodeURIComponent(escape(arguments[0]));var file=decodeURIComponent(escape(arguments[1]));if(query===".")query="";else if(query[0]==="."&&query[1]==="[")query="$"+query.slice(1);if(query[0]==="/"||query===""){if(/~[^0-1]/g.test(query+" "))throw new SyntaxError("JSON Pointer allows ~0 and ~1 only: "+query);query=query.split("/").slice(1).map(function(f){return"["+JSON.stringify(f.replace(/~1/g,"/").replace(/~0/g,"~"))+"]"}).join("")}else if(query[0]==="$"||query[0]==="."&&query[1]!=="."||query[0]==="["){if(/[^A-Za-z_$\d\.\[\]'"]/.test(query.split("").reverse().join("").replace(/(["'])(.*?)\1(?!\\)/g,"")))throw new Error("Invalid path: "+query);}else query=query.replace(/\\\./g,"\uDEAD").split(".").map(function(f){return "["+JSON.stringify(f.replace(/\uDEAD/g,"."))+"]"}).join('');if(query[0]==="$")query=query.slice(1);var data=JSON.parse(readFile(file));try{var result=eval("(data)"+query)}catch(e){}}catch(e){printErr(e);quit()}if(result!==undefined)result!==null&&result.constructor===String?print(result):print(JSON.stringify(result,null,2));else printErr("Path not found.") 13 | EOT 14 | queryArg="${1}"; fileArg="${2}";jsc=$(find "/System/Library/Frameworks/JavaScriptCore.framework/Versions/Current/" -name 'jsc');[ -z "${jsc}" ] && jsc=$(which jsc);if [ -f "${queryArg}" ] && [ -z "${fileArg}" ]; then fileArg="${queryArg}"; unset queryArg; fi;if [ -f "${fileArg:=/dev/stdin}" ]; then { errOut=$( { { "${jsc}" -e "${JSCode}" -- "${queryArg}" "${fileArg}"; } 1>&3 ; } 2>&1); } 3>&1;else [ -t '0' ] && echo -e "ljt (v1.0.10) - Little JSON Tool (https://github.com/brunerd/ljt)\nUsage: ljt [query] [filepath]\n [query] is optional and can be JSON Pointer, canonical JSONPath (with or without leading $), or plutil-style keypath\n [filepath] is optional, input can also be via file redirection, piped input, here doc, or here strings" >/dev/stderr && exit 0; { errOut=$( { { "${jsc}" -e "${JSCode}" -- "${queryArg}" "/dev/stdin" <<< "$(cat)"; } 1>&3 ; } 2>&1); } 3>&1; fi;if [ -n "${errOut}" ]; then /bin/echo "$errOut" >&2; return 1; fi 15 | ) 16 | ## END FUNCTION ## 17 | 18 | # Usage: ljt [query] [filepath] 19 | # [query] is optional and can be JSON Pointer, canonical JSONPath (with or without leading $), or plutil-style keypath 20 | # [filepath] is optional, input can also be via file redirection, piped input, here doc, or here strings 21 | 22 | ljt "$@" 23 | exit $? 24 | --------------------------------------------------------------------------------