`jq`: el procesador de JSON para la línea de comandos

jq es un procesador de JSON que funciona como filtro: lee JSON por stdin o desde un archivo, aplica una expresión de filtro, y escribe el resultado por stdout. La analogía con awk es precisa — igual que awk transforma texto estructurado en columnas, jq transforma JSON estructurado en árboles. La diferencia es que jq entiende la semántica del formato: sabe qué es un objeto, un array, un string, un número nulo.

El diseño es funcional y composicional. Un filtro en jq es una expresión que toma un valor JSON y produce otro. El operador | encadena filtros igual que en bash: la salida de uno es la entrada del siguiente. Esto no es solo sintaxis bonita — es la razón por la que jq escala bien: puedes construir transformaciones complejas componiendo piezas simples sin variables temporales ni bucles explícitos.

¿Cuándo usarlo? Cada vez que necesites extraer datos de una API REST, parsear logs en JSON (Docker escribe en JSON por defecto, journalctl --output json también), o transformar configuración cloud en scripts de aprovisionamiento. El momento en que te encuentras usando grep o sed sobre JSON, es el momento en que deberías estar usando jq.

Lo que se rompe si lo evitas: grep sobre JSON falla silenciosamente cuando los campos cambian de orden, cuando hay espacios o saltos de línea dentro de valores, o cuando el JSON está minificado. Un script que funciona hoy con la API de GitHub puede romperse mañana porque cambiaron el formato de respuesta — jq con rutas explícitas hace ese contrato visible y falla de forma predecible.

jq está en los repositorios oficiales de Debian:

apt install jq

Ejemplo: procesar la API de GitHub y logs de Docker

El escenario es real: tienes un servicio en producción corriendo en Docker, quieres listar los repositorios públicos de una organización en GitHub y también inspeccionar los últimos eventos del contenedor. Todo en un script.

#!/usr/bin/env bash
set -euo pipefail

ORG="debian"
CONTAINER="mi-app"

# ── 1. Petición a la API de GitHub ────────────────────────────────────
# curl -s suprime el progress bar; sin -s el JSON llega mezclado con stderr
REPOS_JSON=$(curl -s "https://api.github.com/orgs/${ORG}/repos?per_page=5")

echo "=== Repositorios de ${ORG} ==="

# .[] itera sobre cada elemento del array raíz
# select() filtra: solo repos que no sean forks
# | .name, .stargazers_count extrae dos campos del objeto filtrado
echo "${REPOS_JSON}" | jq -r '
  .[]
  | select(.fork == false)
  | "\(.name)  ⭐ \(.stargazers_count)"
'

echo ""
echo "=== Repo con más estrellas ==="

# sort_by ordena el array; last() toma el último elemento (el mayor)
# El resultado se reformatea como objeto nuevo con solo los campos útiles
echo "${REPOS_JSON}" | jq '
  [ .[] | select(.fork == false) ]
  | sort_by(.stargazers_count)
  | last
  | { nombre: .name, estrellas: .stargazers_count, url: .html_url }
'

echo ""
echo "=== Últimos eventos del contenedor ${CONTAINER} ==="

# docker inspect devuelve un array con un único objeto; .[0] lo desenvuelve
CONTAINER_INFO=$(docker inspect "${CONTAINER}" 2>/dev/null) || {
  echo "(contenedor no encontrado, saltando)"
  exit 0
}

# .State extrae el subobjeto de estado; keys muestra los campos disponibles
echo "Campos disponibles en .State:"
echo "${CONTAINER_INFO}" | jq '.[0].State | keys'

# Extraer campos específicos del subobjeto anidado
echo "${CONTAINER_INFO}" | jq -r '
  .[0].State
  | "Status:   \(.Status)"
  + "\nPID:      \(.Pid)"
  + "\nIniciado: \(.StartedAt)"
'

echo ""
echo "=== Logs del contenedor en JSON (últimas 3 líneas) ==="

# journalctl con --output json emite una línea JSON por cada entrada de log
# -n 3: las últimas 3 entradas; -u: filtra por unidad systemd del contenedor
# Cada línea es un JSON independiente (JSON Lines), no un array
# Por eso no usamos .[] sino que jq procesa línea a línea con -c (compact)
journalctl -u "docker-${CONTAINER}" -n 3 --output json 2>/dev/null \
  | jq -r '"[\(.__REALTIME_TIMESTAMP | tonumber / 1000000 | todate)] \(.MESSAGE)"'

Qué está pasando en cada decisión

-r (raw output): sin -r, jq envuelve los strings en comillas dobles porque su salida es JSON válido por defecto. Con -r, los strings se emiten sin comillas, lo que permite usar el resultado en variables bash o pasar valores a otros comandos directamente. En el primer bloque lo usamos porque queremos líneas de texto, no JSON.

La expresión "\(.name) ⭐ \(.stargazers_count)": esto es interpolación de strings en jq. El \(expr) dentro de una cadena evalúa la expresión y la convierte a string. Funciona igual que ${} en bash, pero dentro del contexto JSON.

[ .[] | select(...) ]: fíjate en los corchetes que envuelven toda la expresión. Sin ellos, select() emitiría objetos sueltos uno a uno — no un array — y sort_by no podría operar sobre ellos porque necesita un array completo para ordenar. Los corchetes reconstruyen el array filtrado.

sort_by(.stargazers_count) | last: sort_by ordena de menor a mayor. last extrae el último elemento. Alternativa más explícita: | .[-1], que usa índice negativo igual que en Python. Ambas son equivalentes.

| { nombre: .name, estrellas: .stargazers_count, url: .html_url }: esto construye un objeto JSON nuevo con claves renombradas. Útil cuando necesitas normalizar la salida de una API antes de pasarla a otro sistema o guardarla en un log estructurado.

.[0].State: docker inspect siempre devuelve un array aunque inspecciones un único contenedor. .[0] desenvuelve el primer (y único) elemento. .State accede al subobjeto anidado. Se pueden encadenar sin límite: .[0].State.Health.Log[0].Output es perfectamente válido.

JSON Lines y el procesado línea a línea: journalctl --output json no emite un array JSON — emite una línea JSON válida por cada entrada de log. Es el formato JSON Lines (.jsonl). jq lo maneja transparentemente: procesa cada línea como un documento independiente. El campo .__REALTIME_TIMESTAMP es un entero de microsegundos desde epoch; tonumber / 1000000 lo convierte a segundos, y todate lo formatea como ISO 8601. Sin jq, extraer eso con sed o awk requeriría conocer la posición exacta del campo en el texto — frágil y roto en cuanto el campo cambia de orden.

keys: devuelve un array con los nombres de los campos de un objeto, ordenados alfabéticamente. Imprescindible cuando estás explorando una API desconocida y quieres saber qué campos existen antes de escribir filtros.

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio