Search code examples
jsonshelljqcategorization

Getting per-category counts of list items with jq


I'm currently learning how to use jq with shell in Linux since I'm developing custom checks for Check_MK (formerly known as Nagios) and my application (qBittorrent with their WebUI API) returns JSON strings.

Currently, I'm already able to count the total number of torrents just by using a simple jq length. Now, I would like to count the number of torrents that are currently dowloading, seeding or on pause. I'm only interested by the state, so if I have 6 torrents, my JSON could look like that:

[
  {
    "state": "uploading"
  },
  {
    "state": "downloading"
  },
  {
    "state": "downloading"
  },
  {
    "state": "downloading"
  },
  {
    "state": "pauseDL"
  },
  {
    "state": "pauseUP"
  }
]

Here, jq length returns 6. What do I need to do to get the details such as 3 are downloading, 1 is uploading, 2 are paused and 0 are in error?

Here is my actual script:

#!/bin/sh
curl -s http://localhost:8080/query/torrents -o /tmp/torrents.json
count=$(jq length /tmp/torrents.json)
echo "0 qbt_Nb_torrents - $count"

The syntax for the echo is required by Check_MK (as explained here).

I've read multiple examples on filters but they all seem to be working when we're filtering through the top-level attributes. Here, my top level is basically just [0], ..., [5], so it doesn't work with the examples I've found in the manual.

Additional information

The WebUI API says there are 12 different possible states. That's how I intend to split them up:

downloading: queuedDL, checkingDL, downloading 
uploading: queuedUP, checkingUP, uploading 
pause: pausedUP, pausedDL 
error: error 
stalled: stalledUP, stalledDL, metaDL

As per the CheckMK syntax, I need to basically output something like:

0 qbt_Nb_torrents - 6 total, 3 downloading, 1 seeding, 2 on pause, 0 stalled, 0 error

The first 0 at the beginning means an OK status for CheckMK. If there are any stalled torrents, I want that status to become 1, and if there is any torrent in error, the status becomes 2. Example:

2 qbt_Nb_torrents - 8 total, 3 downloading, 1 seeding, 2 on pause, 1 stalled, 1 error


Solution

  • For others with related questions, but not sharing the OP's specific requirements: See edit history! There are several other relevant proposals, including group_by use, in prior iterations of this answer.


    If you need entries for all values, even ones which have no occurrence, you might consider:

    jq -r '
      def filterStates($stateMap):
        if $stateMap[.] then $stateMap[.] else . end;
    
      def errorLevel:
        if (.["error"] > 0) then 2 else
          if (.["stalled"] > 0) then 1 else
            0
          end
        end;
    
      {"queuedDL": "downloading", 
       "checkingDL": "downloading",
       "queuedUP": "uploading", 
       "checkingUP": "uploading",
       "pausedUP": "pause", 
       "pausedDL": "pause",
       "stalledUP": "stalled", 
       "stalledDL": "stalled", 
       "metaDL": "stalled"} as $stateMap |
    
      # initialize an output array since we want 0 outputs for everything
      {"pause": 0,  "stalled": 0, "error": 0, "downloading": 0, "uploading": 0} as $counts |
    
      # count number of items which filter to each value
      reduce (.[].state | filterStates($stateMap)) as $state ($counts; .[$state]+=1) |
    
      # actually format an output string
      "\(. | errorLevel) qbt_Nb_torrents - \(values | add) total, \(.["downloading"]) downloading, \(.["uploading"]) seeding, \(.["pause"]) on pause, \(.["stalled"]) stalled, \(.["error"]) error"
    ' /tmp/torrents.json