During everyday shell sessions, I very often find myself needing to assign data from a JSON document (extracted via some jq filter) to a Zsh shell parameter: JSON scalars to Zsh scalars, JSON arrays to Zsh arrays, and JSON objects to Zsh associative arrays. The problem—and what previously asked questions don’t seem to tackle—is that the data often contains newlines (and even NUL bytes), making this a rather nontrivial task.
Here is what I have come up with so far:
function assign-from-json {
local -A opts && zparseopts -A opts -D -F -M -- a A && typeset -r opts
if [[ $# -ne 3 || ( -v opts[-a] && -v opts[-A] ) ]] ; then
>&2 printf 'Usage: %s [-a|-A] NAME FILTER JSON\n' $0
return 2
fi
if [[ -v opts[-a] ]] ; then
local -a lengths && { lengths=( "${(@f)$( jq -r "$2 | .[] | tostring | length" <<< $3 )}" ) || return $? } && typeset -r lengths
local data && { data="$( jq -j "$2 | .[] | tostring" <<< $3 )" || return $? } && typeset -r data
local elem
local -a elems
for length in "${lengths[@]}" ; do
read -u 0 -k $length elem
elems+=$elem
done <<< $data
eval "${(q)1}"='( "${elems[@]}" )'
elif [[ -v opts[-A] ]] ; then
local transformed_json && { transformed_json="$( jq "$2 | to_entries | map(.key, .value)" <<< $3 )" || return $? } && typeset -r transformed_json
assign-from-json -a $1 "." $transformed_json
else
eval "${(q)1}"="${(q)$( jq -r $2 <<< $3 )}"
fi
}
In most cases it works quite well:
% json='
{
"scalar": "Hello, world",
"array": [1, 2, 3],
"scary_scalar": "\nNewlines\u0000NUL bytes\ttabs",
"scary_array": [
"A\nvery\u0000scary\nvalue",
"A less scary value",
"eh"
]
}
'
% assign-from-json scalar '.scalar' $json && printf '%q\n' $scalar
Hello,\ world
% typeset -a array && assign-from-json -a array '.array' $json && printf '%q\n' "${array[@]}"
1
2
3
% assign-from-json scary_scalar '.scary_scalar' $json && printf '%q\n' $scary_scalar
$'\n'Newlines$'\0'NUL\ bytes$'\t'tabs
% typeset -a scary_array && assign-from-json -a scary_array '.scary_array' $json && printf '%q\n' "${scary_array[@]}"
A$'\n'very$'\0'scary$'\n'value
A\ less\ scary\ value
eh
% typeset -A assoc && assign-from-json -A assoc '.' $json && printf '%q -> %q\n' "${(@kv)assoc}"
array -> \[1,2,3\]
scary_array -> \[\"A\\nvery\\u0000scary\\nvalue\",\"A\ less\ scary\ value\",\"eh\"\]
scary_scalar -> $'\n'Newlines$'\0'NUL\ bytes$'\t'tabs
scalar -> Hello,\ world
Unfortunately it seems to struggle with trailing newlines:
% assign-from-json bad_scalar '.' '"foo\n"' && printf '%q\n$ $bad_scalar
foo
# expected: foo$'\n'
- I assume the problem with trailing newlines is due to command substitution removing them. Do you see an easy way to fix it?
- One can do
assign-from-json -A assoc ...even ifassocis not typeset as an associative array. How can I prevent that from being possible? - Do you see any other problems with the code?
$j->{scary array}->[1]isA less scary value. I'll post a minimal example below.