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 |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
(no native, use awk conversion) |
|
|
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