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.
También te puede interesar
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"
donesalidaHay 4 usuarios - Ana López - Bruno Díaz - Carla Ruiz - David "Dani" Gil
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.jsonsalida 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.jsonsalida{"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
fisalidajq: 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 comillas
jq -r '.registro.nivel' - Todos los elementos
.[],.[].nombre - Filtrar
.[] | select(.activo and .pedidos > 5) - Contar
length,map(select(.activo)) | length - Sumar
map(.pedidos) | add - Ordenar
sort_by(.campo),reverse - Valor por omisión
.servidor.usuario // "admin" - Variable de Bash
--arg x "$x"y$xdentro - 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.