jq — Infrastructure

ip -j — JSON Network Interface Data

List all interfaces with state
ip -j link | jq -r '.[] | "\(.ifname)\t\(.operstate)\t\(.address // "none")"' | column -t
# Output:
# lo         UNKNOWN  00:00:00:00:00:00
# eth0       UP       a8:2b:dd:8f:23:e6
# wlan0      UP       e0:d5:5d:6c:e1:66
# docker0    DOWN     e6:10:99:fb:93:f4

ip -j outputs JSON instead of the usual text format. Every ip subcommand supports it: ip -j addr, ip -j route, ip -j neigh.

Filter to UP interfaces only
ip -j link | jq '[.[] | select(.operstate == "UP") | {name: .ifname, mac: .address}]'
# Output:
# [
#   {"name": "wlan0", "mac": "e0:d5:5d:6c:e1:66"},
#   {"name": "eth0", "mac": "a8:2b:dd:8f:23:e6"}
# ]
Get IPv4 addresses per interface
ip -j addr | jq -r '.[] | select(.addr_info | length > 0) |
    "\(.ifname): \([.addr_info[] | select(.family == "inet") | .local] | join(", "))"' | \
    grep -v '^$'
# Output:
# lo: 127.0.0.1
# wlan0: 10.50.10.126
# docker0: 172.17.0.1
Routing table as JSON
ip -j route | jq '.[] | {dst: .dst, gateway: (.gateway // "direct"), dev: .dev}'
# Output:
# {"dst": "default", "gateway": "10.50.10.1", "dev": "wlan0"}
# {"dst": "10.50.10.0/24", "gateway": "direct", "dev": "wlan0"}
# {"dst": "172.17.0.0/16", "gateway": "direct", "dev": "docker0"}
ARP/neighbor table
ip -j neigh | jq -r '.[] | select(.state != [] and (.state | index("STALE") | not)) |
    "\(.dst)\t\(.lladdr // "incomplete")\t\(.dev)"' | column -t
# Shows only REACHABLE neighbors, not STALE cache entries

ss — Socket Statistics as JSON

Parse ss output to JSON for analysis
ss -tlnp | tail -n +2 | awk '{print "{\"state\":\""$1"\",\"local\":\""$4"\",\"process\":\""$NF"\"}"}' | \
    jq -s 'sort_by(.local)'
# Listening TCP sockets as sorted JSON array

ss does not have a -j flag. The awk step constructs JSON from its columnar output, then jq operates on it.

Count listening ports by address family
ss -tlnp | tail -n +2 | awk '{print "{\"addr\":\""$4"\"}"}' | \
    jq -rs '[.[] | if (.addr | test("^\\[?::")) then "ipv6" elif (.addr | test("^\\*:")) then "any" else "ipv4" end] |
    group_by(.) | map({family: .[0], count: length})'
# Output:
# [
#   {"family": "any", "count": 5},
#   {"family": "ipv4", "count": 3},
#   {"family": "ipv6", "count": 8}
# ]

systemctl — Service Status

List failed units as JSON
systemctl list-units --state=failed --no-pager --no-legend | \
    awk '{print "{\"unit\":\""$2"\",\"load\":\""$3"\",\"active\":\""$4"\",\"sub\":\""$5"\"}"}' | \
    jq -s '.'
# Output: array of failed unit objects (empty array if system is healthy)
Timer schedule overview
systemctl list-timers --no-pager --no-legend | \
    awk '{print "{\"next\":\""$1" "$2" "$3"\",\"unit\":\""$NF"\"}"}' | \
    jq -s 'sort_by(.next)'
# Output: timer units sorted by next activation

nmcli — NetworkManager Queries

Active connections as JSON
nmcli -t -f NAME,TYPE,DEVICE connection show --active | \
    awk -F: '{print "{\"name\":\""$1"\",\"type\":\""$2"\",\"device\":\""$3"\"}"}' | \
    jq -s '.'
# Output:
# [
#   {"name": "Home WiFi", "type": "802-11-wireless", "device": "wlan0"}
# ]
WiFi signal strength report
nmcli -t -f SSID,SIGNAL,SECURITY dev wifi list | \
    awk -F: '{print "{\"ssid\":\""$1"\",\"signal\":"$2",\"security\":\""$3"\"}"}' | \
    jq -s 'sort_by(.signal) | reverse | .[:10]'
# Output: top 10 WiFi networks by signal strength

Existing Infrastructure Examples

The following example contains a complete 7-level infrastructure query progression — from simple filtering through multi-source joins to formatted reports:

Infrastructure Queries

Level 1: Filter — select objects by condition

Only UP interfaces
ip -j link | jq '[.[] | select(.operstate == "UP") | {name: .ifname, mac: .address}]'
# Output:
# [
#   {"name": "wlan0", "mac": "e0:d5:5d:6c:e1:66"},
#   {"name": "br-49799088587f", "mac": "96:1f:81:3a:d5:90"},
#   ...
# ]

select() is jq’s WHERE clause — it keeps objects matching the condition and drops the rest.

Level 2: Enrich — extract nested structures

Interfaces with their IPv4 addresses
ip -j addr | jq '.[] | select(.addr_info | length > 0) | {
  name: .ifname,
  mac: .address,
  state: .operstate,
  ips: [.addr_info[] | select(.family == "inet") | .local]
}'
# Output:
# {"name": "wlan0", "mac": "e0:d5:5d:6c:e1:66", "state": "UP", "ips": ["10.50.10.126"]}
# {"name": "docker0", "mac": "e6:10:99:fb:93:f4", "state": "DOWN", "ips": ["172.17.0.1"]}

.addr_info[] unpacks the nested array. select(.family == "inet") filters to IPv4 only. The […​] wrapper collects results back into an array.

Level 3: Classify — tag data with if/elif

Categorize interfaces by role
ip -j link | jq '.[] | {
  name: .ifname,
  mac: .address,
  role: (
    if .ifname == "lo" then "loopback"
    elif (.ifname | startswith("wlan")) then "wifi"
    elif (.ifname | startswith("enp")) then "wired"
    elif (.ifname | startswith("tailscale")) then "vpn"
    elif (.ifname | startswith("docker")) or (.ifname | startswith("br-")) then "container"
    elif (.ifname | startswith("veth")) then "container"
    else "unknown"
    end
  )
}'
# Output:
# {"name": "wlan0", "mac": "e0:d5:5d:6c:e1:66", "role": "wifi"}
# {"name": "enp134s0", "mac": "a8:2b:dd:8f:23:e6", "role": "wired"}
# {"name": "tailscale0", "mac": null, "role": "vpn"}

if/elif/end inside jq works like a CASE statement. Parentheses around conditions are required.

Level 4: Aggregate — group_by and count

Count interfaces per role
ip -j link | jq '[.[] | {
  name: .ifname,
  role: (
    if .ifname == "lo" then "loopback"
    elif (.ifname | startswith("wlan")) then "wifi"
    elif (.ifname | startswith("enp")) then "wired"
    elif (.ifname | startswith("tailscale")) then "vpn"
    elif (.ifname | startswith("docker")) or (.ifname | startswith("br-")) then "container"
    elif (.ifname | startswith("veth")) then "container"
    else "unknown"
    end
  )
}] | group_by(.role) | map({role: .[0].role, count: length, interfaces: [.[].name]})'
# Output:
# [
#   {"role": "container", "count": 4, "interfaces": ["br-49799088587f","docker0","veth8cde84a","veth52f3b68"]},
#   {"role": "wifi", "count": 1, "interfaces": ["wlan0"]},
#   {"role": "wired", "count": 1, "interfaces": ["enp134s0"]},
#   {"role": "vpn", "count": 1, "interfaces": ["tailscale0"]}
# ]

group_by(.role) creates arrays of objects sharing the same role. map() transforms each group into a summary. This is jq’s GROUP BY + COUNT(*).

Level 5: Join two data sources — --argjson

Combine ip link + ip addr into one enriched export
ip -j link | jq --argjson addrs "$(ip -j addr)" '
[.[] | . as $link |
  ($addrs[] | select(.ifname == $link.ifname) | .addr_info // []) as $addrs_info |
  {
    name: .ifname,
    mac: .address,
    state: .operstate,
    mtu: .mtu,
    ips: [$addrs_info[] | select(.family == "inet") | {addr: .local, prefix: .prefixlen}],
    flags: .flags
  }
] | sort_by(.state)' > /tmp/network-inventory.json
# Joins link data with address data on ifname — like a SQL JOIN
# --argjson loads the second command's output as a jq variable

Level 6: Output formats — CSV, TSV, tables

Query the exported JSON as a database
# Interfaces with IPs
jq -r '.[] | select(.ips | length > 0) | "\(.name) → \(.ips[0].addr)/\(.ips[0].prefix)"' /tmp/network-inventory.json

# CSV export (for spreadsheets)
jq -r '.[] | [.name, .mac, .state, (.ips[0].addr // "none")] | @csv' /tmp/network-inventory.json

# TSV for column-formatted terminal display
jq -r '["NAME","MAC","STATE","IP"], (.[] | [.name, .mac, .state, (.ips[0].addr // "--")]) | @tsv' /tmp/network-inventory.json | column -t
# Output:
# NAME             MAC                STATE    IP
# enp134s0         a8:2b:dd:8f:23:e6  DOWN     --
# wlan0            e0:d5:5d:6c:e1:66  UP       10.50.10.126
# docker0          e6:10:99:fb:93:f4  DOWN     172.17.0.1

Level 7: Shell + jq — multi-source enrichment

Full inventory combining ip + nmcli with formatted output
ip -j link | jq -c '.[]' | while read -r line; do
    name=$(echo "$line" | jq -r '.ifname')
    mac=$(echo "$line" | jq -r '.address')
    state=$(echo "$line" | jq -r '.operstate')
    ip=$(ip -j addr show "$name" 2>/dev/null | jq -r '.[].addr_info[] | select(.family=="inet") | .local' 2>/dev/null | head -1)
    uuid=$(nmcli -t -f DEVICE,UUID connection show --active 2>/dev/null | grep "^$name:" | cut -d: -f2)
    printf "%-20s %-18s %-8s %-16s %s\n" "$name" "${mac:-none}" "$state" "${ip:---}" "${uuid:---}"
done
# Combines three data sources: ip link (MAC, state), ip addr (IPs), nmcli (UUIDs)
# UUIDs persist across adapter renames — use in scripts instead of interface names

The Pipeline: Query → Transform → Report

This is the pattern your management wants:

1. QUERY    — ip -j, nmcli, curl (API), sql
              → produces raw JSON

2. TRANSFORM — jq select, group_by, map, join
              → normalize, classify, aggregate

3. EXPORT   — @csv, @tsv, > file.json
              → feed to Python, spreadsheet, dashboard

4. VISUALIZE — matplotlib, plotly, d2 diagrams
              → charts, graphs, architecture diagrams

5. REPORT   — AsciiDoc, PDF, HTML
              → consumable by leadership

Every API you work with (ISE, QRadar, Vault, Wazuh, Sentinel) returns JSON. jq is the universal transformer between the API response and whatever output format the audience needs.

Package Manager Queries

pacman — installed package sizes (top 10)
pacman -Qi | awk '/^Name/{name=$3} /^Installed Size/{
    size=$4; unit=$5;
    if(unit=="MiB") bytes=size*1048576;
    else if(unit=="KiB") bytes=size*1024;
    else bytes=size;
    print "{\"name\":\""name"\",\"size_bytes\":"int(bytes)"}";
}' | jq -s 'sort_by(.size_bytes) | reverse | .[:10] | .[] |
    "\(.name): \(.size_bytes / 1048576 | floor)M"'
# Output: top 10 packages by installed size
Flatpak apps as JSON
flatpak list --app --columns=name,application,version 2>/dev/null | \
    tail -n +1 | awk -F'\t' '{print "{\"name\":\""$1"\",\"id\":\""$2"\",\"version\":\""$3"\"}"}' | \
    jq -s 'sort_by(.name)'
# Output: sorted array of installed Flatpak applications

journalctl — Structured Log Queries

Recent errors as JSON
journalctl -p err -n 10 --no-pager -o json | \
    jq '{time: .__REALTIME_TIMESTAMP, unit: ._SYSTEMD_UNIT, msg: .MESSAGE}'
# journalctl -o json outputs native JSON -- no awk bridge needed

journalctl -o json is one of the few system tools with native JSON output. Every log field is available.

Count errors per unit (last hour)
journalctl -p err --since "1 hour ago" --no-pager -o json | \
    jq -s 'group_by(._SYSTEMD_UNIT) | map({unit: (.[0]._SYSTEMD_UNIT // "kernel"), count: length}) |
    sort_by(.count) | reverse'
# Output: error frequency per systemd unit