Assigning jq output to bash array when json value contains spaces

Viewed 1761

When I do this:

arr=($(echo '{"crew":[{"name":"kirk"},{"name":"bones"},{"name":"mr spock"}]}' | jq -r '.crew[].name | @sh'))

I get:

echo "${arr[2]}"
mr
echo "${arr[3]}"
spock

However when I do this:

arr=("kirk" "bones" "mr spock")

I get this:

echo "${arr[2]}"
mr spock

Why, in the first example, is bash ignoring the quotes that each jq value is wrapped in when it creates the array?

2 Answers
$ ary=($(echo '{"crew":[{"name":"kirk"},{"name":"bones"},{"name":"mr spock"}]}' | jq -r '.crew[].name | @sh'))
$ declare -p ary
declare -a ary=([0]="'kirk'" [1]="'bones'" [2]="'mr" [3]="spock'")

This does not work as expected because the command substitution is unquoted, bash will perform word splitting on the output. It doesn't matter that the actual output contains quote characters. Refer to 3.5 Shell Expansions in the manual

You need to delay the array assignment until the contents of the output can be examined by the shell. This could be done with eval, but for variable assignment it's better done with declare:

$ declare -a "ary=($(echo '{"crew":[{"name":"kirk"},{"name":"bones"},{"name":"mr spock"}]}' | jq -r '.crew[].name | @sh'))"
$ declare -p ary
declare -a ary=([0]="kirk" [1]="bones" [2]="mr spock")

For readability, split that into steps:

$ jq_out=$(echo '{"crew":[{"name":"kirk"},{"name":"bones"},{"name":"mr spock"}]}' | jq -r '.crew[].name | @sh')
$ declare -a "ary=( $jq_out )"
$ declare -p ary
declare -a ary=([0]="kirk" [1]="bones" [2]="mr spock")

If your bash does not support mapfile/readarray, you could use the idiom illustrated by the following:

echo $BASH_VERSION

while read -r name ; do
    ary+=("$name")
done < <(echo '{"crew":[{"name":"kirk"},{"name":"bones"},{"name":"mr spock"}]}' |
   jq -r '.crew[].name')

declare -p ary
Output
3.2.57(1)-release
declare -a ary='([0]="kirk" [1]="bones" [2]="mr spock")'

Notice in particular that

  • $name is quoted in the array-update line
  • there is no need to use @sh, which indeed in this case is probably not what you want.

Of course, if the JSON strings might contain embedded new lines, then the above would have to be tweaked accordingly, e.g. to use NUL ("\u0000") as the delimiter.

Related