json dictionary to bash hash table using readarray

Viewed 217

First a working example with arrays

json_array() {
    local -n array="${1}"
    readarray -d $'\0' -t array < <(
        # Create nul delimited array entry using jq
        jq -cjn --argjson arr "$array" '$arr|map(tostring)|.[]+"\u0000"'
    )
}

> unset arr; arr='["a", "b", "c"]'; json_array arr; echo "${arr[0]} ${arr[1]} ${arr[2]}"
a b c

Now I'm trying to do something similar with dict, convert a json dict into a bash associative array

json_dict() {
    local -n dict="${1}"
    declare -A hash_table

    append_to_hash_table() {
        shift
        { read -r key;  read -r value; } <<<"$1"
        hash_table+=([$key]="$value")
    }

    readarray -d $'\0' -c 1 -C append_to_hash_table < <(
        # Create nul delimited dict entry using jq
        jq -cjn --argjson d "$dict" '$d|to_entries|map("\(.key)\n\(.value|tostring|@sh)")|.[]+"\u0000"'
    )

    # Here hash_table contain the correct output
    dict=""
    dict="$hash_table"
}

> unset arr; arr='{"a": "aa", "l": "bb", "c": "ccccc"}'; json_dict arr; echo "${arr[@]}"
Nothing

It seems dict="$hash_table" doesn't correctly update the refname, How can I make bash dict refname point to hash_table?

2 Answers

There's no need for readarray here: You can have two separate NUL-delimited reads as part of your while loop.

See the below answer demonstrated at https://replit.com/@CharlesDuffy2/GrandioseDraftyArguments#main.sh

while IFS= read -r -d '' key && IFS= read -r -d '' value; do
  hash_table[$key]=$value
done < <(jq -cjn --argjson d "$arr" \
           '$d | to_entries[] | ( .key, "\u0000", .value, "\u0000")')

Putting this into context:

json_dict() {
  declare key value in_value="${!1}"
  unset "$1"                   # FIXME: Better to take a $2 for output variable
  declare -g -A "$1"
  declare -n hash_table="$1"
  while IFS= read -r -d '' key && IFS= read -r -d '' value; do
    hash_table[$key]=$value
  done < <(
    jq -cjn --argjson d "$in_value" \
      '$d | to_entries[] | ( .key, "\u0000", .value, "\u0000")'
  )
}
 
arr='{"a": "aa", "l": "bb", "c": "ccccc"}'
json_dict arr
declare -p arr

...emits as output:

declare -A arr=([a]="aa" [c]="ccccc" [l]="bb" )

That said, to answer the question exactly as-asked, thus using readarray:

json_dict() {
  declare -a pieces=()

  readarray -d '' pieces < <(
    jq -cjn --argjson d "${!1}" \
      '$d | to_entries[] | ( .key, "\u0000", .value, "\u0000")'
  )

  unset "$1"
  declare -g -A "$1"
  declare -n hash_table="$1"
  set -- "${pieces[@]}"

  while (( $# )); do
    hash_table[$1]=$2
    { shift && shift; } || return
  done
}

arr='{"a": "aa", "l": "bb", "c": "ccccc"}'
json_dict arr
declare -p arr

Wouldn't it be simpler to just use declare and have jq create the contents using @sh for shell conformity?

Indexed array:

unset arr; arr='["a", "b", "c"]'
declare -a arr="($(jq -r @sh <<< "$arr"))"

Associative array:

unset arr; arr='{"a": "aa", "l": "bb", "c": "ccccc"}'
declare -A arr="($(jq -r 'to_entries[] | @sh "[\(.key)]=\(.value)"' <<< "$arr"))"

Reacting to an edit request: The above requires the values to be strings (numbers and booleans will kind of work, too), but other structures need to be brought into a stringy format first. In particular, following the request, @json could be used to encode an arbitrary JSON content as a string. However, keep in mind that in doing so, the bash array's items will be JSON-encoded, which means that also the simple cases (strings like aa from above) will be encoded (e.g. as "aa", including the quotes, as required by JSON). If this is what you want, go for it:

Indexed array with JSON-encoded values:

unset arr; arr='["a", {}, [null, {"a": true}]]'
declare -a arr="($(jq -r 'map(@json) | @sh' <<< "$arr"))"

Associative array with JSON-encoded values:

unset arr; arr='{"a": "aa", "b": {"l": "bb", "c": "ccccc"}}'
declare -A arr="($(jq -r 'to_entries[] | @sh "[\(.key)]=\(.value | @json)"' <<< "$arr"))"
Related