Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to loop over jq unique array in bash?

Tags:

bash

jq

I'm trying to loop over unique names and commit messages from a github json object. However when there are spaces in the arrays bash will treat them as individual array items

#!/usr/bin/env bash

commits='[
  {
    "author": {
      "email": "[email protected]",
      "name": "Chris",
      "username": "chris"
    },
    "committer": {
      "email": "[email protected]",
      "name": "Chris",
      "username": "chris"
    },
    "message": "commit message 1"
  },
  {
    "author": {
      "email": "[email protected]",
      "name": "John",
      "username": "jdoe"
    },
    "committer": {
      "email": "[email protected]",
      "name": "John",
      "username": "jdoe"
    },
    "message": "commit message 2"
  },
    {
    "author": {
      "email": "[email protected]",
      "name": "John",
      "username": "jdoe"
    },
    "committer": {
      "email": "[email protected]",
      "name": "John",
      "username": "jdoe"
    },
    "message": "commit message 3"
  }
]'

authors=$( jq -rc '[.[].author.name] | unique | @sh' <<<"${commits}" )
echo "authors: $authors"

# this works
for author in $authors
do
  echo "author: $author"
done

echo "------------"

# this does not
messages=$( jq -rc '[.[].message] | unique | @sh' <<<"${commits}" )
echo "messages: $messages"

for message in $messages
do
  echo "message: $message"
done

Which outputs

authors: 'Chris' 'John'
author: 'Chris'
author: 'John'
------------
messages: 'commit message 1' 'commit message 2' 'commit message 3'
message: 'commit
message: message
message: 1'
message: 'commit
message: message
message: 2'
message: 'commit
message: message
message: 3'

While I expect:

authors: 'Chris' 'John'
author: 'Chris'
author: 'John'
------------
messages: 'commit message 1' 'commit message 2' 'commit message 3'
message: 'commit message 1'
message: 'commit message 2'
message: 'commit message 3'
like image 913
chrisan Avatar asked Mar 18 '26 08:03

chrisan


1 Answers

Use readarray (Bash 4+) to map null delimited output from jq:

#!/usr/bin/env bash

commits='[
  {
    "author": {
      "email": "[email protected]",
      "name": "Chris",
      "username": "chris"
    },
    "committer": {
      "email": "[email protected]",
      "name": "Chris",
      "username": "chris"
    },
    "message": "commit message 1"
  },
  {
    "author": {
      "email": "[email protected]",
      "name": "John",
      "username": "jdoe"
    },
    "committer": {
      "email": "[email protected]",
      "name": "John",
      "username": "jdoe"
    },
    "message": "commit message 2"
  },
    {
    "author": {
      "email": "[email protected]",
      "name": "John",
      "username": "jdoe"
    },
    "committer": {
      "email": "[email protected]",
      "name": "John",
      "username": "jdoe"
    },
    "message": "commit message 3"
  }
]'

readarray -d '' authors < <(jq -j '.[].author.name + "\u0000"' <<<"${commits}")

for author in "${authors[@]}"
do
  echo "author: $author"
done

echo "------------"

readarray -d '' messages < <(jq -j '.[].message + "\u0000"' <<<"${commits}")

for message in "${messages[@]}"
do
  echo "message: $message"
done

Alternatively, if you have an older Bash version without readarray or mapfile you may separate the strings with the ASCII control character ETX (End of TeXt 03) and use read instead like this:

IFS=$'\03' read -d '' -ra authors < <(jq -j '.[].author.name + "\u0003"' <<<"${commits}")

IFS=$'\03' read -d '' -ra messages < <(jq -j '.[].message + "\u0003"' <<<"${commits}")

It is also possible to populate both arrays from a single jq call:

# Populates both arrays from a single jq call
{
  IFS=$'\03' read -r -d '' -a authors
  IFS=$'\03' read -r -d '' -a messages
} < <(jq -j '([.[].author.name] | unique | .[] + "\u0003"), "\u0000",  ([.[].message] | unique | .[] + "\u0003")' <<<"${commits}")

Explanation:

  • [.[].author.name] | unique | .[] + "\u0003":
    Output an ETX (03) delimited list of unique author names.

  • "\u0000": insert a null delimiter

  • [.[].message] | unique | .[] + "\u0003":
    Output an ETX (03) delimited list of unique messages.

  • Feeds the whole output of jq to a command group with two read commands.
    Each read will stop at the null delimiter or end of the stream.

{
  IFS=$'\03' read -r -d '' -a authors
  IFS=$'\03' read -r -d '' -a messages
}
like image 50
Léa Gris Avatar answered Mar 19 '26 21:03

Léa Gris



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!