Leer y crear JSON en Bash con jq: campos, filtros, variables y CSV

Cómo leer, filtrar y crear JSON desde un script de Bash con jq: sacar un campo, recorrer un array, filtrar y agrupar, pasar variables de Bash sin romper nada, construir JSON válido aunque los datos traigan comillas, modificar un archivo y convertirlo a CSV.

Bash no sabe nada de JSON: para él es solo texto. Intentar sacar un campo con grep, cut o sed funciona hasta que el JSON cambia de formato, trae una coma dentro de un texto o un campo en otra línea. La herramienta para hacerlo bien es jq, que entiende JSON de verdad. En Debian y Ubuntu se instala con sudo apt install jq y en macOS con brew install jq. Aquí usamos jq 1.8 y dos archivos de ejemplo, usuarios.json:

[
  {"id": 1, "nombre": "Ana López", "email": "ana@example.com", "activo": true, "rol": "admin", "pedidos": 12},
  {"id": 2, "nombre": "Bruno Díaz", "email": "bruno@example.com", "activo": false, "rol": "cliente", "pedidos": 3},
  {"id": 3, "nombre": "Carla Ruiz", "email": "carla@example.com", "activo": true, "rol": "cliente", "pedidos": 7},
  {"id": 4, "nombre": "David \"Dani\" Gil", "email": "dani@example.com", "activo": true, "rol": "soporte", "pedidos": 0}
]

y config.json:

{
  "servidor": {"host": "203.0.113.10", "puerto": 8080},
  "registro": {"nivel": "info", "archivo": "/var/log/app.log"},
  "funciones": ["pagos", "informes"]
}

Sacar un campo

Un filtro de jq es un camino dentro del JSON: . es el documento entero, .servidor el objeto servidor y .servidor.puerto su puerto. La opción -r (de raw) escribe los textos sin comillas, que es lo que quieres casi siempre para usarlos en Bash:

terminal$ jq '.servidor.puerto' config.json
8080
$ jq '.registro.nivel' config.json
"info"
$ jq -r '.registro.nivel' config.json
info
$ jq -r '.funciones[0]' config.json
pagos
$ jq -r '.funciones | length' config.json
2
$ jq '.servidor' config.json
{
  "host": "203.0.113.10",
  "puerto": 8080
}

Para guardar un valor en una variable, la sustitución de órdenes de siempre:

#!/usr/bin/env bash
host=$(jq -r '.servidor.host' config.json)
puerto=$(jq -r '.servidor.puerto' config.json)
echo "Conectando a $host:$puerto"
salidaConectando a 203.0.113.10:8080

Recorrer un array

.[] saca uno a uno los elementos de un array. Detrás, con una tubería de jq (|, dentro de las comillas), eliges qué escribir de cada uno:

terminal$ jq -r '.[].nombre' usuarios.json
Ana López
Bruno Díaz
Carla Ruiz
David "Dani" Gil
$ jq -r '.[] | "\(.id): \(.email)"' usuarios.json
1: ana@example.com
2: bruno@example.com
3: carla@example.com
4: dani@example.com
$ jq 'length' usuarios.json
4

La forma \(…) dentro de un texto de jq mete el valor de un campo, como $variable dentro de unas comillas dobles de Bash. Para tener los valores en un array de Bash, usa mapfile con una sustitución de procesos; así cada línea es un elemento, aunque tenga espacios:

#!/usr/bin/env bash
mapfile -t nombres < <(jq -r '.[].nombre' usuarios.json)

echo "Hay ${#nombres[@]} usuarios"
for n in "${nombres[@]}"; do
    echo "- $n"
done
salidaHay 4 usuarios
- Ana López
- Bruno Díaz
- Carla Ruiz
- David "Dani" Gil
No recorras la salida de jq con for n in $(jq …): Bash partiría «Ana López» en dos palabras. mapfile o un while IFS= read -r mantienen cada línea entera.

Filtrar, ordenar y agrupar

select(condición) deja pasar solo los elementos que la cumplen. Con map, sort_by y group_by se hace casi todo lo que harías con una hoja de cálculo:

terminal$ jq -r '.[] | select(.activo) | .nombre' usuarios.json
Ana López
Carla Ruiz
David "Dani" Gil
$ jq -r '.[] | select(.pedidos >= 5) | "\(.nombre) (\(.pedidos))"' usuarios.json
Ana López (12)
Carla Ruiz (7)
$ jq -r 'sort_by(.pedidos) | reverse | .[0].nombre' usuarios.json
Ana López
$ jq 'map(.pedidos) | add' usuarios.json
22
$ jq -c 'group_by(.rol) | map({rol: .[0].rol, usuarios: length})' usuarios.json
[{"rol":"admin","usuarios":1},{"rol":"cliente","usuarios":2},{"rol":"soporte","usuarios":1}]

La opción -c escribe el resultado en una sola línea, útil para registros o para pasarlo a otra orden.

Pasar variables de Bash a jq

Este es el error más típico: meter una variable de Bash dentro del filtro. Si el valor tiene comillas o espacios, el filtro se rompe, y si viene del usuario, puede cambiar lo que hace jq. La forma correcta es pasarla con --arg (siempre como texto) o --argjson (como número, booleano u objeto), y usarla dentro como $nombre:

#!/usr/bin/env bash
rol=cliente
minimo=5

jq -r --arg rol "$rol" '.[] | select(.rol == $rol) | .nombre' usuarios.json
jq -r --argjson min "$minimo" '.[] | select(.pedidos >= $min) | .email' usuarios.json
salidaBruno Díaz
Carla Ruiz
ana@example.com
carla@example.com

Crear JSON sin romper las comillas

Montar un JSON pegando textos con echo parece fácil hasta que un valor trae unas comillas. Mira lo que pasa con un nombre como el del usuario 4:

#!/usr/bin/env bash
nombre='David "Dani" Gil'
email='dani@example.com'

# MAL: el JSON se arma a mano
echo "{\"nombre\": \"$nombre\", \"email\": \"$email\"}" > roto.json
jq . roto.json
salida con errorjq: parse error: Invalid numeric literal at line 1, column 24
código de salida: 5

Las comillas de «Dani» cierran el texto antes de tiempo y el resultado ya no es JSON. Con jq -n (sin archivo de entrada) y --arg, jq se encarga de escapar todo lo que haga falta:

#!/usr/bin/env bash
nombre='David "Dani" Gil'
email='dani@example.com'
pedidos=0

jq -n --arg nombre "$nombre" --arg email "$email" --argjson pedidos "$pedidos" \
    '{nombre: $nombre, email: $email, pedidos: $pedidos, activo: true}'
salida{
  "nombre": "David \"Dani\" Gil",
  "email": "dani@example.com",
  "pedidos": 0,
  "activo": true
}

Modificar un archivo JSON

jq no cambia el archivo: escribe el resultado en la salida. Para modificarlo, escribe en un archivo temporal y, solo si jq acabó bien, sustituye el original. Nunca redirijas al mismo archivo que lees (jq … config.json > config.json): la shell lo vacía antes de que jq lo lea y lo pierdes.

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

tmp=$(mktemp)
jq '.servidor.puerto = 9090 | .funciones += ["avisos"]' config.json > "$tmp"
mv "$tmp" config.json

jq -c '{puerto: .servidor.puerto, funciones}' config.json
salida{"puerto":9090,"funciones":["pagos","informes","avisos"]}

De JSON a CSV

Los formatos @csv y @tsv convierten un array en una línea de CSV o separada por tabuladores, con las comillas bien puestas:

#!/usr/bin/env bash
echo "id,nombre,email,pedidos"
jq -r '.[] | [.id, .nombre, .email, .pedidos] | @csv' usuarios.json
salidaid,nombre,email,pedidos
1,"Ana López","ana@example.com",12
2,"Bruno Díaz","bruno@example.com",3
3,"Carla Ruiz","carla@example.com",7
4,"David ""Dani"" Gil","dani@example.com",0

Errores y códigos de salida

Si el JSON no es válido, jq acaba con un código distinto de 0 y explica dónde está el problema. Si el campo no existe, en cambio, escribe null y acaba bien, algo que suele colarse en los scripts. La opción -e hace que null y false den también código de salida 1:

#!/usr/bin/env bash
echo '{"a": 1,}' > malo.json
jq . malo.json
echo "JSON inválido: código $?"

jq -r '.servidor.usuario' config.json
echo "campo que no existe: código $?"

jq -re '.servidor.usuario' config.json
echo "con -e: código $?"

if usuario=$(jq -re '.servidor.usuario' config.json); then
    echo "Usuario: $usuario"
else
    echo "Falta servidor.usuario en config.json" >&2
fi
salidajq: parse error: Expected another key-value pair at line 1, column 9
JSON inválido: código 5
null
campo que no existe: código 0
null
con -e: código 1
Falta servidor.usuario en config.json
  • QuieresFiltro de jq
  • Un campo.servidor.puerto
  • Un texto sin comillasjq -r '.registro.nivel'
  • Todos los elementos.[], .[].nombre
  • Filtrar.[] | select(.activo and .pedidos > 5)
  • Contarlength, map(select(.activo)) | length
  • Sumarmap(.pedidos) | add
  • Ordenarsort_by(.campo), reverse
  • Valor por omisión.servidor.usuario // "admin"
  • Variable de Bash--arg x "$x" y $x dentro
  • A CSV[.a, .b] | @csv

Todos los scripts de esta página se ejecutaron con Bash 5.3 en Ubuntu 26.04, en un contenedor limpio sin acceso a la red, y pasaron ShellCheck 0.11 sin avisos antes de publicarla; las salidas están copiadas de esa ejecución.