jq — Infrastructure
ip -j — JSON Network Interface Data
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.
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"}
# ]
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
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"}
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
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.
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
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)
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
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"}
# ]
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
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
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
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
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
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
# 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
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 -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 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
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.
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