Llevaba tiempo dándole vueltas a cómo monitorizar mis servicios sin tener que abrir veinte pestañas del navegador. Cada vez que quería saber el estado de un despliegue, consultar un issue o lanzar una notificación, terminaba con diez terminales abiertas y el caos asegurado. Y al final la solución era más simple de lo que pensaba: Bash, curl y jq. Nada de Python, nada de Node. Lo que ya tenía en el servidor desde el primer día. Como te comenté en el capítulo anterior, cuando dominas funciones, arrays y señales, el siguiente paso natural es salir al mundo exterior. Tus scripts ya saben organizarse internamente, pero llega el momento de que hablen con otros servicios.
¿Y si pudieras consultar el estado de tus servicios desde un script de monitorización? ¿Notificar incidentes a Slack o Telegram? ¿Crear issues en GitHub automáticamente? Todo eso es posible con Bash, curl y jq.
Sobre API REST
El 90% de los servicios modernos exponen una API REST. Tu dashboard de infraestructura, el CI/CD, el sistema de monitorización, el chat del equipo, el proveedor de cloud… todos hablan HTTP. Saber consumir APIs desde Bash es la habilidad que convierte a un administrador de sistemas en un automatizador.
Y lo mejor es que no necesitas Python, Node ni Go. Bash + curl + jq es un stack ubicuo: está en cualquier servidor Linux, en cualquier contenedor mínimo y no requiere instalar runtime alguno. Si puedes hacer SSH a una máquina, puedes consumir APIs desde ella.
Una API REST no es más que un intercambio de documentos HTTP. Bash habla HTTP con curl, parsea la respuesta con jq, y orquesta la lógica con sus capacidades nativas. No hay magia, solo HTTP y JSON.
curl, el cuchillo suizo HTTP
curl es la herramienta por excelencia para transferir datos con URLs. Soporta HTTP, HTTPS, FTP, SFTP, SCP, LDAP y más de 20 protocolos. Para APIs REST usarás casi exclusivamente HTTP y HTTPS.
Opciones esenciales
Aquí tienes las opciones que usarás en el 99% de las llamadas:
-s(–silent): suprime la barra de progreso y mensajes de error-S(–show-error): muestra errores aunque estés en modo silencioso-o(–output): guarda la respuesta en un archivo-w(–write-out): muestra información adicional tras la respuesta-X(–request): método HTTP: GET, POST, PUT, DELETE, PATCH-H(–header): añade una cabecera HTTP-d(–data): envía datos en el cuerpo de la petición-k(–insecure): omite verificación SSL, úsalo con cuidado-L(–location): sigue redirecciones--connect-timeout: tiempo máximo para establecer conexión--max-time: tiempo máximo total para la operación-f(–fail): falla silenciosamente en errores HTTP-i(–include): incluye las cabeceras HTTP en la salida-v(–verbose): modo verboso para depuración
# Todas las protecciones activadas
curl -sSfL --connect-timeout 10 --max-time 30 \
-H "Authorization: Bearer ***" \
"https://api.github.com/repos/user/repo"
GET con parámetros URL
# Parámetros directamente en la URL
curl -s "https://api.github.com/search/repositories?q=bash&sort=stars&per_page=5"
# Con variables
QUERY="bash"
SORT="stars"
PER_PAGE=5
curl -s "https://api.github.com/search/repositories?q=${QUERY}&sort=${SORT}&per_page=${PER_PAGE}"
# Usando -G y -d para construir la URL limpiamente
curl -s -G "https://api.github.com/search/repositories" \
-d "q=bash" \
-d "sort=stars" \
-d "per_page=5"
La forma con -G y -d es más legible y evita problemas con caracteres especiales en la URL. Te recomiendo que la uses siempre que tengas varios parámetros.
POST con cuerpo JSON
# Enviar JSON como string
curl -s -X POST "https://api.github.com/repos/user/repo/issues" \
-H "Authorization: Bearer ***" \
-H "Content-Type: application/json" \
-d '{"title":"Bug en producción","body":"Descripción del bug","labels":["bug"]}'
# Con datos en archivo
cat > /tmp/issue.json <<EOF
{
"title": "Bug en producción",
"body": "Descripción detallada",
"labels": ["bug", "urgente"]
}
EOF
curl -s -X POST "https://api.github.com/repos/user/repo/issues" \
-H "Authorization: Bearer ***" \
-H "Content-Type: application/json" \
-d @/tmp/issue.json
Usar @archivo es preferible cuando el JSON es largo o se genera dinámicamente. Esto te puede ahorrar más de un disgusto con caracteres escapados incorrectamente.
Autenticación: Bearer y Basic
# Bearer Token (el más común en APIs modernas)
curl -s -H "Authorization: Bearer ***" "https://api.ejemplo.com/recurso"
# Basic Auth
curl -s -u "usuario:contraseña" "https://api.ejemplo.com/recurso"
# Basic Auth con token codificado manualmente
TOKEN_B64=$(echo -n "usuario:contraseña" | base64)
curl -s -H "Authorization: Basic $TOKEN_B64" "https://api.ejemplo.com/recurso"
# API Key en cabecera
curl -s -H "X-API-Key: $API_KEY" "https://api.ejemplo.com/recurso"
Manejo de códigos HTTP con -w
-w permite extraer metadatos de la respuesta. La opción más útil es extraer el código HTTP:
# Extraer solo el código HTTP
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "https://api.github.com/users/nousresearch")
echo "Código HTTP: $HTTP_CODE"
# Patrón completo para validar respuesta
RESPONSE=$(mktemp)
HTTP_CODE=$(curl -s -o "$RESPONSE" -w "%{http_code}" \
-H "Authorization: Bearer ***" \
"https://api.github.com/repos/user/repo")
if [[ "$HTTP_CODE" != "200" ]]; then
echo "Error $HTTP_CODE: $(cat "$RESPONSE")" >&2
rm -f "$RESPONSE"
exit 1
fi
cat "$RESPONSE"
rm -f "$RESPONSE"
Otras variables útiles de -w:
curl -s -o /dev/null -w "
http_code: %{http_code}
content_type: %{content_type}
time_total: %{time_total}s
time_connect: %{time_connect}s
size_download: %{size_download} bytes
url_effective: %{url_effective}
" "https://api.github.com"
jq, tu navaja para JSON
jq es un procesador JSON de línea de comandos. Es a JSON lo que sed es a texto, pero con conciencia de la estructura del documento. Sin jq, parsear JSON en Bash es una pesadilla de grep y sed frágil. Con jq, es trivial.
Instalación
# Debian/Ubuntu
apt install jq
# RHEL/CentOS/Fedora
dnf install jq
# macOS
brew install jq
# Desde el código fuente
curl -sL "https://github.com/jqlang/jq/releases/latest/download/jq-linux-amd64" -o /usr/local/bin/jq
chmod +x /usr/local/bin/jq
Conceptos básicos
# Supongamos esta respuesta JSON
JSON='{
"name": "scripts-en-bash",
"stars": 42,
"owner": {"login": "atareao", "id": 12345},
"tags": ["bash", "tutorial", "linux"],
"contributors": [
{"name": "Lorenzo", "contributions": 30},
{"name": "Ana", "contributions": 15}
]
}'
# . — el documento completo
echo "$JSON" | jq '.'
# .campo — acceder a un campo
echo "$JSON" | jq '.name' # "scripts-en-bash"
echo "$JSON" | jq '.stars' # 42
# .campo.anidado
echo "$JSON" | jq '.owner.login' # "atareao"
echo "$JSON" | jq '.owner.id' # 12345
# .[]? — iterar sobre arrays (con ? para null safety)
echo "$JSON" | jq '.tags[]' # "bash", "tutorial", "linux"
echo "$JSON" | jq '.contributors[].name' # "Lorenzo", "Ana"
select(), filtrar elementos
# Seleccionar contributors con más de 20 contribuciones
echo "$JSON" | jq '.contributors[] | select(.contributions > 20)'
# {"name": "Lorenzo", "contributions": 30}
# Combinar condiciones
echo "$JSON" | jq '.contributors[] | select(.contributions > 10 and .name != "Lorenzo")'
map(), transformar arrays
# Extraer solo los nombres
echo "$JSON" | jq '[.contributors[] | .name]'
# ["Lorenzo", "Ana"]
# Mapear a un nuevo formato
echo "$JSON" | jq '[.contributors[] | {nombre: .name, aportes: .contributions}]'
length, contar elementos
echo "$JSON" | jq '.tags | length' # 3
echo "$JSON" | jq '.contributors | length' # 2
echo "$JSON" | jq '.name | length' # 14 (caracteres)
-r, raw output sin comillas
La opción -r (–raw-output) elimina las comillas alrededor de strings. Es esencial cuando asignas valores a variables de Bash:
# Sin -r: el valor incluye comillas
NOMBRE=$(echo "$JSON" | jq '.name')
echo "$NOMBRE" # "scripts-en-bash" (con comillas incluidas)
# Con -r: valor limpio
NOMBRE=$(echo "$JSON" | jq -r '.name')
echo "$NOMBRE" # scripts-en-bash
# Ejemplo práctico con API real
REPO_NAME=$(curl -s "https://api.github.com/repos/atareao/scripts-en-bash" | jq -r '.name')
STARS=$(curl -s "https://api.github.com/repos/atareao/scripts-en-bash" | jq -r '.stargazers_count')
echo "Repositorio: $REPO_NAME — Estrellas: $STARS"
Arrays y objetos anidados, casos reales
# API de GitHub: lista de repositorios
RESP=$(curl -s "https://api.github.com/users/atareao/repos?per_page=5")
# Extraer nombres y estrellas
echo "$RESP" | jq -r '.[] | "\(.name) — \(.stargazers_count) estrellas"'
# Filtrar repos con más de 10 estrellas
echo "$RESP" | jq -r '.[] | select(.stargazers_count > 10) | .name'
# Tabla formateada
echo "$RESP" | jq -r '.[] | [.name, .language // "N/A", .stargazers_count | tostring] | join(" | ")'
El operador // es el operador alternativo de jq: devuelve el valor de la izquierda si no es null, o el de la derecha si lo es.
Patrones profesionales
Reintentos con exponential backoff
Cuando una API falla por rate limit, timeout o error temporal, reintentar inmediatamente suele empeorar las cosas. El patrón profesional es exponential backoff: esperar 1s, luego 2s, luego 4s, luego 8s, hasta un máximo.
# Función de exponential backoff genérica
api_call_with_backoff() {
local url=$1
local max_retries=${2:-5}
local max_wait=${3:-60}
local wait_time=1
local attempt=1
while (( attempt <= max_retries )); do
echo " Intento $attempt/$max_retries..." >&2
RESPONSE=$(mktemp)
HTTP_CODE=$(curl -s -o "$RESPONSE" -w "%{http_code}" "$url")
if [[ "$HTTP_CODE" == "200" || "$HTTP_CODE" == "201" ]]; then
cat "$RESPONSE"
rm -f "$RESPONSE"
return 0
fi
# Si es 4xx (error de cliente), no tiene sentido reintentar
if [[ "$HTTP_CODE" =~ ^4[0-9][0-9]$ ]] && [[ "$HTTP_CODE" != "429" ]]; then
echo "Error $HTTP_CODE (cliente) — no se reintenta" >&2
cat "$RESPONSE" >&2
rm -f "$RESPONSE"
return 1
fi
rm -f "$RESPONSE"
# Si es 429 (rate limit), usar cabecera Retry-After si existe
if [[ "$HTTP_CODE" == "429" ]]; then
local retry_after
retry_after=$(curl -sI "$url" | grep -i "retry-after" | awk '{print $2}' | tr -d '\r')
if [[ -n "$retry_after" && "$retry_after" -gt "$wait_time" ]]; then
wait_time=$retry_after
fi
fi
echo " Error $HTTP_CODE — esperando ${wait_time}s..." >&2
sleep "$wait_time"
# Exponential backoff: doblar tiempo, con tope
wait_time=$(( wait_time * 2 ))
if (( wait_time > max_wait )); then
wait_time=$max_wait
fi
((attempt++))
done
echo "Error: agotados $max_retries reintentos" >&2
return 1
}
Rate limiting con sleep
Muchas APIs imponen un límite de peticiones por minuto u hora. GitHub permite 60 peticiones por hora sin autenticar y 5000 autenticado. El patrón consiste en leer las cabeceras de rate limit y esperar si es necesario.
# Función que respeta rate limits
rate_limited_call() {
local url=$1
local response
local http_code
response=$(mktemp)
http_code=$(curl -s -o "$response" -w "%{http_code}" \
-H "Authorization: Bearer ***" \
"$url")
# Leer cabeceras de rate limit (una sola llamada HEAD)
local headers remaining limit reset
headers=$(curl -sI -H "Authorization: Bearer ***" "$url")
remaining=$(echo "$headers" | grep -i "x-ratelimit-remaining" | awk '{print $2}' | tr -d '\r')
limit=$(echo "$headers" | grep -i "x-ratelimit-limit" | awk '{print $2}' | tr -d '\r')
reset=$(echo "$headers" | grep -i "x-ratelimit-reset" | awk '{print $2}' | tr -d '\r')
if (( remaining < 5 )); then
local wait_time=$(( reset - $(date +%s) + 1 ))
echo "Rate limit próximo ($remaining/$limit). Esperando ${wait_time}s..." >&2
sleep "$wait_time"
fi
cat "$response"
rm -f "$response"
return 0
}
Paginación con bucles
Muchas APIs devuelven resultados paginados. GitHub usa Link headers; otras APIs usan page y per_page en la URL.
# Paginación estilo GitHub (Link header + page param)
fetch_all_pages_github() {
local base_url=$1
local page=1
local per_page=100
local all_data="[]"
while true; do
echo "Obteniendo página $page..." >&2
response=$(mktemp)
http_code=$(curl -s -o "$response" -w "%{http_code}" \
-H "Authorization: Bearer ***" \
"${base_url}?per_page=${per_page}&page=${page}")
if [[ "$http_code" != "200" ]]; then
echo "Error $http_code en página $page" >&2
rm -f "$response"
break
fi
local data
data=$(cat "$response")
rm -f "$response"
local count
count=$(echo "$data" | jq 'length')
if (( count == 0 )); then
break
fi
all_data=$(echo "$all_data" "$data" | jq -s 'add')
local link_header
link_header=$(curl -sI -H "Authorization: Bearer ***" \
"${base_url}?per_page=${per_page}&page=${page}" | \
grep -i "^link:" || true)
if ! echo "$link_header" | grep -q 'rel="next"'; then
break
fi
((page++))
done
echo "$all_data"
}
Subida de archivos con -F
Para APIs que aceptan multipart/form-data:
# Subir un archivo
curl -s -X POST "https://api.ejemplo.com/upload" \
-H "Authorization: Bearer ***" \
-F "archivo=@/ruta/al/archivo.pdf"
# Con campo adicional
curl -s -X POST "https://api.ejemplo.com/upload" \
-H "Authorization: Bearer ***" \
-F "archivo=@/ruta/al/archivo.pdf" \
-F "descripcion=Informe mensual"
# Cambiar el nombre del archivo en el multipart
curl -s -F "archivo=@/ruta/local.pdf;filename=informe_final.pdf" \
"https://api.ejemplo.com/upload"
Llamadas paralelas con xargs
Cuando necesitas consultar múltiples endpoints, las llamadas secuenciales son lentas. xargs -P lanza procesos en paralelo:
# Consultar el estado de varios servicios en paralelo
SERVICIOS=(
"https://api.github.com"
"https://api.github.com/users/atareao"
"https://api.github.com/repos/atareao/scripts-en-bash"
)
# xargs -P N: lanza hasta N procesos simultáneos
printf "%s\n" "${SERVICIOS[@]}" | \
xargs -I {} -P 5 bash -c '
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "{}")
NOMBRE=$(echo "{}" | sed "s|https://api.github.com||")
echo "$NOMBRE → $HTTP_CODE"
'
Para un control más fino con timeout por llamada y manejo de errores individual:
check_endpoint() {
local url=$1
local timeout=${2:-10}
local start end elapsed
start=$(date +%s%N)
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
--max-time "$timeout" "$url" 2>/dev/null || echo "000")
end=$(date +%s%N)
elapsed=$(( (end - start) / 1000000 ))
echo "$url|$HTTP_CODE|${elapsed}ms"
}
export -f check_endpoint
# Lanzar 10 checks en paralelo
printf "%s\n" "${ENDPOINTS[@]}" | xargs -I {} -P 10 bash -c 'check_endpoint "{}"'
/dev/tcp, curl sin curl
Bash tiene una característica poco conocida: puede abrir conexiones TCP directamente con /dev/tcp. No necesitas curl para peticiones HTTP simples, aunque tiene sus limitaciones:
# GET request sin curl (HTTP/1.0 plano, sin SSL)
exec 3<>/dev/tcp/example.com/80
echo -e "GET / HTTP/1.0\r\nHost: example.com\r\n\r\n" >&3
cat <&3
exec 3>&-
Para conexiones HTTPS necesitas openssl, pero entonces ya no es tan «sin curl»:
# GET vía HTTPS con openssl
echo -e "GET /users/atareao HTTP/1.1\r\nHost: api.github.com\r\nConnection: close\r\n\r\n" | \
openssl s_client -quiet -connect api.github.com:443 2>/dev/null | head -20
Cuidado con esta técnica: no maneja HTTPS directamente, el parseo de respuesta es manual y es poco portable. Usa curl siempre que esté disponible. /dev/tcp es un buen recurso en contenedores ultra-mínimos donde no cabe ni curl.
APIs reales desde Bash
GitHub API, listar repositorios y crear issues
#!/bin/bash
# github_api.sh
GITHUB_TOKEN="${GITHUB_TOKEN:-}"
[[ -z "$GITHUB_TOKEN" ]] && { echo "Define GITHUB_TOKEN" >&2; exit 1; }
GITHUB_API="https://api.github.com"
USER="atareao"
# Listar repositorios del usuario
echo "=== Repositorios de $USER ==="
curl -s -H "Authorization: Bearer ***" \
"${GITHUB_API}/users/${USER}/repos?per_page=5&sort=updated" | \
jq -r '.[] | "\(.name) — \(.stargazers_count) estrellas — \(.forks_count) forks — \(.language // "N/A")"'
# Crear un issue
crear_issue() {
local repo=$1
local title=$2
local body=$3
local labels=${4:-"[]"}
echo "Creando issue en $repo: $title"
RESPONSE=$(mktemp)
HTTP_CODE=$(curl -s -o "$RESPONSE" -w "%{http_code}" \
-X POST "${GITHUB_API}/repos/${USER}/${repo}/issues" \
-H "Authorization: Bearer ***" \
-H "Content-Type: application/json" \
-d "$(jq -n --arg t "$title" --arg b "$body" --argjson l "$labels" \
'{title: $t, body: $b, labels: $l}')")
if [[ "$HTTP_CODE" == "201" ]]; then
local issue_url
issue_url=$(jq -r '.html_url' "$RESPONSE")
echo "Issue creado: $issue_url"
else
echo "Error $HTTP_CODE: $(cat "$RESPONSE")" >&2
return 1
fi
rm -f "$RESPONSE"
}
Weather API, clima desde terminal
No necesitas API key para consultar el clima. wttr.in es un servicio que devuelve el tiempo en varios formatos:
# Clima actual en JSON
curl -s "https://wttr.in/Madrid?format=j1" | \
jq -r '.current_condition[0] |
"\(.temp_C)°C — \(.weatherDesc[0].value) — Humedad \(.humidity)% — Viento \(.windspeedKmph) km/h"'
# Pronóstico resumido
curl -s "wttr.in/Buenos+Aires?format=%l:+%c+%t+%w+%h"
# Pronóstico para los próximos 3 días
curl -s "wttr.in/London?format=j1" | \
jq -r '.weather[] |
"\(.date): Max \(.maxtempC)°C / Min \(.mintempC)°C — \(.hourly[0].weatherDesc[0].value)"'
Telegram Bot, enviar mensajes desde el servidor
#!/bin/bash
# telegram.sh
TELEGRAM_TOKEN="${TELEGRAM_TOKEN:-}"
CHAT_ID="${CHAT_ID:-}"
[[ -z "$TELEGRAM_TOKEN" || -z "$CHAT_ID" ]] && {
echo "Define TELEGRAM_TOKEN y CHAT_ID" >&2
exit 1
}
enviar_telegram() {
local mensaje=$1
local parse_mode=${2:-"Markdown"}
RESPONSE=$(mktemp)
HTTP_CODE=$(curl -s -o "$RESPONSE" -w "%{http_code}" \
-X POST "https://api.telegram.org/bot${TELEGRAM_TOKEN}/sendMessage" \
-H "Content-Type: application/json" \
-d "$(jq -n --arg chat "$CHAT_ID" --arg text "$mensaje" --arg mode "$parse_mode" \
'{chat_id: $chat, text: $text, parse_mode: $mode}')")
if [[ "$HTTP_CODE" != "200" ]]; then
echo "Error al enviar mensaje: $(cat "$RESPONSE")" >&2
rm -f "$RESPONSE"
return 1
fi
rm -f "$RESPONSE"
}
# Enviar notificación
enviar_telegram "Alerta en servidor $(hostname)
- Disco: 85% usado
- Memoria: 3.2 GB libres
- CPU: carga media 2.5"
# Enviar archivo
enviar_documento() {
local archivo=$1
local caption=${2:-""}
curl -s -X POST "https://api.telegram.org/bot${TELEGRAM_TOKEN}/sendDocument" \
-F "chat_id=$CHAT_ID" \
-F "document=@$archivo" \
-F "caption=$caption"
}
Webhooks, disparar eventos
Los webhooks son callbacks HTTP: cuando algo ocurre, haces un POST a una URL.
# Disparar webhook genérico
disparar_webhook() {
local url=$1
local event_type=$2
local payload=${3:-"{}"}
curl -s -X POST "$url" \
-H "Content-Type: application/json" \
-H "X-Event-Type: $event_type" \
-H "X-Source: $(hostname)" \
-d "$payload"
}
# Notificar a Slack via webhook
SLACK_WEBHOOK_URL="https://hooks.slack.com/services/T00/B00/XXXXX"
notificar_slack() {
local mensaje=$1
local nivel=${2:-"info"}
local color
case "$nivel" in
error) color="#FF0000" ;;
warning) color="#FFA500" ;;
success) color="#00FF00" ;;
*) color="#808080" ;;
esac
curl -s -X POST "$SLACK_WEBHOOK_URL" \
-H "Content-Type: application/json" \
-d "$(jq -n --arg msg "$mensaje" --arg col "$color" \
'{
attachments: [{
color: $col,
text: $msg,
footer: "Servidor: \(env.HOSTNAME // "desconocido")",
ts: (now | floor)
}]
}')"
}
Funciones profesionales
Construyamos una librería REST completa para Bash. Estas funciones te servirán en cualquier script que interactúe con APIs:
#!/bin/bash
# lib-rest.sh
# Configuración global
REST_TIMEOUT=30
REST_CONNECT_TIMEOUT=10
REST_RETRIES=3
REST_BASE_URL=""
REST_TOKEN=""
REST_VERBOSE=false
# Función base
_rest_request() {
local method=$1
local endpoint=$2
local data=${3:-}
local content_type=${4:-"application/json"}
local url="${REST_BASE_URL}${endpoint}"
local response
local http_code
response=$(mktemp)
local curl_args=(
-s -o "$response"
-w "%{http_code}"
-X "$method"
--connect-timeout "$REST_CONNECT_TIMEOUT"
--max-time "$REST_TIMEOUT"
)
[[ -n "$REST_TOKEN" ]] && curl_args+=(-H "Authorization: Bearer $REST_TOKEN")
curl_args+=(-H "Content-Type: $content_type")
[[ -n "$data" ]] && curl_args+=(-d "$data")
curl_args+=(-H "User-Agent: bash-rest-client/1.0")
$REST_VERBOSE && curl_args+=(-v)
http_code=$(curl "${curl_args[@]}" "$url" 2>/dev/null)
echo "${http_code}|$(cat "$response" | base64 -w0)"
rm -f "$response"
}
# rest_get, rest_post, rest_put, rest_delete
rest_get() {
local endpoint=$1
local result
result=$(_rest_request "GET" "$endpoint")
local http_code="${result%%|*}"
local body=$(echo "${result#*|}" | base64 -d 2>/dev/null || echo "")
if [[ "$http_code" != "200" ]]; then
echo "Error $http_code en GET $endpoint" >&2
echo "$body" >&2
return 1
fi
echo "$body"
}
rest_post() {
local endpoint=$1
local data=$2
local result
result=$(_rest_request "POST" "$endpoint" "$data")
local http_code="${result%%|*}"
local body=$(echo "${result#*|}" | base64 -d 2>/dev/null || echo "")
if [[ "$http_code" != "201" && "$http_code" != "200" ]]; then
echo "Error $http_code en POST $endpoint" >&2
echo "$body" >&2
return 1
fi
echo "$body"
}
rest_put() {
local endpoint=$1
local data=$2
local result
result=$(_rest_request "PUT" "$endpoint" "$data")
local http_code="${result%%|*}"
local body=$(echo "${result#*|}" | base64 -d 2>/dev/null || echo "")
if [[ "$http_code" != "200" && "$http_code" != "204" ]]; then
echo "Error $http_code en PUT $endpoint" >&2
echo "$body" >&2
return 1
fi
echo "$body"
}
rest_delete() {
local endpoint=$1
local result
result=$(_rest_request "DELETE" "$endpoint")
local http_code="${result%%|*}"
local body=$(echo "${result#*|}" | base64 -d 2>/dev/null || echo "")
if [[ "$http_code" != "204" && "$http_code" != "200" && "$http_code" != "202" ]]; then
echo "Error $http_code en DELETE $endpoint" >&2
echo "$body" >&2
return 1
fi
echo "$body"
}
# fetch_paginated — obtiene todas las páginas
fetch_paginated() {
local base_endpoint=$1
local per_page=${2:-100}
local page=1
local all_data="[]"
while true; do
local endpoint="${base_endpoint}?per_page=${per_page}&page=${page}"
local result
echo "Obteniendo página $page..." >&2
result=$(rest_get "$endpoint") || break
local count=$(echo "$result" | jq 'length' 2>/dev/null || echo "0")
(( count == 0 )) && break
all_data=$(echo "$all_data" "$result" | jq -s 'add')
(( count < per_page )) && break
((page++))
done
echo "$all_data"
}
# api_with_retry — ejecuta con reintentos
api_with_retry() {
local rest_func=$1
local endpoint=$2
local data=${3:-}
local max_retries=${4:-3}
local base_wait=2
local attempt=1
local result
while (( attempt <= max_retries )); do
echo "Llamada $attempt/$max_retries a $endpoint..." >&2
if [[ -n "$data" ]]; then
result=$($rest_func "$endpoint" "$data") && { echo "$result"; return 0; }
else
result=$($rest_func "$endpoint") && { echo "$result"; return 0; }
fi
local wait_time=$(( base_wait * (2 ** (attempt - 1)) ))
echo " Reintentando en ${wait_time}s..." >&2
sleep "$wait_time"
((attempt++))
done
echo "Error: agotados $max_retries reintentos en $endpoint" >&2
return 1
}
Dale caña a esta librería. Es la base que usarás una y otra vez.
Errores comunes
Error 1: No verificar el código HTTP
# Mal: asumes que la respuesta es siempre 200
RESP=$(curl -s "https://api.github.com/users/noexisto")
echo "$RESP" | jq '.name' # Muestra: null
# Bien: siempre verifica el código HTTP
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "https://api.github.com/users/noexisto")
if [[ "$HTTP_CODE" == "200" ]]; then
echo "$RESP" | jq '.name'
else
echo "Error $HTTP_CODE"
fi
Error 2: Olvidar -s en scripts automatizados
# Mal: la barra de progreso aparece en logs
curl "https://api.github.com" > respuesta.json
# Bien: silencio absoluto
curl -s "https://api.github.com" > respuesta.json
Error 3: No escapar variables en el JSON
# Mal: si $TITLE contiene comillas, el JSON se rompe
curl -d "{\"title\":\"$TITLE\"}" "https://api.ejemplo.com"
# Bien: usa jq para construir JSON seguro
DATA=$(jq -n --arg t "$TITLE" '{title: $t}')
curl -d "$DATA" "https://api.ejemplo.com"
Error 4: Olvidar el Content-Type en POST
# Mal: el servidor no interpreta el body como JSON
curl -X POST -d '{"key":"value"}' "https://api.ejemplo.com"
# Bien: Content-Type explícito
curl -X POST -H "Content-Type: application/json" -d '{"key":"value"}' "https://api.ejemplo.com"
Error 5: Ignorar rate limiting
# Mal: 100 peticiones seguidas, te bloquean
for i in {1..100}; do
curl -s "https://api.github.com/users/user"
done
# Bien: con pausa y control
for i in {1..100}; do
curl -s "https://api.github.com/users/user"
sleep 1
done
Error 6: Ignorar timeouts
# Mal: curl espera indefinidamente
curl -s "https://api.lenta.com/recurso"
# Bien: timeout máximo
curl -s --connect-timeout 10 --max-time 30 "https://api.lenta.com/recurso"
Error 7: No usar -L para seguir redirecciones
# Mal: HTTP 301/302 sin seguir, respuesta vacía
curl -s "http://api.github.com" | jq '.'
# Bien: sigue la redirección
curl -sL "http://api.github.com" | jq '.'
Error 8: No validar la respuesta JSON
# Mal: si la API devuelve HTML, jq falla
curl -s "https://api.ejemplo.com/error" | jq '.data'
# Bien: validar antes de procesar
RESP=$(curl -s "https://api.ejemplo.com/error")
if echo "$RESP" | jq -e '.' > /dev/null 2>&1; then
echo "$RESP" | jq '.data'
else
echo "Respuesta no JSON: $RESP"
fi
Error 9: Exponer tokens en el historial
# Mal: el token queda en ~/.bash_history
curl -s -H "Authorization: Bearer ***" "https://api.ejemplo.com"
# Bien: usar variable de entorno
curl -s -H "Authorization: Bearer $TOKEN" "https://api.ejemplo.com"
Error 10: No limpiar archivos temporales
# Mal: archivos temporales quedan en /tmp
RESP=$(mktemp)
curl -s -o "$RESP" "https://api.ejemplo.com"
# No borras $RESP
# Bien: limpieza garantizada con trap
RESP=$(mktemp)
trap 'rm -f "$RESP"' EXIT
curl -s -o "$RESP" "https://api.ejemplo.com"
Para terminar
Consumir APIs REST desde Bash no es un parche temporal. Es una habilidad fundamental para cualquier persona que administre sistemas Linux. Con curl y jq tienes un stack completo para interactuar con cualquier servicio HTTP desde la línea de comandos.
Si te llevas algo de este capítulo, que sean estos principios:
- Siempre verifica el código HTTP. No asumas que la API responde correctamente.
- Usa jq para construir JSON. No concatenes strings. jq –arg es seguro contra inyección.
- Respeta los rate limits. Lee las cabeceras de límite, espera cuando sea necesario.
- Añade un User-Agent, usa timeouts razonables, reintenta con backoff.
- Nunca expongas tokens. Usa variables de entorno o archivos seguros.
- Pon las funciones de este capítulo en un archivo
lib-rest.shy fuentealo. Te ahorrará escribir lo mismo una y otra vez.
El stack Bash + curl + jq es ligero, ubicuo y extraordinariamente potente. La mayoría de las tareas que requieren una librería HTTP en Python o Node se resuelven con una línea de curl y un pipe a jq. Domínalo y cualquier API estará a un script de distancia.
En el próximo capítulo veremos cómo trabajar con bases de datos desde Bash. Porque tus datos no siempre van a llegar en JSON, y a veces lo que necesitas es meterlos en una base.