7

Given this file:

$ cat fruits.json
[
  { "name": "apple" },
  { "name": "banana\nfofanna" },
  { "name": "my kiwi" }
]

how can one use jq to retrieve a list of fruit names which the shell use as data, for example to populate an array?

Something equivalent to the assignment to the fruits array below:

$ fruits=( 
'apple'
'banana
fofanna'
'my kiwi'
)
$ for f in "${fruits[@]}" ;do echo "<$f>"; done
<apple>
<banana
fofanna>
<my kiwi>

None of the following work:

$ fruits=( $(jq -r ' .[].name       ' fruits.json))
$ fruits=( $(jq -r ' .[].name | @sh ' fruits.json))
$ fruits=( $(jq    ' .[].name | @sh ' fruits.json))
2

3 Answers 3

16

The first two attempts at the end of the question do not work because they rely on the shell splitting the output of jq on whitespace. Since newline is one type of whitespace, you lose the newlines (and tabs and the original spaces) in the data.

The attempts additionally fail to quote the string data, so you would get filename globbing happening if any string contained filename globbing characters.

The last attempt fails for similar reasons but additionally does not properly decode the data, leaving encoded newlines in place.


Using the built-in @sh operator in jq to quote the data and build an array assignment:

eval "$( jq -r '"fruits=(" + (map(.name)|@sh) + ")"' fruits.json )"

For the given JSON data, this would cause the following assignment to be evaluated by the current shell, creating the array fruits:

fruits=('apple' 'banana
fofanna' 'my kiwi')

After evaluating that statement,

$ printf '<%s>\n' "${fruits[@]}"
<apple>
<banana
fofanna>
<my kiwi>

As an alternative, the following would append each name element's value to the shell array:

$ jq -r '"fruits+=(" + (.[].name | @sh) + ")"' fruits.json
fruits+=('apple')
fruits+=('banana
fofanna')
fruits+=('my kiwi')
$ unset -v fruits
$ eval "$(jq -r '"fruits+=(" + (.[].name | @sh) + ")"' fruits.json)"
$ printf '<%s>\n' "${fruits[@]}"
<apple>
<banana
fofanna>
<my kiwi>
3

You did not mention what shell; for ZSH one might use

% fru=( ${(fQg::)"$(jq '.[].name' ~/tmp/fru)"} )
% for f in $fru; printf '<%s>\n' $f
<apple>
<banana
fofanna>
<my kiwi>
% =echo -n "<${fru[2]}>" | od -bc
0000000   074 142 141 156 141 156 141 012 146 157 146 141 156 156 141 076
           <   b   a   n   a   n   a  \n   f   o   f   a   n   n   a   >
0000020

which performs the jq call, then f splits the result on newlines, and Q removes the quoting that the jq output leaves on the strings, and g:: acts like the builtin echo to perform interpolation on such strings as \n in the input.

1
  • Interesting that this technique in zsh doesn't need to use eval.
    – jrw32982
    Commented Aug 2, 2022 at 19:58
0

Based on answers and comments from @Kusalananda, @roaima, and @StéphaneChazelas and in the similar question mentioned in a comment above, I was able to come up with:

list=$( jq -r ' .[].name | @sh ' fruits.json ) || exit 1
eval "fruits=( $list )" || exit 1
printf "<%s>\n" "${fruits[@]}"

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .