Search code examples
jsonbashjqassociative-array

create json from bash variable and associative array


Lets say I have the following declared in bash:

mcD="had_a_farm"
eei="eeieeio"
declare -A animals=( ["duck"]="quack_quack" ["cow"]="moo_moo" ["pig"]="oink_oink" )

and I want the following json:

{
  "oldMcD": "had a farm",
  "eei": "eeieeio",
  "onThisFarm":[
    {
      "duck": "quack_quack",
      "cow": "moo_moo",
      "pig": "oink_oink"
    }
  ]
}

Now I know I could do this with an echo, printf, or assign text to a variable, but lets assume animals is actually very large and it would be onerous to do so. I could also loop through my variables and associative array and create a variable as I'm doing so. I could write either of these solutions, but both seem like the "wrong way". Not to mention its obnoxious to deal with the last item in animals, after which I do not want a ",".

I'm thinking the right solution uses jq, but I'm having a hard time finding much documentation and examples on how to use this tool to write jsons (especially those that are nested) rather than parse them.

Here is what I came up with:

jq -n --arg mcD "$mcD" --arg eei "$eei" --arg duck "${animals['duck']}" --arg cow "${animals['cow']}" --arg pig "${animals['pig']}" '{onThisFarm:[ { pig: $pig, cow: $cow, duck: $duck } ], eei: $eei, oldMcD: $mcD }'

Produces the desired result. In reality, I don't really care about the order of the keys in the json, but it's still annoying that the input for jq has to go backwards to get it in the desired order. Regardless, this solution is clunky and was not any easier to write than simply declaring a string variable that looks like a json (and would be impossible with larger associative arrays). How can I build a json like this in an efficient, logical manner?

Thanks!


Solution

  • Assuming that none of the keys or values in the "animals" array contains newline characters:

    for i in "${!animals[@]}"
    do
      printf "%s\n%s\n"  "${i}" "${animals[$i]}"
    done | jq -nR --arg oldMcD "$mcD" --arg eei "$eei" '
      def to_o:
        . as $in
        | reduce range(0;length;2) as $i ({}; 
            .[$in[$i]]= $in[$i+1]);
    
      {$oldMcD, 
       $eei,
       onthisfarm: [inputs] | to_o}
    '
    

    Notice the trick whereby {$x} in effect expands to {(x): $x}

    Using "\u0000" as the separator

    If any of the keys or values contains a newline character, you could tweak the above so that "\u0000" is used as the separator:

    for i in "${!animals[@]}"
    do
        printf "%s\0%s\0"  "${i}"  "${animals[$i]}"
    done | jq -sR --arg oldMcD "$mcD" --arg eei "$eei" '
     def to_o:
       . as $in
       | reduce range(0;length;2) as $i ({};
           .[$in[$i]]= $in[$i+1]);
    
      {$oldMcD, 
       $eei,
       onthisfarm: split("\u0000") | to_o }
    '
    
    

    Note: The above assumes jq version 1.5 or later.