3

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?

5
  • 2
    You stripped out the quotes by using jq -r Commented Feb 25, 2021 at 14:51
  • @RamanSailopal The jq operator @sh escapes the output, as per the jq manual: "The input is escaped suitable for use in a command-line for a POSIX shell. If the input is an array, the output will be a series of space-separated strings." Commented Feb 25, 2021 at 14:59
  • 3
    Can you not do ... readarray -t arr <<< "$(echo '{"crew":[{"name":"kirk"},{"name":"bones"},{"name":"mr spock"}]}' | jq '.crew[].name')" Commented Feb 25, 2021 at 15:05
  • @RamanSailopal yes, readarray seems to work. Thank you. However, I am still interested to know why bash seems to remove/ignore the quotes that jq is wrapping the values in. Commented Feb 25, 2021 at 15:40
  • @JonHudson See "Why does shell ignore quoting characters in arguments passed to it through variables?" (it's about a different situation, but the word-splitting process is exactly the same). Commented Feb 25, 2021 at 17:22

2 Answers 2

2
$ 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")
Sign up to request clarification or add additional context in comments.

1 Comment

Thanks, this helps me understand why it was happening. I didn't know quote removal was a thing gnu.org/software/bash/manual/bash.html#Quote-Removal I think however that for simplicity and because I understand it more, I will use the readarray solution above.
2

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.

1 Comment

You'll want to use IFS= read -r name -- that will read the line exactly verbatim. Without IFS= then leading/trailing whitespace will be removed.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.