11

I would like to convert an associative array in bash to a JSON hash/dict. I would prefer to use JQ to do this as it is already a dependency and I can rely on it to produce well formed json. Could someone demonstrate how to achieve this?

#!/bin/bash

declare -A dict=()

dict["foo"]=1
dict["bar"]=2
dict["baz"]=3

for i in "${!dict[@]}"
do
    echo "key  : $i"
    echo "value: ${dict[$i]}"
done

echo 'desired output using jq: { "foo": 1, "bar": 2, "baz": 3 }'
oguz ismail
  • 1
  • 16
  • 47
  • 69
htaccess
  • 2,800
  • 26
  • 31

5 Answers5

15

There are many possibilities, but given that you already have written a bash for loop, you might like to begin with this variation of your script:

#!/bin/bash
# Requires bash with associative arrays
declare -A dict

dict["foo"]=1
dict["bar"]=2
dict["baz"]=3

for i in "${!dict[@]}"
do
    echo "$i" 
    echo "${dict[$i]}"
done |
jq -n -R 'reduce inputs as $i ({}; . + { ($i): (input|(tonumber? // .)) })'

The result reflects the ordering of keys produced by the bash for loop:

{
  "bar": 2,
  "baz": 3,
  "foo": 1
}

In general, the approach based on feeding jq the key-value pairs, with one key on a line followed by the corresponding value on the next line, has much to recommend it. A generic solution following this general scheme, but using NUL as the "line-end" character, is given below.

Keys and Values as JSON Entities

To make the above more generic, it would be better to present the keys and values as JSON entities. In the present case, we could write:

for i in "${!dict[@]}"
do
    echo "\"$i\""
    echo "${dict[$i]}"
done | 
jq -n 'reduce inputs as $i ({}; . + { ($i): input })'

Other Variations

JSON keys must be JSON strings, so it may take some work to ensure that the desired mapping from bash keys to JSON keys is implemented. Similar remarks apply to the mapping from bash array values to JSON values. One way to handle arbitrary bash keys would be to let jq do the conversion:

printf "%s" "$i" | jq -Rs .

You could of course do the same thing with the bash array values, and let jq check whether the value can be converted to a number or to some other JSON type as desired (e.g. using fromjson? // .).

A Generic Solution

Here is a generic solution along the lines mentioned in the jq FAQ and advocated by @CharlesDuffy. It uses NUL as the delimiter when passing the bash keys and values to jq, and has the advantage of only requiring one call to jq. If desired, the filter fromjson? // . can be omitted or replaced by another one.

declare -A dict=( [$'foo\naha']=$'a\nb' [bar]=2 [baz]=$'{"x":0}' )

for key in "${!dict[@]}"; do
    printf '%s\0%s\0' "$key" "${dict[$key]}"
done |
jq -Rs '
  split("\u0000")
  | . as $a
  | reduce range(0; length/2) as $i 
      ({}; . + {($a[2*$i]): ($a[2*$i + 1]|fromjson? // .)})'

Output:

{
  "foo\naha": "a\nb",
  "bar": 2,
  "baz": {
    "x": 0
  }
}
peak
  • 105,803
  • 17
  • 152
  • 177
  • Ok, I like that this only invokes jq once with newline separated input, seems like more generically useful approach. I'm a little confused how --null-input and --raw-input interact, reading the docs on reduce now. I think this needs to be the accepted answer. – htaccess Jun 28 '17 at 02:53
  • For the second solution, where it is written "for the present case", it works only when values are integers. Just in case someone does like me and paste and cut and try to use it for another case. – Dominic108 Dec 28 '19 at 09:30
5

This answer is from nico103 on freenode #jq:

#!/bin/bash

declare -A dict=()

dict["foo"]=1
dict["bar"]=2
dict["baz"]=3

assoc2json() {
    declare -n v=$1
    printf '%s\0' "${!v[@]}" "${v[@]}" |
    jq -Rs 'split("\u0000") | . as $v | (length / 2) as $n | reduce range($n) as $idx ({}; .[$v[$idx]]=$v[$idx+$n])'
}

assoc2json dict
htaccess
  • 2,800
  • 26
  • 31
  • 1
    Granted, this is using a bunch of bashisms unavoidably, but I'd suggest POSIX-compliant function declaration syntax just to avoid spreading shell-local idioms. `assoc2json() {`, with no `function` keyword, avoids depending on ksh syntax supported by bash only for compatibility purposes. – Charles Duffy Jun 28 '17 at 04:07
  • Hmm. One thing that worries me here a little, by the way, is that `%q` quotes strings for `eval`-style consumption **by bash**. I'm quite certain that there's a substantial number of inputs for which the processing done here is going to yield something that doesn't evaluate back to the original literal values. On the other hand, it'd be pretty straightforward to adopt the `printf '%s\0'` approach taken in my answer, combining it with the much shorter jq code here (just adopting the string-splitting bits)... – Charles Duffy Jun 28 '17 at 04:15
  • If you see the answer I (nico103) wrote up below, I meant to leave the dequoting/unescaping to the reader. It should be easy enough (though maybe not a one-liner). – user2259432 Jun 28 '17 at 20:43
  • Interesting about `printf '%s\0'`! Thanks for that idea! jq will turn the NULs into `\u0000`, so you'd have to split on that, which... you can! So the one-liner is now simpler: `printf '%s\0' "${!arrayvar[@]}" "${arrayvar[@]}" | jq -Rs 'split("\u0000") | . as $v | (length / 2) as $n | reduce range($n) as $idx ({}; .[$v[$idx]]=$v[$idx+$n])'`. – user2259432 Jun 28 '17 at 20:46
  • Changed to POSIX-compliant function declaration syntax as per Charles Duffys suggestion above. – htaccess Jun 30 '17 at 22:16
  • @CharlesDuffy, what do you mean by 'spreading shell-local idioms'? – Ján Lalinský May 05 '18 at 09:32
  • @CharlesDuffy, why do you think the function keyword is supported only for compatibility purposes? The keyword makes the code more readable and lots of people use it in bash. – Ján Lalinský May 05 '18 at 09:33
  • @JánLalinský, see http://wiki.bash-hackers.org/scripting/obsolete. The `function` keyword was literally added only because pre-POSIX ksh had it -- but in ksh, it serves an actual useful purpose, making variables function-local by default. In bash it doesn't even do that, thus making functions defined in this way incompatible between bash and the ksh releases the syntax was taken from. Moreover, the ksh form is `function foo {`, whereas `function foo() {` is a hybrid of the two (POSIX and ksh-compatibility) forms, not itself compatible with *either* POSIX or ksh. – Charles Duffy May 06 '18 at 17:40
  • @CharlesDuffy, it seems whoever introduced the keyword function into bash, didn't want to introduce also its original ksh meaning, so perhaps he did not care about 100% compatibility with ksh that much and perhaps there was another reason to introduce it. Anyways, many years later I think it does not matter what was the original intent, the keyword is there and is useful in some bash scripts: it makes the code more readable. – Ján Lalinský May 07 '18 at 00:37
  • @JánLalinský, if someone can only read code that works with bash, and can't read code written for `/bin/sh`, that's rather a significant fault in their skillset, no? Likewise, if we're promulgating habits that cause people to *write* code incompatible with `/bin/sh` without making it clear that that code is bash-only and a different form should be used when cross-shell compatibility is desired, I consider that a clear fault in our teaching. – Charles Duffy May 07 '18 at 10:34
  • @CharlesDuffy, that was not what I meant. I meant that using the function keyword improves readability. – Ján Lalinský May 07 '18 at 10:37
  • @JánLalinský, ...I also can lean on community consensus here -- the bash-hackers' wiki -- previously linked -- and the [Wooledge BashGuide](https://mywiki.wooledge.org/BashGuide/CompoundCommands#Functions) are both actively-maintained resources managed by community members who care a great deal about best practices. Similarly, see the [history for the `function` keyword in the freenode #bash channel](http://wooledge.org/~greybot/meta/function). TLDP's "ABS" promotes the keyword, but it's also woefully undermaintained and unresponsive, even to outright inaccuracies. – Charles Duffy May 07 '18 at 10:38
  • @JánLalinský, it only improves readability to people who've trained their eyes to expect it. – Charles Duffy May 07 '18 at 10:39
4

You can initialize a variable to an empty object {} and add the key/values {($key):$value} for each iteration, re-injecting the result in the same variable :

#!/bin/bash

declare -A dict=()

dict["foo"]=1
dict["bar"]=2
dict["baz"]=3

data='{}'

for i in "${!dict[@]}"
do
    data=$(jq -n --arg data "$data" \
                 --arg key "$i"     \
                 --arg value "${dict[$i]}" \
                 '$data | fromjson + { ($key) : ($value | tonumber) }')
done

echo "$data"
Bertrand Martel
  • 42,756
  • 16
  • 135
  • 159
3

bash 5.2 introduces the @k parameter transformation which, makes this much easier. Like:

$ declare -A dict=([foo]=1 [bar]=2 [baz]=3)
$ jq -n '[$ARGS.positional | _nwise(2) | {(.[0]): .[1]}] | add' --args "${dict[@]@k}"
{
  "foo": "1",
  "bar": "2",
  "baz": "3"
}
oguz ismail
  • 1
  • 16
  • 47
  • 69
  • 1
    Nice, might need to make this the accepted answer in a few years when this version is more common. – htaccess Sep 29 '22 at 20:38
  • This version gave me an error: > ./echo-args.sh: line 23: ${args[@]@k}: bad substitution Where as the accepted answer "Generic solution" did not and output my args correctly > { "9": "-s", "8": "alpha1", "7": "-e", "6": "foo", "5": "-b", "4": "foo-foo-foo-foo-01", "3": "-r", "2": "foo-foo-foo-foo-01", "1": "-n", "20": "fooFoo", "18": "foo", "19": "-f", "12": 9092, "13": "-c", "10": "foo-alpha1", "11": "-p", "16": "alpha2.dev2.foo.io", "17": "-l", "14": "", "15": "-h" } – Darrell Mar 02 '23 at 17:27
  • @Darrell it says *bash-5.2*, you have an older version. – oguz ismail Mar 02 '23 at 17:39
  • @oguzismail ah yes apologies, ~downvote removed.~ stack over flow won't let me remove my downvote :-( – Darrell Mar 02 '23 at 18:21
  • 1
    @Darrell it's ok. you can remove it now if you want. have a nice day – oguz ismail Mar 02 '23 at 19:55
1

This has been posted, and credited to nico103 on IRC, which is to say, me.

The thing that scares me, naturally, is that these associative array keys and values need quoting. Here's a start that requires some additional work to dequote keys and values:

function assoc2json {
    typeset -n v=$1
    printf '%q\n' "${!v[@]}" "${v[@]}" |
        jq -Rcn '[inputs] |
                . as $v |
                (length / 2) as $n |
                reduce range($n) as $idx ({}; .[$v[$idx]]=$v[$idx+$n])'
}


$ assoc2json a
{"foo\\ bar":"1","b":"bar\\ baz\\\"\\{\\}\\[\\]","c":"$'a\\nb'","d":"1"}
$

So now all that's needed is a jq function that removes the quotes, which come in several flavors:

  • if the string starts with a single-quote (ksh) then it ends with a single quote and those need to be removed
  • if the string starts with a dollar sign and a single-quote and ends in a double-quote, then those need to be removed and internal backslash escapes need to be unescaped
  • else leave as-is

I leave this last iterm as an exercise for the reader.

I should note that I'm using printf here as the iterator!

user2259432
  • 2,239
  • 1
  • 19
  • 15