When the JSON string to be parsed is meant to be piped on and used just on the command line, then js and jsawk make good tools however, parsing JSON from within a script can be cumbersome when the script is to be tranferred to other machines that may not have js or jsawk installed.
Some solutions do exist:
Some of the syntax and some semantics change from bash to dash but not much such that the second script by Florian Kalis (the author of bash-json-parser) can just be adjusted in order to be able to run under dash. A good resource is perhaps GreyCat's bashism page that extensively documents the extensions to the command shell that are particular to Bash.
Without further ado, the following script should be compatible with dash and calling the json
function will output via echo key and value pairs to the standard output.
#!/usr/bin/env sh ########################################################################### ## Original @ https://github.com/fkalis/bash-json-parser ## ## Modifications by Wizardry and Steamworks: ## ## * dash compatibility ## ########################################################################### ## conversion from bash with the following changes: ## ## * parse "" "" <<< "${INPUT}" -> echo "${INPUT}" | parse "" "" ## ## * == -> = ## ## * read -r -s -n 1 c -> readc c ## ########################################################################### json_readc() { if [ -t 0 ]; then saved_tty_settings=$(stty -g); stty -icanon -echo min 1 time 0; fi; c=$(dd bs=1 count=1 conv=noerror,sync 2>/dev/null); if [ -t 0 ]; then stty "$saved_tty_settings"; fi; }; json_set_entry() { echo "$1=$2"; }; json_parse_array() { local current_path="${1:+$1.}$2"; local current_scope="root"; local current_index=0; while [ "$chars_read" -lt "$INPUT_LENGTH" ]; do [ "$preserve_current_char" = "0" ] && chars_read=$((chars_read + 1)) && json_readc c; preserve_current_char=0; c=${c:-' '}; case "$current_scope" in "root") case "$c" in '{') json_parse_object "$current_path" "$current_index"; current_scope="entry_separator"; ;; ']') return; ;; [\"tfTF\-0-9]) preserve_current_char=1; json_parse_value "$current_path" "$current_index"; preserve_current_char=1; current_scope="entry_separator"; ;; esac ;; "entry_separator") [ "$c" = "," ] && current_index=$((current_index + 1)) && current_scope="root"; [ "$c" = "]" ] && return; ;; esac done }; json_parse_value() { local current_path="${1:+$1.}$2"; local current_scope="root"; while [ "$chars_read" -lt "$INPUT_LENGTH" ]; do [ "$preserve_current_char" = "0" ] && chars_read=$((chars_read + 1)) && json_readc c; preserve_current_char=0; c=${c:-' '}; case "$current_scope" in "root") case "$c" in '"') current_scope="string"; current_varvalue=""; ;; [\-0-9]) current_scope="number"; current_varvalue="$c"; ;; [tfTF]) current_scope="boolean"; current_varvalue="$c"; ;; "[") json_parse_array "" "$current_path"; return; ;; "{") json_parse_object "" "$current_path"; return; ;; esac ;; "string") case "$c" in '"') [ "$current_escaping" = "0" ] && json_set_entry "$current_path" "$current_varvalue" && return; [ "$current_escaping" = "1" ] && current_varvalue="$current_varvalue$c" && current_escaping=0; ;; '\') [ "$current_escaping" = "1" ] && current_varvalue="$current_varvalue$c"; current_escaping=$((1 - current_escaping)); ;; *) current_escaping=0; current_varvalue="$current_varvalue$c"; ;; esac ;; "number") case "$c" in [,\]}]) json_set_entry "$current_path" "$current_varvalue"; preserve_current_char=1; return ;; [\-0-9.]) current_varvalue="$current_varvalue$c"; ;; esac ;; "boolean") case "$c" in [,\]}]) json_set_entry "$current_path" "$current_varvalue"; preserve_current_char=1; return; ;; [a-zA-Z]) current_varvalue="$current_varvalue$c"; ;; esac ;; esac done }; json_parse_object() { local current_path="${1:+$1.}$2"; local current_scope="root"; while [ "$chars_read" -lt "$INPUT_LENGTH" ]; do [ "$preserve_current_char" = "0" ] && chars_read=$((chars_read + 1)) && json_readc c; preserve_current_char=0; c=${c:-' '}; case "$current_scope" in "root") [ "$c" = "}" ] && return; [ "$c" = "\"" ] && current_scope="varname" && current_varname="" && current_escaping=0; ;; "varname") case "$c" in '"') [ "$current_escaping" = "0" ] && current_scope="key_value_separator"; [ "$current_escaping" = "1" ] && current_varname="$current_varname$c" && current_escaping=0; ;; '\') current_escaping=$((1 - current_escaping)); current_varname="$current_varname$c"; ;; *) current_escaping=0; current_varname="$current_varname$c"; ;; esac ;; "key_value_separator") [ "$c" = ":" ] && json_parse_value "$current_path" "$current_varname" && current_scope="field_separator"; ;; "field_separator") [ "$c" = ',' ] && current_scope="root"; [ "$c" = '}' ] && return; ;; esac done }; json_read() { chars_read=0; preserve_current_char=0; while [ "$chars_read" -lt "$INPUT_LENGTH" ]; do json_readc c; c=${c:-' '}; chars_read=$((chars_read + 1)); [ "$c" = "{" ] && json_parse_object "" "" && return; [ "$c" = "[" ] && json_parse_array "" "" && return; done }; json() { if [ -z "$@" ]; then INPUT=$(cat -); else INPUT=$(echo "$@"); fi; INPUT_LENGTH="${#INPUT}"; echo "${INPUT}" | json_read; }; json "$@"
The function json_readc
is used due to limiting POSIX capabilities that make it very difficult to read one and only one character from standard input. Even though it seems that json_readc
could be simplified with head -c 1
, the -c
parameter is not defined in POSIX but rather a GNU addition.
More ample testing was performed, but re-using the exact same example as on the bash-json-parser page, the same parameters yield the exact same result:
# ./json_bash_mini.sh '{ "a": "b", "c": [ "hallo", "welt" ] }' a=b c.0=hallo c.1=welt
with the main difference that json_bash_mini.sh
will also work under ash
, dash
and zsh
.
Since carrying around a large library of scripts is not always feasible, the functions can be compacted (or minified) to a minimum and a very ugly one-liner.
r() { if [ -t 0 ]; then b=$(stty -g); stty -icanon -echo min 1 time 0; fi; c=$(dd bs=1 count=1 conv=noerror,sync 2>/dev/null); if [ -t 0 ]; then stty "$b"; fi; }; e() { echo "$1=$2"; }; a() { local d="${1:+$1.}$2"; local f="u"; local g=0; while [ "$h" -lt "$j" ]; do [ "$i" = "0" ] && h=$((h + 1)) && r c; i=0; c=${c:-' '}; case "$f" in "u") case "$c" in '{') o "$d" "$g"; f="l"; ;; ']') return; ;; [\"tfTF\-0-9]) i=1; v "$d" "$g"; i=1; f="l"; ;; esac ;; "l") [ "$c" = "," ] && g=$((g + 1)) && f="u"; [ "$c" = "]" ] && return; ;; esac done }; v() { local d="${1:+$1.}$2"; local f="u"; while [ "$h" -lt "$j" ]; do [ "$i" = "0" ] && h=$((h + 1)) && r c; i=0; c=${c:-' '}; case "$f" in "u") case "$c" in '"') f="z"; k=""; ;; [\-0-9]) f="y"; k="$c"; ;; [tfTF]) f="x"; k="$c"; ;; "[") a "" "$d"; return; ;; "{") o "" "$d"; return; ;; esac ;; "z") case "$c" in '"') [ "$m" = "0" ] && e "$d" "$k" && return; [ "$m" = "1" ] && k="$k$c" && m=0; ;; '\') [ "$m" = "1" ] && k="$k$c"; m=$((1 - m)); ;; *) m=0; k="$k$c"; ;; esac ;; "y") case "$c" in [,\]}]) e "$d" "$k"; i=1; return ;; [\-0-9.]) k="$k$c"; ;; esac ;; "x") case "$c" in [,\]}]) e "$d" "$k"; i=1; return; ;; [a-zA-Z]) k="$k$c"; ;; esac ;; esac done }; o() { local d="${1:+$1.}$2"; local f="u"; while [ "$h" -lt "$j" ]; do [ "$i" = "0" ] && h=$((h + 1)) && r c; i=0; c=${c:-' '}; case "$f" in "u") [ "$c" = "}" ] && return; [ "$c" = "\"" ] && f="w" && n="" && m=0; ;; "w") case "$c" in '"') [ "$m" = "0" ] && f="p"; [ "$m" = "1" ] && n="$n$c" && m=0; ;; '\') m=$((1 - m)); n="$n$c"; ;; *) m=0; n="$n$c"; ;; esac ;; "p") [ "$c" = ":" ] && v "$d" "$n" && f="q"; ;; "q") [ "$c" = ',' ] && f="u"; [ "$c" = '}' ] && return; ;; esac done }; s() { h=0; i=0; while [ "$h" -lt "$j" ]; do r c; c=${c:-' '}; h=$((h + 1)); [ "$c" = "{" ] && o "" "" && return; [ "$c" = "[" ] && a "" "" && return; done }; json() { if [ -z "$@" ]; then t=$(cat -); else t=$(echo "$@"); fi; j="${#t}"; echo "${t}" | s; }
Note that the only user-defined string in the one-liner is json
which represents the method through which a JSON string will be decoded to key-value tuples.
Since that is not near enough and if including the script guerilla style is preferred, then the one-liner can be base64 encoded, included in the script that requires a json parser and then at runtime, unpacked and loaded dynamically.
For instance, the following commands:
JSON_BASH_PARSER=$(cat <<EOF cigpIHsgaWYgWyAtdCAwIF07IHRoZW4gYj0kKHN0dHkgLWcpOyBzdHR5IC1pY2Fub24gLWVjaG8g bWluIDEgdGltZSAwOyBmaTsgYz0kKGRkIGJzPTEgY291bnQ9MSBjb252PW5vZXJyb3Isc3luYyAy Pi9kZXYvbnVsbCk7IGlmIFsgLXQgMCBdOyB0aGVuIHN0dHkgIiRiIjsgZmk7IH07IGUoKSB7IGVj aG8gIiQxPSQyIjsgfTsgYSgpIHsgbG9jYWwgZD0iJHsxOiskMS59JDIiOyBsb2NhbCBmPSJ1Ijsg bG9jYWwgZz0wOyB3aGlsZSBbICIkaCIgLWx0ICIkaiIgXTsgZG8gWyAiJGkiID0gIjAiIF0gJiYg aD0kKChoICsgMSkpICYmIHIgYzsgaT0wOyBjPSR7YzotJyAnfTsgY2FzZSAiJGYiIGluICJ1Iikg Y2FzZSAiJGMiIGluICd7JykgbyAiJGQiICIkZyI7IGY9ImwiOyA7OyAnXScpIHJldHVybjsgOzsg W1widGZURlwtMC05XSkgaT0xOyB2ICIkZCIgIiRnIjsgaT0xOyBmPSJsIjsgOzsgZXNhYyA7OyAi bCIpIFsgIiRjIiA9ICIsIiBdICYmIGc9JCgoZyArIDEpKSAmJiBmPSJ1IjsgWyAiJGMiID0gIl0i IF0gJiYgcmV0dXJuOyA7OyBlc2FjIGRvbmUgfTsgdigpIHsgbG9jYWwgZD0iJHsxOiskMS59JDIi OyBsb2NhbCBmPSJ1Ijsgd2hpbGUgWyAiJGgiIC1sdCAiJGoiIF07IGRvIFsgIiRpIiA9ICIwIiBd ICYmIGg9JCgoaCArIDEpKSAmJiByIGM7IGk9MDsgYz0ke2M6LScgJ307IGNhc2UgIiRmIiBpbiAi dSIpIGNhc2UgIiRjIiBpbiAnIicpIGY9InoiOyBrPSIiOyA7OyBbXC0wLTldKSBmPSJ5Ijsgaz0i JGMiOyA7OyBbdGZURl0pIGY9IngiOyBrPSIkYyI7IDs7ICJbIikgYSAiIiAiJGQiOyByZXR1cm47 IDs7ICJ7IikgbyAiIiAiJGQiOyByZXR1cm47IDs7IGVzYWMgOzsgInoiKSBjYXNlICIkYyIgaW4g JyInKSBbICIkbSIgPSAiMCIgXSAmJiBlICIkZCIgIiRrIiAmJiByZXR1cm47IFsgIiRtIiA9ICIx IiBdICYmIGs9IiRrJGMiICYmIG09MDsgOzsgJ1wnKSBbICIkbSIgPSAiMSIgXSAmJiBrPSIkayRj IjsgbT0kKCgxIC0gbSkpOyA7OyAqKSBtPTA7IGs9IiRrJGMiOyA7OyBlc2FjIDs7ICJ5IikgY2Fz ZSAiJGMiIGluIFssXF19XSkgZSAiJGQiICIkayI7IGk9MTsgcmV0dXJuIDs7IFtcLTAtOS5dKSBr PSIkayRjIjsgOzsgZXNhYyA7OyAieCIpIGNhc2UgIiRjIiBpbiBbLFxdfV0pIGUgIiRkIiAiJGsi OyBpPTE7IHJldHVybjsgOzsgW2EtekEtWl0pIGs9IiRrJGMiOyA7OyBlc2FjIDs7IGVzYWMgZG9u ZSB9OyBvKCkgeyBsb2NhbCBkPSIkezE6KyQxLn0kMiI7IGxvY2FsIGY9InUiOyB3aGlsZSBbICIk aCIgLWx0ICIkaiIgXTsgZG8gWyAiJGkiID0gIjAiIF0gJiYgaD0kKChoICsgMSkpICYmIHIgYzsg aT0wOyBjPSR7YzotJyAnfTsgY2FzZSAiJGYiIGluICJ1IikgWyAiJGMiID0gIn0iIF0gJiYgcmV0 dXJuOyBbICIkYyIgPSAiXCIiIF0gJiYgZj0idyIgJiYgbj0iIiAmJiBtPTA7IDs7ICJ3IikgY2Fz ZSAiJGMiIGluICciJykgWyAiJG0iID0gIjAiIF0gJiYgZj0icCI7IFsgIiRtIiA9ICIxIiBdICYm IG49IiRuJGMiICYmIG09MDsgOzsgJ1wnKSBtPSQoKDEgLSBtKSk7IG49IiRuJGMiOyA7OyAqKSBt PTA7IG49IiRuJGMiOyA7OyBlc2FjIDs7ICJwIikgWyAiJGMiID0gIjoiIF0gJiYgdiAiJGQiICIk biIgJiYgZj0icSI7IDs7ICJxIikgWyAiJGMiID0gJywnIF0gJiYgZj0idSI7IFsgIiRjIiA9ICd9 JyBdICYmIHJldHVybjsgOzsgZXNhYyBkb25lIH07IHMoKSB7IGg9MDsgaT0wOyB3aGlsZSBbICIk aCIgLWx0ICIkaiIgXTsgZG8gciBjOyBjPSR7YzotJyAnfTsgaD0kKChoICsgMSkpOyBbICIkYyIg PSAieyIgXSAmJiBvICIiICIiICYmIHJldHVybjsgWyAiJGMiID0gIlsiIF0gJiYgYSAiIiAiIiAm JiByZXR1cm47IGRvbmUgfTsganNvbigpIHsgaWYgWyAteiAiJEAiIF07IHRoZW4gdD0kKGNhdCAt KTsgZWxzZSB0PSQoZWNobyAiJEAiKTsgZmk7IGo9IiR7I3R9IjsgZWNobyAiJHt0fSIgfCBzOyB9 Cgo= EOF ) eval `printf '%s\n' "$JSON_BASH_PARSER" | base64 -d`
will unpack the JSON parser by base64 decoding the string and then load the defined functions using eval
. After the previous snippet, an instruction such as:
echo '{ "a": "b", "c": [ "hallo", "welt" ] }' | json
will print out the expected output from the original script.
stats.ripe.net
.