jq for Sysadmins

jq patterns for parsing system administration JSON output.

The Revolution

Most Linux tools now support -j, --json, or -o json output. This transforms sysadmin work from regex hell to structured queries.

Quick Reference

Tool JSON Flag

ip

-j or --json

ss

-O json (capital O)

lsblk

-J or --json

findmnt

-J or --json

systemctl

--output=json

journalctl

-o json

podman

--format json

docker

--format json or inspect

kubectl

-o json

nmcli

(no native, use awk conversion)

resolvectl

--json=pretty

ip command

# All interfaces with IPs
ip -j addr | jq '.[] | {name: .ifname, ips: [.addr_info[].local]}'

# Just interface names and states
ip -j link | jq -r '.[] | "\(.ifname): \(.operstate)"'

# Routing table
ip -j route | jq -r '.[] | "\(.dst) via \(.gateway // "direct") dev \(.dev)"'

# Default gateway only
ip -j route | jq -r '.[] | select(.dst == "default") | .gateway'

# Find interface for specific IP
ip -j addr | jq -r '.[] | select(.addr_info[].local == "10.50.1.100") | .ifname'

# All IPv4 addresses (no loopback)
ip -j -4 addr | jq -r '.[] | select(.ifname != "lo") | "\(.ifname): \(.addr_info[0].local)"'

# MAC addresses
ip -j link | jq -r '.[] | select(.ifname != "lo") | "\(.ifname): \(.address)"'

ss (sockets)

# Listening TCP ports with process
ss -tlnp -O json | jq -r '.[] | "\(.local.port)\t\(.process // "kernel")"' | sort -n

# Established connections
ss -tnp -O json | jq '.[] | select(.state == "ESTAB") | {local: .local, peer: .peer}'

# Connections to specific port
ss -tnp -O json | jq '.[] | select(.peer.port == 443)'

# Count connections by state
ss -tn -O json | jq 'group_by(.state) | map({state: .[0].state, count: length})'

# Top talkers (most connections)
ss -tnp -O json | jq -r '.[].peer.address' | sort | uniq -c | sort -rn | head -10

resolvectl

# DNS servers per interface
resolvectl --json=pretty | jq '.[] | {iface: .ifname, dns: .dns}'

# Current DNS for all interfaces
resolvectl --json=short | jq -r '.[] | "\(.ifname): \(.dns | join(", "))"'

lsblk

# Disks and sizes
lsblk -J | jq -r '.blockdevices[] | "\(.name): \(.size) (\(.type))"'

# Only disks (no partitions)
lsblk -J | jq '.blockdevices[] | select(.type == "disk") | {name, size}'

# Mounted filesystems
lsblk -J | jq '.blockdevices[] | select(.mountpoint != null) | {name, mountpoint, size}'

# Find disk by size (e.g., >100G)
lsblk -b -J | jq '.blockdevices[] | select(.size > 107374182400) | .name'

# Partition layout
lsblk -J | jq '.blockdevices[] | {disk: .name, partitions: [.children[]?.name]}'

findmnt

# All mounts
findmnt -J | jq -r '.filesystems[] | "\(.target): \(.source)"'

# Filter by filesystem type
findmnt -J | jq '.filesystems[] | select(.fstype == "ext4")'

# NFS mounts only
findmnt -J | jq '.filesystems[] | select(.fstype == "nfs" or .fstype == "nfs4")'

# Mount options for specific path
findmnt -J /home | jq -r '.filesystems[0].options'

df (via awk to JSON)

# Convert df to JSON
df -h | awk 'NR>1 {printf "{\"fs\":\"%s\",\"size\":\"%s\",\"used\":\"%s\",\"avail\":\"%s\",\"pct\":\"%s\",\"mount\":\"%s\"}\n",$1,$2,$3,$4,$5,$6}' | jq -s '.'

# Find filesystems over 80% used
df | awk 'NR>1 {gsub(/%/,"",$5); if($5>80) printf "{\"mount\":\"%s\",\"pct\":%d}\n",$6,$5}' | jq -s '.'

systemctl

# All active services
systemctl list-units --type=service --state=active --output=json | jq '.[].unit'

# Failed units
systemctl list-units --state=failed --output=json | jq -r '.[].unit'

# Unit details
systemctl show sshd --output=json | jq '{name: .Id, state: .ActiveState, pid: .MainPID}'

# Memory usage by service
systemctl list-units --type=service --state=running --output=json | \
  jq -r '.[].unit' | xargs -I{} systemctl show {} -p MemoryCurrent,Id --output=json | \
  jq -s 'sort_by(-.MemoryCurrent) | .[:10] | .[] | "\(.Id): \(.MemoryCurrent / 1048576 | floor) MB"'

# Enabled services
systemctl list-unit-files --type=service --state=enabled --output=json | jq -r '.[].unit_file'

journalctl

# Recent errors (priority 3 = error)
journalctl -p 3 -o json --since "1 hour ago" | jq -s '.[] | {time: .__REALTIME_TIMESTAMP, unit: ._SYSTEMD_UNIT, msg: .MESSAGE}'

# Logs from specific unit
journalctl -u sshd -o json -n 50 | jq -s '.[] | {time: .__REALTIME_TIMESTAMP, msg: .MESSAGE}'

# Count messages by unit
journalctl -o json --since today | jq -s 'group_by(._SYSTEMD_UNIT) | map({unit: .[0]._SYSTEMD_UNIT, count: length}) | sort_by(-.count) | .[:10]'

# SSH auth failures
journalctl -u sshd -o json | jq -s '[.[] | select(.MESSAGE | contains("Failed"))] | length'

# Kernel messages only
journalctl -k -o json -n 100 | jq -s '.[] | select(.PRIORITY == "4" or .PRIORITY == "3") | .MESSAGE'

loginctl

# Active sessions
loginctl list-sessions --output=json | jq '.[] | {session: .session, user: .user, tty: .tty}'

# Session details
loginctl show-session 1 --output=json | jq '{user: .Name, remote: .Remote, since: .Timestamp}'

podman

# Running containers
podman ps --format json | jq -r '.[] | "\(.Names): \(.State) (\(.Image | split("/")[-1]))"'

# Container IPs
podman ps --format json | jq -r '.[] | "\(.Names): \(.Networks | to_entries[0].value.IPAddress // "none")"'

# Image sizes
podman images --format json | jq -r '.[] | "\(.names[0] // .Id[:12]): \(.size / 1048576 | floor) MB"'

# Container resource usage
podman stats --no-stream --format json | jq '.[] | {name: .Name, cpu: .CPUPerc, mem: .MemUsage}'

# Inspect specific container
podman inspect mycontainer | jq '.[0] | {ip: .NetworkSettings.IPAddress, ports: .NetworkSettings.Ports}'

docker

# Same patterns work - just replace podman with docker

# Container IPs (docker format slightly different)
docker ps -q | xargs docker inspect | jq -r '.[] | "\(.Name): \(.NetworkSettings.IPAddress)"'

# Volumes
docker volume ls --format json | jq '.[] | {name: .Name, driver: .Driver}'

kubectl

# Pod status
kubectl get pods -o json | jq -r '.items[] | "\(.metadata.name): \(.status.phase)"'

# Pods not running
kubectl get pods -o json | jq '.items[] | select(.status.phase != "Running") | .metadata.name'

# Container images in use
kubectl get pods -o json | jq -r '[.items[].spec.containers[].image] | unique | .[]'

# Resource requests/limits
kubectl get pods -o json | jq '.items[] | {name: .metadata.name, containers: [.spec.containers[] | {name, resources}]}'

# Node capacity
kubectl get nodes -o json | jq '.items[] | {name: .metadata.name, cpu: .status.capacity.cpu, mem: .status.capacity.memory}'

# Events (recent)
kubectl get events -o json | jq '.items | sort_by(.lastTimestamp) | reverse | .[:10] | .[] | {type: .type, reason: .reason, msg: .message}'

# Secrets (list keys, not values)
kubectl get secret mysecret -o json | jq '.data | keys'

ps (via awk to JSON)

# Top CPU processes as JSON
ps aux --sort=-%cpu | head -11 | awk 'NR>1 {printf "{\"user\":\"%s\",\"pid\":%s,\"cpu\":%.1f,\"mem\":%.1f,\"cmd\":\"%s\"}\n",$1,$2,$3,$4,$11}' | jq -s '.'

# Top memory processes
ps aux --sort=-%mem | head -11 | awk 'NR>1 {printf "{\"pid\":%s,\"mem\":%.1f,\"cmd\":\"%s\"}\n",$2,$4,$11}' | jq -s '.'

# Process tree (parent-child)
ps -eo pid,ppid,comm --no-headers | awk '{printf "{\"pid\":%s,\"ppid\":%s,\"cmd\":\"%s\"}\n",$1,$2,$3}' | jq -s '.'

/proc as JSON

# Memory info
awk '/MemTotal|MemFree|MemAvailable|Buffers|Cached/ {gsub(/:/,"",$1); printf "\"%s\": %d,\n",$1,$2}' /proc/meminfo | sed '$ s/,$//' | jq -s 'add'

# CPU info
grep -E "^(processor|model name|cpu MHz)" /proc/cpuinfo | awk -F: '{gsub(/^[ \t]+/,"",$2); printf "\"%s\": \"%s\",\n",$1,$2}' | sed '$ s/,$//' | jq -s 'add'

# Load average
awk '{printf "{\"1min\": %.2f, \"5min\": %.2f, \"15min\": %.2f}\n",$1,$2,$3}' /proc/loadavg | jq '.'

/etc/passwd to JSON

# All users
awk -F: '{printf "{\"user\":\"%s\",\"uid\":%d,\"gid\":%d,\"home\":\"%s\",\"shell\":\"%s\"}\n",$1,$3,$4,$6,$7}' /etc/passwd | jq -s '.'

# Real users only (uid >= 1000)
awk -F: '$3 >= 1000 {printf "{\"user\":\"%s\",\"uid\":%d,\"shell\":\"%s\"}\n",$1,$3,$7}' /etc/passwd | jq -s '.'

# Users with bash shell
awk -F: '$7 ~ /bash/ {print $1}' /etc/passwd | jq -R -s 'split("\n") | map(select(. != ""))'

lastlog / last

# Recent logins as JSON
last -n 20 | head -n -2 | awk '{printf "{\"user\":\"%s\",\"tty\":\"%s\",\"from\":\"%s\",\"time\":\"%s %s %s %s\"}\n",$1,$2,$3,$4,$5,$6,$7}' | jq -s '.'

# Failed logins
lastb 2>/dev/null | head -20 | awk '{printf "{\"user\":\"%s\",\"from\":\"%s\",\"time\":\"%s %s %s\"}\n",$1,$3,$4,$5,$6}' | jq -s '.'

YAML to JSON (yq)

# Convert YAML file to JSON
yq -o=json file.yml

# Query YAML with jq syntax
yq '.key.nested' file.yml

# Convert and pipe to jq
yq -o=json file.yml | jq '.items[] | select(.enabled)'

# Multiple YAML docs to JSON array
yq -o=json -s '.' file.yml

JSON to YAML

# Convert JSON to YAML
yq -P file.json

# Pipe JSON to YAML
curl -s api.example.com/data | yq -P

CSV to JSON

# CSV with headers
cat file.csv | jq -R -s 'split("\n") | .[1:] | map(split(",")) | map({name: .[0], value: .[1]})'

# Using miller (better for complex CSV)
mlr --c2j cat file.csv

INI to JSON

# Simple INI parsing
awk -F= '/^\[/ {section=$0; gsub(/[\[\]]/,"",section)} /=/ {printf "{\"section\":\"%s\",\"key\":\"%s\",\"value\":\"%s\"}\n",section,$1,$2}' file.ini | jq -s '.'

Table output to JSON

# Generic: convert any table with headers
some_command | awk 'NR==1 {for(i=1;i<=NF;i++) h[i]=$i; next} {printf "{"; for(i=1;i<=NF;i++) printf "\"%s\":\"%s\"%s",h[i],$i,(i<NF?",":""); print "}"}' | jq -s '.'

JSON output

# All netapi commands support -f json
netapi ise mnt sessions -f json | jq '.[] | {mac: .calling_station_id, ip: .framed_ip_address}'

netapi github repos -f json | jq '.[] | select(.language == "Python") | .full_name'

netapi gitlab projects -f json | jq 'sort_by(-.star_count) | .[:5] | .[].path_with_namespace'

netapi monad pipelines -f json | jq '.[] | select(.enabled) | .name'

Combine with system data

# ISE sessions + local ARP
netapi ise mnt sessions -f json | jq -r '.[].framed_ip_address' | while read ip; do
  mac=$(ip -j neigh | jq -r ".[] | select(.dst == \"$ip\") | .lladdr")
  echo "$ip -> $mac"
done

# Cross-reference container IPs with firewall rules
podman ps --format json | jq -r '.[].Networks | to_entries[0].value.IPAddress' | while read ip; do
  echo "=== $ip ==="
  sudo iptables -L -n | grep "$ip"
done

Aggregation

# Group by field
jq 'group_by(.status) | map({status: .[0].status, count: length})'

# Sum values
jq '[.[].value] | add'

# Average
jq '[.[].value] | add / length'

# Min/Max
jq '[.[].value] | min, max'

Filtering

# Multiple conditions
jq '.[] | select(.status == "active" and .count > 10)'

# Contains string
jq '.[] | select(.name | contains("prod"))'

# Regex match
jq '.[] | select(.name | test("^prod-[0-9]+"))'

# Not null
jq '.[] | select(.field != null)'

Transformation

# Rename keys
jq '.[] | {hostname: .name, ip: .address}'

# Add computed field
jq '.[] | . + {full_name: "\(.first) \(.last)"}'

# Flatten nested
jq '[.items[].subitems[]] | flatten'

# Unique values
jq '[.[].category] | unique'

Output formats

# TSV for shell processing
jq -r '.[] | [.name, .ip, .status] | @tsv'

# CSV
jq -r '.[] | [.name, .ip, .status] | @csv'

# Shell variables
jq -r '@sh "NAME=\(.name) IP=\(.ip)"'

# Custom format
jq -r '.[] | "Host: \(.name) [\(.ip)]"'

Daily Driver One-Liners

# What's listening?
ss -tlnp -O json | jq -r '.[] | "\(.local.port)\t\(.process // "kernel")"' | sort -n

# My IPs
ip -j -4 addr | jq -r '.[] | select(.ifname != "lo") | "\(.ifname): \(.addr_info[0].local)"'

# Disk space (>80%)
df | awk 'NR>1 {gsub(/%/,"",$5); if($5>80) print $6": "$5"%"}'

# Failed services
systemctl list-units --state=failed --output=json | jq -r '.[].unit'

# Recent errors
journalctl -p err -o json --since "1 hour ago" -n 20 | jq -s '.[] | .MESSAGE' | head -20

# Container status
podman ps --format json | jq -r '.[] | "\(.Names): \(.State)"'

# Who's logged in
loginctl list-sessions --output=json | jq -r '.[] | "\(.user)@\(.tty // "pts")"'

ANSI Escape Codes in jq

# Color codes
# \u001b[31m  red
# \u001b[32m  green
# \u001b[33m  yellow
# \u001b[34m  blue
# \u001b[35m  magenta
# \u001b[36m  cyan
# \u001b[0m   reset
# \u001b[1m   bold

# Green interface, cyan IP
ip -j -4 addr | jq -r '.[] | select(.ifname != "lo") | "\u001b[32m\(.ifname)\u001b[0m: \u001b[36m\(.addr_info[0].local)\u001b[0m"'

# With readable variables
ip -j -4 addr | jq -r '
  def green: "\u001b[32m";
  def cyan: "\u001b[36m";
  def reset: "\u001b[0m";
  .[] | select(.ifname != "lo") | "\(green)\(.ifname)\(reset): \(cyan)\(.addr_info[0].local)\(reset)"
'

# Status with colors (green=good, red=bad)
systemctl list-units --type=service --output=json | jq -r '
  def green: "\u001b[32m";
  def red: "\u001b[31m";
  def reset: "\u001b[0m";
  .[] | if .active == "active" then "\(green)●\(reset) \(.unit)"
        else "\(red)●\(reset) \(.unit)" end
'

# Port status - highlight well-known ports
ss -tlnp 2>/dev/null | awk 'NR>1 {
  port=$4; sub(/.*:/,"",port)
  if (port < 1024) printf "\033[33m%s\033[0m\t%s\n", port, $6
  else printf "%s\t%s\n", port, $6
}' | sort -n

Color Helper Functions

Add to ~/.zshrc or ~/.bashrc:

# jq with color helpers
jqc() {
  jq -r '
    def red: "\u001b[31m";
    def green: "\u001b[32m";
    def yellow: "\u001b[33m";
    def blue: "\u001b[34m";
    def magenta: "\u001b[35m";
    def cyan: "\u001b[36m";
    def bold: "\u001b[1m";
    def reset: "\u001b[0m";
    '"$1"'
  '
}

# Usage:
# ip -j addr | jqc '.[] | "\(green)\(.ifname)\(reset): \(.addr_info[0].local // "none")"'

Conditional Colors

# Color by value
jq -r '
  def colorize(v):
    if v > 80 then "\u001b[31m\(v)%\u001b[0m"      # red
    elif v > 50 then "\u001b[33m\(v)%\u001b[0m"   # yellow
    else "\u001b[32m\(v)%\u001b[0m" end;          # green
  .[] | "\(.name): \(colorize(.usage))"
'

# Color log levels
journalctl -o json -n 50 | jq -r '
  def colorize:
    if .PRIORITY == "3" then "\u001b[31m[ERR]\u001b[0m"
    elif .PRIORITY == "4" then "\u001b[33m[WARN]\u001b[0m"
    elif .PRIORITY == "6" then "\u001b[36m[INFO]\u001b[0m"
    else "[???]" end;
  "\(colorize) \(.MESSAGE[:80])"
'

Discovery: Finding JSON Support

# Check if tool supports JSON
man <tool> | grep -i json
<tool> --help | grep -i json

# Common patterns to try
<tool> --json
<tool> -j
<tool> -J
<tool> --output=json
<tool> -o json
<tool> --format json
<tool> --format=json

Install yq

# Arch
sudo pacman -S yq

# Fedora/RHEL
sudo dnf install yq

# Ubuntu/Debian
sudo snap install yq

# Binary (any Linux)
wget https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 -O ~/.local/bin/yq
chmod +x ~/.local/bin/yq