Como te comenté en el capítulo anterior, después de dominar los menús interactivos y el parseo de argumentos, llega el momento de que tus scripts hablen con otros programas. Imagina que tienes un script que monitorea servidores, otro que ejecuta backups cada noche, y un tercero que analiza logs de acceso. ¿Qué haces con los resultados? ¿Los miras en la terminal y luego los olvidas?
La respuesta profesional es generar reportes automáticos en formatos universales: CSV para hojas de cálculo, HTML para visualización en navegador o email, JSON para APIs, Markdown para documentación, y PDF para informes formales.
Bash es el pegamento perfecto para esta tarea. Ya tienes los datos, tienes herramientas nativas como echo, printf, awk, sed y heredocs, y no necesitas dependencias pesadas. Un reporte HTML básico no necesita Python ni PHP.
¿Por qué generar reportes desde Bash?
Imagina que tienes un script que monitorea el estado de tus servidores, otro que ejecuta backups cada noche, y un tercero que analiza logs de acceso. ¿Qué haces con los resultados? ¿Los miras en la terminal y luego los olvidas?
La respuesta profesional es: generar reportes automáticos en formatos universales — CSV para hojas de cálculo, HTML para visualización en navegador o email, JSON para APIs, Markdown para documentación, y PDF para informes formales.
Bash es el pegamento perfecto para esta tarea:
- Ya tienes los datos: tus scripts producen archivos, logs, estadísticas
- Herramientas nativas:
echo,printf,awk,sedy heredocs - Sin dependencias pesadas: un reporte HTML básico no necesita Python ni PHP
- Pipeline UNIX: cada formato es una transformación más del flujo de datos
En este capítulo aprenderás a convertir la salida de cualquier script en reportes profesionales, listos para compartir, archivar o enviar por correo.
Filosofía del capítulo: un reporte no es solo datos crudos — es información presentada de forma clara, accesible y accionable. Bash te da el control total.
CSV desde Bash
echo y printf para campos simples
CSV (Comma-Separated Values) es el formato de intercambio de datos más universal. Cualquier hoja de cálculo (Excel, LibreOffice Calc, Google Sheets) lo abre sin configuración.
La generación más básica es directa:
#!/bin/bash
# reporte_csv_simple.sh
echo "nombre,email,rol"
echo "Ana García,ana@example.com,admin"
echo "Carlos Ruiz,carlos@example.com,editor"
echo "Laura Méndez,laura@example.com,lector"
Esto funciona… hasta que un campo contiene una coma o comillas.
Quoting y escaping de comas
La especificación CSV (RFC 4180) define reglas claras:
- Si un campo contiene comas, saltos de línea o comillas dobles, debe ir entre comillas dobles
- Las comillas dobles dentro del campo se escapan duplicándolas (
"")
#!/bin/bash
# csv_quoting.sh — Escapado correcto
csv_escape() {
local campo="$1"
# Si contiene comas, comillas o saltos de línea, citar y escapar
if [[ "$campo" == *[,\"\n]* ]]; then
# Escapar comillas duplicándolas
local escapado="${campo//\"/\"\"}"
echo "\"$escapado\""
else
echo "$campo"
fi
}
nombre="Pérez & Asociados, S.L."
email="info@perezasoc.com"
notas="Cliente desde 2020; facturación > 10k"
echo "$(csv_escape "$nombre"),$(csv_escape "$email"),$(csv_escape "$notas")"
# → "Pérez & Asociados, S.L.",info@perezasoc.com,"Cliente desde 2020; facturación > 10k"
Valores multilínea y caracteres especiales
Un campo CSV puede contener saltos de línea si va entre comillas:
#!/bin/bash
# csv_multiline.sh
csv_escape() {
local campo="$1"
if [[ "$campo" == *[\",\n\"]* ]]; then
local escapado="${campo//\"/\"\"}"
echo "\"$escapado\""
else
echo "$campo"
fi
}
# Campo con salto de línea
observaciones=$'Primera línea de observaciones\nSegunda línea'
echo "$(csv_escape "$observaciones")"
# → "Primera línea de observaciones
# Segunda línea"
Excel CSV: delimitadores y UTF-8 BOM
Excel tiene peculiaridades que debes conocer:
- BOM (Byte Order Mark): Excel no detecta UTF-8 sin BOM para CSV. Añadir
\xEF\xBB\xBFal inicio fuerza UTF-8 en Excel para Windows. - Delimitador: Excel en español espera punto y coma (
;) como separador según la configuración regional. Puedes usar\;o generar TSV (tab-separated) para evitarlo. - Codificación: UTF-8 sin BOM en macOS/LibreOffice funciona; en Excel Windows requiere BOM.
#!/bin/bash
# csv_excel.sh — CSV compatible con Excel Windows
generar_csv_excel() {
local archivo="${1:-reporte.csv}"
local sep="${2:-,}" # ',' en inglés, ';' en español
# BOM UTF-8 para Excel Windows
printf '\xEF\xBB\xBF' > "$archivo"
# Cabecera
echo "nombre${sep}email${sep}rol" >> "$archivo"
# Datos
echo "Ana García${sep}ana@example.com${sep}admin" >> "$archivo"
echo "Carlos Ruiz${sep}carlos@example.com${sep}editor" >> "$archivo"
echo "✅ CSV generado: $archivo"
}
generar_csv_excel "reporte_excel.csv" ";"
Pro tip: si tu público usa Excel español, usa
;como separador. Para máxima compatibilidad, genera TSV (tab-separated) con\t.
HTML desde Bash
Tablas HTML desde arrays/datos
Generar HTML desde Bash es sorprendentemente simple con heredocs y bucles:
#!/bin/bash
# html_table.sh — Generar tabla HTML desde arrays
TITULO="Reporte de Servidores"
encabezados=("Servidor" "IP" "Estado" "Uptime")
datos=(
"web01 192.168.1.10 ACTIVO 45d"
"db01 192.168.1.20 ACTIVO 120d"
"cache 192.168.1.30 INACTIVO 0d"
"backup 192.168.1.40 ACTIVO 30d"
)
cat << HTML > reporte.html
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<title>$TITULO</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
h1 { color: #333; }
table { border-collapse: collapse; width: 100%; margin-top: 20px; }
th { background: #4CAF50; color: white; padding: 10px; text-align: left; }
td { padding: 8px; border-bottom: 1px solid #ddd; }
tr:hover { background: #f5f5f5; }
.activo { color: green; font-weight: bold; }
.inactivo { color: red; font-weight: bold; }
.footer { margin-top: 20px; font-size: 0.8em; color: #666; }
</style>
</head>
<body>
<h1>$TITULO</h1>
<p>Generado el $(date '+%d/%m/%Y %H:%M')</p>
<table>
<thead><tr>
HTML
# Cabeceras
for h in "${encabezados[@]}"; do
echo " <th>$h</th>" >> reporte.html
done
echo " </tr></thead>" >> reporte.html
echo " <tbody>" >> reporte.html
# Datos
for fila in "${datos[@]}"; do
read -r serv ip estado uptime <<< "$fila"
clase="${estado,,}" # lowercase
echo " <tr>" >> reporte.html
echo " <td>$serv</td>" >> reporte.html
echo " <td>$ip</td>" >> reporte.html
echo " <td class=\"$clase\">$estado</td>" >> reporte.html
echo " <td>$uptime</td>" >> reporte.html
echo " </tr>" >> reporte.html
done
cat << 'HTML' >> reporte.html
</tbody>
</table>
<div class="footer">
Reporte automático · Datos actualizados cada hora
</div>
</body>
</html>
HTML
echo "✅ Reporte HTML generado: reporte.html"
CSS inline styling para emails
Los clientes de correo (Gmail, Outlook) ignoran <style> en el <head>. Para emails, debes aplicar CSS inline en cada elemento:
#!/bin/bash
# html_email_inline.sh — CSS inline para email
generar_html_email() {
local titulo="$1"
shift
local filas=("$@")
cat << HTML
<!DOCTYPE html>
<html>
<body style="font-family: Arial, Helvetica, sans-serif; margin: 0; padding: 20px; background-color: #f4f4f4;">
<div style="max-width: 600px; margin: 0 auto; background: white; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
<div style="background: #4CAF50; padding: 15px; text-align: center;">
<h1 style="color: white; margin: 0; font-size: 20px;">$titulo</h1>
</div>
<div style="padding: 20px;">
<p style="color: #555; font-size: 14px;">Reporte generado el $(date '+%d/%m/%Y %H:%M')</p>
<table style="width: 100%; border-collapse: collapse; margin-top: 15px;">
<tr>
<th style="background: #333; color: white; padding: 8px; text-align: left; font-size: 13px;">Servidor</th>
<th style="background: #333; color: white; padding: 8px; text-align: left; font-size: 13px;">Estado</th>
<th style="background: #333; color: white; padding: 8px; text-align: left; font-size: 13px;">Uptime</th>
</tr>
HTML
for fila in "${filas[@]}"; do
read -r serv estado uptime <<< "$fila"
local color="green"
[[ "$estado" == "INACTIVO" ]] && color="red"
cat << HTML
<tr>
<td style="padding: 8px; border-bottom: 1px solid #ddd; font-size: 13px;">$serv</td>
<td style="padding: 8px; border-bottom: 1px solid #ddd; color: $color; font-weight: bold; font-size: 13px;">$estado</td>
<td style="padding: 8px; border-bottom: 1px solid #ddd; font-size: 13px;">$uptime</td>
</tr>
HTML
done
cat << HTML
</table>
</div>
<div style="background: #eee; padding: 10px; text-align: center; font-size: 11px; color: #888;">
Reporte automático · <a href="#" style="color: #4CAF50;">Ver online</a>
</div>
</div>
</body>
</html>
HTML
}
# Uso
datos=(
"web01 ACTIVO 45d"
"db01 ACTIVO 120d"
"cache INACTIVO 0d"
)
generar_html_email "🚀 Estado de Servidores" "${datos[@]}" > email_reporte.html
Heredoc templates para estructura HTML
Para reportes complejos, separa la plantilla HTML en variables o archivos:
#!/bin/bash
# heredoc_template.sh — Plantilla HTML reutilizable
HTML_HEADER=$(cat << 'HEADER'
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
:root { --primary: #2563eb; --bg: #f8fafc; --text: #1e293b; }
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: system-ui, -apple-system, sans-serif; background: var(--bg); color: var(--text); padding: 2rem; }
.container { max-width: 900px; margin: 0 auto; }
h1 { font-size: 1.5rem; margin-bottom: 0.5rem; color: var(--primary); }
.meta { color: #64748b; font-size: 0.875rem; margin-bottom: 1.5rem; }
table { width: 100%; border-collapse: collapse; background: white; border-radius: 8px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
th { background: var(--primary); color: white; padding: 12px; text-align: left; font-size: 0.875rem; }
td { padding: 10px 12px; border-bottom: 1px solid #e2e8f0; font-size: 0.875rem; }
tr:last-child td { border-bottom: none; }
tr:hover { background: #f1f5f9; }
.ok { color: #16a34a; font-weight: 600; }
.warn { color: #d97706; font-weight: 600; }
.err { color: #dc2626; font-weight: 600; }
.footer { margin-top: 1.5rem; padding-top: 1rem; border-top: 1px solid #e2e8f0; font-size: 0.75rem; color: #94a3b8; }
</style>
</head>
<body>
<div class="container">
HEADER
)
HTML_FOOTER=$(cat << 'FOOTER'
</div>
</body>
</html>
FOOTER
)
# Uso: ensamblar reporte
build_report() {
local titulo="$1"
local contenido="$2"
echo "$HTML_HEADER"
echo "<h1>$titulo</h1>"
echo "<p class='meta'>Generado: $(date '+%d/%m/%Y %H:%M:%S') · Host: $(hostname)</p>"
echo "$contenido"
echo "<div class='footer'>Reporte automático · scripts-en-bash</div>"
echo "$HTML_FOOTER"
}
# Ejemplo: tabla en contenido
tabla_servidores() {
cat << TABLE
<table>
<thead><tr><th>Servidor</th><th>Estado</th><th>Uptime</th><th>Carga</th></tr></thead>
<tbody>
<tr><td>web01</td><td class="ok">ACTIVO</td><td>45d</td><td>0.32</td></tr>
<tr><td>db01</td><td class="ok">ACTIVO</td><td>120d</td><td>0.12</td></tr>
<tr><td>cache</td><td class="err">INACTIVO</td><td>0d</td><td>—</td></tr>
</tbody>
</table>
TABLE
}
build_report "📊 Estado de Servidores" "$(tabla_servidores)" > reporte_template.html
JSON con jq
Construcción de JSON desde Bash
jq no es solo para consultar JSON — también es excelente para generarlo de forma estructurada y válida:
#!/bin/bash
# json_build.sh — Construir JSON con jq
# Método 1: jq -n con --arg
nombre="Ana García"
email="ana@example.com"
rol="admin"
jq -n \
--arg nombre "$nombre" \
--arg email "$email" \
--arg rol "$rol" \
'{nombre: $nombre, email: $email, rol: $rol}'
# → {"nombre":"Ana García","email":"ana@example.com","rol":"admin"}
# Método 2: arrays de objetos
nombres=("Ana" "Carlos" "Laura")
emails=("ana@ex.com" "carlos@ex.com" "laura@ex.com")
roles=("admin" "editor" "lector")
jq -n \
--arg nombres "$(printf '%s\n' "${nombres[@]}")" \
--arg emails "$(printf '%s\n' "${emails[@]}")" \
--arg roles "$(printf '%s\n' "${roles[@]}")" \
'[
[$nombres | split("\n")[]],
[$emails | split("\n")[]],
[$roles | split("\n")[]]
] | transpose | map({nombre: .[0], email: .[1], rol: .[2]})'
jq como generador y validador
Una ventaja de usar jq es que valida el JSON automáticamente:
#!/bin/bash
# json_valido.sh
datos_json=$(jq -n \
--arg servidor "web01" \
--argjson uptime 45 \
--argjson activo true \
'{servidor: $servidor, uptime_dias: $uptime, activo: $activo}'
)
echo "$datos_json" | jq . # Pretty-print y validación
# Guardar a archivo
echo "$datos_json" > reporte.json
# Agregar a array existente
if [[ -f reportes.json ]]; then
jq --argjson nuevo "$datos_json" '. += [$nuevo]' reportes.json > tmp.json
mv tmp.json reportes.json
else
jq -n --argjson primero "$datos_json" '[$primero]' > reportes.json
fi
Arrays y objetos anidados
#!/bin/bash
# json_anidado.sh
# Datos jerárquicos
jq -n \
--arg nombre "Proyecto X" \
--argjson version 2.1 \
--arg autor "Ana" \
--arg servidores '["web01","db01","cache01"]' \
--arg metricas '{"cpu": 45.2, "ram": 68.1, "disk": 82.3}' \
'{
nombre: $nombre,
version: $version,
autor: $autor,
servidores: ($servidores | fromjson),
metricas: ($metricas | fromjson),
timestamp: now | strftime("%Y-%m-%dT%H:%M:%SZ")
}'
Alternativa sin jq: si no puedes instalar
jq, puedes construir JSON manualmente conprintfy arrays, pero no obtienes validación automática y el escapado de strings es más tedioso.
Markdown tables desde datos
Las tablas Markdown con formato pipe son ideales para documentación y READMEs:
#!/bin/bash
# md_table.sh — Generar tabla Markdown
generar_md_table() {
local -n _headers=$1
local -n _data=$2
local align="${3:-left}" # left, center, right
# Mapa de alineación
local align_char
case "$align" in
center) align_char=":"; col=":---:" ;;
right) align_char=":"; col="---:" ;;
*) align_char=""; col="---" ;;
esac
# Cabecera
echo -n "|"
for h in "${_headers[@]}"; do
echo -n " $h |"
done
echo
# Separador de alineación
echo -n "|"
for _ in "${_headers[@]}"; do
echo -n " ${align_char}${col}${align_char} |"
done
echo
# Datos
for fila in "${_data[@]}"; do
echo -n "|"
IFS='|' read -ra campos <<< "$fila"
for campo in "${campos[@]}"; do
echo -n " $campo |"
done
echo
done
}
# Uso
encabezados=("Servidor" "IP" "Estado")
datos=(
"web01|192.168.1.10|✅ ACTIVO"
"db01|192.168.1.20|✅ ACTIVO"
"cache|192.168.1.30|❌ INACTIVO"
)
generar_md_table encabezados datos left
Salida:
| Servidor | IP | Estado |
| --- | --- | --- |
| web01 | 192.168.1.10 | ✅ ACTIVO |
| db01 | 192.168.1.20 | ✅ ACTIVO |
| cache | 192.168.1.30 | ❌ INACTIVO |
Gráficos con gnuplot
Barras, líneas y pie
gnuplot es el estándar de facto para gráficos científicos desde terminal. Produce PNG, SVG, PDF, EPS y más.
#!/bin/bash
# gnuplot_bar.sh — Gráfico de barras
cat > /tmp/datos_bar.dat << EOF
# Servidor Uso_CPU Uso_RAM Uso_Disk
web01 45.2 68.1 82.3
db01 32.7 55.4 91.2
cache 12.3 24.6 45.8
backup 5.1 10.2 30.0
EOF
gnuplot << 'GNUPLOT'
set terminal pngcairo size 800,500 enhanced font 'Arial,11'
set output 'grafico_barras.png'
set title 'Uso de recursos por servidor (%)'
set style data histogram
set style histogram cluster gap 2
set style fill solid 0.8 border -1
set boxwidth 0.9
set ylabel 'Porcentaje (%)'
set yrange [0:100]
set grid ytics
set key outside right top
plot '/tmp/datos_bar.dat' using 2:xtic(1) title 'CPU' linecolor rgb '#2563eb', \
'' using 3 title 'RAM' linecolor rgb '#16a34a', \
'' using 4 title 'Disk' linecolor rgb '#d97706'
GNUPLOT
echo "✅ Gráfico generado: grafico_barras.png"
Líneas (series temporales)
#!/bin/bash
# gnuplot_line.sh
cat > /tmp/datos_line.dat << EOF
# Hora Peticiones Errores Latencia_ms
00:00 120 2 45
01:00 85 1 42
02:00 65 0 38
03:00 50 0 35
04:00 45 0 36
05:00 60 1 40
06:00 110 3 48
07:00 250 5 55
08:00 480 12 72
09:00 520 15 78
10:00 510 13 75
11:00 490 10 70
EOF
gnuplot << 'GNUPLOT'
set terminal pngcairo size 900,500 enhanced font 'Arial,11'
set output 'grafico_series.png'
set title 'Métricas del servidor — 24h'
set xlabel 'Hora'
set ylabel 'Peticiones'
set y2label 'Latencia (ms)'
set y2tics
set grid
set key inside right top
plot '/tmp/datos_line.dat' using 2:xtic(1) title 'Peticiones' with lines linewidth 2 linecolor rgb '#2563eb', \
'' using 3 title 'Errores' with lines linewidth 2 linecolor rgb '#dc2626', \
'' using 4 title 'Latencia' axis x1y2 with lines linewidth 2 linecolor rgb '#d97706' dashtype 2
GNUPLOT
echo "✅ Gráfico de series generado: grafico_series.png"
Gráfico de pastel (pie chart)
#!/bin/bash
# gnuplot_pie.sh — Gráfico circular
cat > /tmp/datos_pie.dat << EOF
# Categoría Valor Color
"Web" 45 "#2563eb"
"DB" 30 "#16a34a"
"Cache" 15 "#d97706"
"Backup" 10 "#dc2626"
EOF
gnuplot << 'GNUPLOT'
set terminal pngcairo size 600,600 enhanced font 'Arial,11'
set output 'grafico_pie.png'
set title 'Distribución de recursos'
set size square
set style data histograms
unset xtics
unset ytics
unset border
set style fill solid 1.0 border -1
plot '/tmp/datos_pie.dat' using 2:xtic(1):3 with boxes linecolor rgb variable, \
'' using 0:2:(sprintf("%s (%.0f%%)", stringcolumn(1), column(2))) with labels offset 0,0.5 notitle
GNUPLOT
echo "✅ Gráfico circular generado: grafico_pie.png"
Datos desde heredocs y archivos
La forma más limpia de pasar datos a gnuplot es mediante un heredoc y un archivo temporal:
#!/bin/bash
# gnuplot_heredoc.sh
generar_grafico() {
local titulo="$1"
local output="$2"
local archivo_datos="$3"
gnuplot << GNUPLOT
set terminal pngcairo size 800,500 enhanced font 'Arial,10'
set output '$output'
set title '$titulo'
set style data histogram
set style histogram cluster gap 2
set style fill solid 0.8 border -1
set boxwidth 0.9
set grid ytics
set key outside right top
plot '$archivo_datos' using 2:xtic(1) title 'Valor' linecolor rgb '#2563eb'
GNUPLOT
echo "✅ Gráfico generado: $output"
}
# Crear datos en heredoc
cat > /tmp/ventas.dat << EOF
# Mes Ventas
Enero 12000
Febrero 15000
Marzo 11000
Abril 18000
Mayo 16500
Junio 20000
EOF
generar_grafico "Ventas mensuales (€)" "ventas.png" /tmp/ventas.dat
Reportes por email
mailx (bsd-mailx / heirloom-mailx)
El comando mailx envía correos desde terminal. Con -a (attachment) y -s (subject):
#!/bin/bash
# mailx_simple.sh
REPORTE="reporte_$(date +%Y%m%d).html"
DESTINO="admin@example.com"
ASUNTO="📊 Reporte diario de servidores — $(date +%d/%m/%Y)"
# Generar el reporte HTML
cat > "$REPORTE" << HTML
<html><body>
<h1>Reporte Diario</h1>
<p>Generado el $(date '+%d/%m/%Y %H:%M')</p>
<pre>$(df -h | head -10)</pre>
</body></html>
HTML
# Enviar con mailx
mailx -s "$ASUNTO" \
-a "$REPORTE" \
"$DESTINO" <<< "Adjunto reporte diario de servidores."
Para body HTML (no attachment) con mailx:
# Para body HTML con heirloom-mailx
(
echo "Subject: $ASUNTO"
echo "MIME-Version: 1.0"
echo "Content-Type: text/html; charset=UTF-8"
echo ""
cat "$REPORTE"
) | /usr/bin/sendmail "$DESTINO"
mutt — HTML y attachments
mutt es más potente: soporta HTML inline y attachments múltiples de forma nativa:
#!/bin/bash
# mutt_report.sh
REPORTE_HTML="reporte_$(date +%Y%m%d).html"
REPORTE_CSV="datos_$(date +%Y%m%d).csv"
DESTINO="admin@example.com"
ASUNTO="📊 Reporte $(date +%d/%m/%Y)"
# Generar archivos
echo "<html><body><h1>Reporte</h1><p>Todo OK</p></body></html>" > "$REPORTE_HTML"
echo "servidor,estado,uptime" > "$REPORTE_CSV"
echo "web01,ACTIVO,45d" >> "$REPORTE_CSV"
# Enviar con mutt: HTML inline + CSV attachment
mutt -e "set content_type=text/html" \
-s "$ASUNTO" \
-a "$REPORTE_CSV" \
-- "$DESTINO" < "$REPORTE_HTML"
sendmail directo
Para entornos sin mailx ni mutt, puedes enviar directamente con sendmail construyendo el mensaje MIME manualmente:
#!/bin/bash
# sendmail_directo.sh
FROM="reportes@example.com"
TO="admin@example.com"
ASUNTO="📊 Reporte automático"
BOUNDARY="==BOUNDARY_$(date +%s)=="
# Body HTML
BODY_HTML=$(cat << HTML
<html><body>
<h1 style="color:#2563eb;">Reporte de Estado</h1>
<p>Todo los sistemas operativos correctamente.</p>
</body></html>
HTML
)
# Construir mensaje MIME
cat > /tmp/mensaje.mime << MIME
From: $FROM
To: $TO
Subject: $ASUNTO
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="$BOUNDARY"
--$BOUNDARY
Content-Type: text/html; charset=UTF-8
Content-Transfer-Encoding: 7bit
$BODY_HTML
--$BOUNDARY
Content-Type: text/csv; charset=UTF-8
Content-Disposition: attachment; filename="datos.csv"
nombre,email
Ana,ana@example.com
Carlos,carlos@example.com
--$BOUNDARY--
MIME
# Enviar
/usr/sbin/sendmail -t < /tmp/mensaje.mime
msmtp como relay ligero
msmtp es un cliente SMTP minimalista, ideal para servidores sin MTA completo:
#!/bin/bash
# msmtp_report.sh
# Configuración ~/.msmtprc:
# account default
# host smtp.gmail.com
# port 587
# auth on
# user tu@email.com
# passwordeval "gpg -q --decrypt ~/.msmtp-pass.gpg"
# tls on
# tls_trust_file /etc/ssl/certs/ca-certificates.crt
FROM="tu@email.com"
TO="admin@example.com"
ASUNTO="📊 Reporte $(date +%d/%m/%Y)"
(
echo "From: $FROM"
echo "To: $TO"
echo "Subject: $ASUNTO"
echo "Content-Type: text/html; charset=UTF-8"
echo ""
echo "<html><body><h1>Reporte</h1><p>OK</p></body></html>"
) | msmtp "$TO"
HTML email con imágenes incrustadas
CID embedding (MIME multipart)
Para que las imágenes aparezcan dentro del cuerpo del email (no como attachments), necesitas incrustarlas vía Content-ID (CID):
#!/bin/bash
# email_cid.sh — Imágenes incrustadas en email
FROM="reportes@example.com"
TO="admin@example.com"
ASUNTO="📊 Reporte con gráficos"
BOUNDARY="==BOUNDARY_$(date +%s)_part=="
CID_GRAFICO="grafico01@reporte"
# Generar gráfico
cat > /tmp/datos.dat << EOF
# Mes Valor
Ene 100
Feb 150
Mar 120
EOF
gnuplot << GNUPLOT
set terminal pngcairo size 500,300
set output '/tmp/grafico.png'
set title 'Ventas mensuales'
plot '/tmp/datos.dat' using 2:xtic(1) with boxes notitle
GNUPLOT
# Codificar imagen a base64 para MIME
GRAFICO_B64=$(base64 /tmp/grafico.png | tr -d '\n')
# Construir mensaje MIME con imagen incrustada
cat > /tmp/email_mime.txt << MIME
From: $FROM
To: $TO
Subject: $ASUNTO
MIME-Version: 1.0
Content-Type: multipart/related; boundary="$BOUNDARY"
--$BOUNDARY
Content-Type: text/html; charset=UTF-8
<html>
<body style="font-family: Arial, sans-serif;">
<h1 style="color: #2563eb;">📊 Reporte Mensual</h1>
<p>Adjuntamos gráfico de ventas:</p>
<img src="cid:$CID_GRAFICO" alt="Gráfico ventas" style="max-width: 100%;">
<p style="color: #666; font-size: 0.8em;">Generado automáticamente</p>
</body>
</html>
--$BOUNDARY
Content-Type: image/png
Content-Transfer-Encoding: base64
Content-ID: <$CID_GRAFICO>
Content-Disposition: inline
$GRAFICO_B64
--$BOUNDARY--
MIME
# Enviar
/usr/sbin/sendmail -t < /tmp/email_mime.txt
echo "✅ Email con imagen CID enviado"
Base64 inline en HTML
Alternativa más simple (pero con emails más pesados): incrustar la imagen en base64 directamente en el HTML:
#!/bin/bash
# email_base64_inline.sh
IMAGEN_B64=$(base64 -w0 /tmp/grafico.png)
cat << HTML
<html><body>
<h1>Reporte con gráfico inline</h1>
<img src="data:image/png;base64,$IMAGEN_B64" alt="Gráfico" style="max-width:100%;">
</body></html>
HTML
# Enviar como body HTML
mutt -e "set content_type=text/html" \
-s "📊 Reporte con gráfico" \
-- "admin@example.com" < "$(cat)"
⚠️ La base64 inline aumenta el tamaño del email ~33%. Para gráficos grandes (>100 KB), prefiere CID embedding.
PDF desde Bash
wkhtmltopdf — HTML a PDF
wkhtmltopdf convierte HTML a PDF usando WebKit. Es la forma más directa de generar PDFs con estilo:
#!/bin/bash
# wkhtmltopdf_report.sh
if ! command -v wkhtmltopdf &>/dev/null; then
echo "❌ wkhtmltopdf no instalado. Instalar con: sudo apt install wkhtmltopdf"
exit 1
fi
# Generar HTML
REPORTE_HTML="/tmp/reporte_pdf.html"
REPORTE_PDF="reporte_$(date +%Y%m%d).pdf"
cat > "$REPORTE_HTML" << HTML
<!DOCTYPE html>
<html>
<head><meta charset="UTF-8">
<style>
body { font-family: 'DejaVu Sans', Arial, sans-serif; margin: 2cm; }
h1 { color: #2563eb; border-bottom: 2px solid #2563eb; padding-bottom: 10px; }
table { width: 100%; border-collapse: collapse; margin-top: 20px; }
th { background: #2563eb; color: white; padding: 8px; }
td { padding: 6px; border-bottom: 1px solid #ddd; }
.footer { margin-top: 30px; font-size: 0.8em; color: #666; text-align: center; }
</style>
</head>
<body>
<h1>📊 Reporte Mensual de Servidores</h1>
<p>Generado: $(date '+%d/%m/%Y %H:%M')</p>
<table>
<tr><th>Servidor</th><th>Estado</th><th>Uptime</th><th>CPU</th></tr>
<tr><td>web01</td><td style="color:green;">✅ ACTIVO</td><td>45d</td><td>32%</td></tr>
<tr><td>db01</td><td style="color:green;">✅ ACTIVO</td><td>120d</td><td>55%</td></tr>
<tr><td>cache</td><td style="color:red;">❌ INACTIVO</td><td>0d</td><td>—</td></tr>
</table>
<div class="footer">Reporte automático · scripts-en-bash</div>
</body></html>
HTML
# Convertir a PDF
wkhtmltopdf --page-size A4 --margin-top 15mm --margin-bottom 15mm \
"$REPORTE_HTML" "$REPORTE_PDF"
echo "✅ PDF generado: $REPORTE_PDF"
pandoc — Markdown a PDF
Si trabajas con Markdown, pandoc es la navaja suiza de la conversión:
#!/bin/bash
# pandoc_pdf.sh
if ! command -v pandoc &>/dev/null; then
echo "❌ pandoc no instalado. Instalar con: sudo apt install pandoc"
exit 1
fi
# Verificar LaTeX (necesario para PDF)
if ! command -v pdflatex &>/dev/null; then
echo "⚠️ pdflatex no encontrado. Prueba con: sudo apt install texlive-latex-base"
fi
# Generar Markdown
cat > /tmp/reporte.md << MD
---
title: "Reporte de Métricas"
date: $(date '+%Y-%m-%d')
---
# Resumen
| Servidor | Estado | Uptime | CPU |
|----------|----------|--------|-----|
| web01 | ✅ ACTIVO | 45d | 32% |
| db01 | ✅ ACTIVO | 120d | 55% |
| cache | ❌ INACTIVO | 0d | — |
## Observaciones
- **web01**: carga normal
- **db01**: pico de RAM a las 14:00
- **cache**: requiere reinicio
---
*Generado automáticamente*
MD
# Convertir a PDF
pandoc /tmp/reporte.md -o "reporte_$(date +%Y%m%d).pdf" \
--pdf-engine=xelatex \
-V mainfont='DejaVu Sans'
echo "✅ PDF generado con pandoc"
Combinación y paginación
Para reportes largos con múltiples secciones:
#!/bin/bash
# pdf_combinar.sh
# Generar páginas individuales
for i in 1 2 3; do
echo "<html><body><h1>Página $i</h1><p>Contenido de página $i</p></body></html>" > "/tmp/pagina$i.html"
wkhtmltopdf "/tmp/pagina$i.html" "/tmp/pagina$i.pdf" 2>/dev/null
done
# Combinar con pdfunite (poppler-utils)
if command -v pdfunite &>/dev/null; then
pdfunite /tmp/pagina1.pdf /tmp/pagina2.pdf /tmp/pagina3.pdf "reporte_completo_$(date +%Y%m%d).pdf"
echo "✅ PDFs combinados"
else
echo "⚠️ pdfunite no disponible, instalar: sudo apt install poppler-utils"
# Fallback: pdftk (otra opción)
# pdftk /tmp/pagina*.pdf cat output reporte_completo.pdf
fi
Datos desde logs y archivos
awk aggregation para reportes
awk es la herramienta reina para agregar datos desde logs:
#!/bin/bash
# awk_aggregate.sh — Resumen de logs de acceso
LOG="/var/log/nginx/access.log"
REPORTE_CSV="resumen_accesos.csv"
REPORTE_HTML="resumen_accesos.html"
echo "Procesando $LOG ..."
# Top 10 IPs más frecuentes
echo "=== Top 10 IPs ==="
awk '{ ips[$1]++ } END { for (ip in ips) print ips[ip], ip }' "$LOG" \
| sort -rn | head -10 > /tmp/top_ips.txt
# Códigos de estado HTTP agrupados
echo "=== Códigos HTTP ==="
awk '{
status = $9
if (status ~ /^2/) codigos["2xx"]++
else if (status ~ /^3/) codigos["3xx"]++
else if (status ~ /^4/) codigos["4xx"]++
else if (status ~ /^5/) codigos["5xx"]++
} END {
for (c in codigos) print c, codigos[c]
}' "$LOG" | sort > /tmp/http_codes.txt
# Tráfico por hora
echo "=== Tráfico por hora ==="
awk '{
hora = substr($4, 14, 2)
peticiones[hora]++
} END {
for (h in peticiones) print h, peticiones[h]
}' "$LOG" | sort -n > /tmp/trafico_hora.txt
# Peticiones por método HTTP
echo "=== Métodos HTTP ==="
awk '{ metodos[$6]++ } END { for (m in metodos) print m, metodos[m] }' "$LOG" \
| tr -d '"' > /tmp/metodos.txt
# Generar CSV de resumen
printf '\xEF\xBB\xBF' > "$REPORTE_CSV"
echo "Métrica,Valor" >> "$REPORTE_CSV"
echo "Total peticiones,$(wc -l < "$LOG")" >> "$REPORTE_CSV"
echo "IPs únicas,$(awk '!seen[$1]++' "$LOG" | wc -l)" >> "$REPORTE_CSV"
echo "✅ CSV generado: $REPORTE_CSV"
Parseo de logs a estructuras
#!/bin/bash
# log_to_json.sh — Log de acceso a JSON
LOG="/var/log/nginx/access.log"
# Parsear líneas de log a objetos JSON
tail -100 "$LOG" | while IFS= read -r linea; do
# Formato: IP - - [fecha] "METHOD URL PROTO" STATUS BYTES "REFERER" "UA"
ip=$(echo "$linea" | awk '{print $1}')
fecha=$(echo "$linea" | awk -F'[][]' '{print $2}')
metodo=$(echo "$linea" | awk '{print $6}' | tr -d '"')
url=$(echo "$linea" | awk '{print $7}')
status=$(echo "$linea" | awk '{print $9}')
jq -n \
--arg ip "$ip" \
--arg fecha "$fecha" \
--arg metodo "$metodo" \
--arg url "$url" \
--argjson status "${status:-0}" \
'{ip: $ip, fecha: $fecha, metodo: $metodo, url: $url, status: $status}'
done | jq -s '.' > accesos_recientes.json
echo "✅ JSON generado con $(jq length accesos_recientes.json) registros"
Rotación de reportes (GFS style)
Al igual que los backups, los reportes deben rotarse para no llenar el disco:
#!/bin/bash
# report_rotation.sh — Rotación estilo GFS
REPORTES_DIR="/var/reportes"
RETENCION_DIARIA=7
RETENCION_SEMANAL=4
RETENCION_MENSUAL=3
mkdir -p "$REPORTES_DIR"/{diario,semanal,mensual}
generar_reporte_diario() {
local fecha=$(date +%Y%m%d)
local archivo="$REPORTES_DIR/diario/reporte_$fecha.html"
echo "<html><body><h1>Reporte diario $fecha</h1></body></html>" > "$archivo"
echo "✅ Reporte diario: $archivo"
}
rotar_reportes() {
local dir="$1"
local max="$2"
local prefix="$3"
# Contar archivos y eliminar los más antiguos
local archivos=()
mapfile -t archivos < <(ls -1t "$dir"/"$prefix"*.html 2>/dev/null)
if [[ ${#archivos[@]} -gt "$max" ]]; then
local a_eliminar=("${archivos[@]:$max}")
for f in "${a_eliminar[@]}"; do
echo "🗑️ Eliminando reporte antiguo: $f"
rm "$f"
done
fi
}
# Promover reporte diario a semanal (domingos)
if [[ $(date +%u) -eq 7 ]]; then
cp "$REPORTES_DIR/diario/reporte_$(date +%Y%m%d).html" \
"$REPORTES_DIR/semanal/reporte_semana_$(date +%V).html" 2>/dev/null
fi
# Promover reporte semanal a mensual (primer día del mes)
if [[ $(date +%d) -eq 1 ]]; then
cp "$REPORTES_DIR/semanal/reporte_semana_$(date +%V --date='-1 week').html" \
"$REPORTES_DIR/mensual/reporte_$(date +%Y%m).html" 2>/dev/null
fi
# Ejecutar rotación
generar_reporte_diario
rotar_reportes "$REPORTES_DIR/diario" "$RETENCION_DIARIA" "reporte_"
rotar_reportes "$REPORTES_DIR/semanal" "$RETENCION_SEMANAL" "reporte_semana_"
rotar_reportes "$REPORTES_DIR/mensual" "$RETENCION_MENSUAL" "reporte_"
Programación de reportes
Cron scheduling
# /etc/cron.d/reportes
# Generar reporte cada hora
0 * * * * root /usr/local/bin/reporte_horario.sh
# Reporte diario a las 6:00
0 6 * * * root /usr/local/bin/reporte_diario.sh
# Reporte semanal (lunes 7:00)
0 7 * * 1 root /usr/local/bin/reporte_semanal.sh
# Reporte mensual (día 1 a las 8:00)
0 8 1 * * root /usr/local/bin/reporte_mensual.sh
Systemd timer
# /etc/systemd/system/reporte-diario.service
[Unit]
Description=Genera reporte diario de servidores
[Service]
Type=oneshot
ExecStart=/usr/local/bin/reporte_diario.sh
User=root
# /etc/systemd/system/reporte-diario.timer
[Unit]
Description=Timer para reporte diario
[Timer]
OnCalendar=daily
Persistent=true
RandomizedDelaySec=300
[Install]
WantedBy=timers.target
Estrategias de horarios
#!/bin/bash
# scheduler_reportes.sh
# Determinar qué reporte generar según el día
case $(date +%u) in # ... 7=domingo
1) # Lunes: reporte semanal completo
./generar_reporte.sh --type weekly --format html,pdf
;;
2|3|4|5|6) # Martes-Sábado: reporte diario ligero
./generar_reporte.sh --type daily --format csv
;;
7) # Domingo: reporte semanal + rotación
./generar_reporte.sh --type weekly --format html,csv
./rotar_reportes.sh
;;
esac
Funciones profesionales
Funciones reutilizables para tu biblioteca personal:
# ──────────────────────────────────────────────
# Funciones profesionales para reportes
# ──────────────────────────────────────────────
# csv_escape — Escapa un campo CSV según RFC 4180
# Uso: csv_escape "campo con, comillas"
# Devuelve: print del campo escapado
csv_escape() {
local campo="$1"
local escapado
# Si contiene comillas, escapar duplicando
if [[ "$campo" == *\"* ]]; then
escapado="${campo//\"/\"\"}"
else
escapado="$campo"
fi
# Si necesita comillas (comas, saltos, comillas)
if [[ "$campo" == *[\",\n\"]* ]]; then
echo "\"$escapado\""
elif [[ "$campo" == *\;* ]]; then
# Punto y coma también requiere quoting
echo "\"$escapado\""
else
echo "$campo"
fi
}
# html_table — Genera tabla HTML desde array asociativo
# Uso: html_table "Título" "clase" "cab1,cab2,cab3" "dato1|dato2|dato3" ...
# Devuelve: print del HTML de tabla completo
html_table() {
local titulo="${1:-Tabla}"
local clase="${2:-default}"
local cabeceras="$3"
shift 3
local filas=("$@")
IFS=',' read -ra header_arr <<< "$cabeceras"
cat << HTML
<h2>$titulo</h2>
<table class="$clase">
<thead><tr>
HTML
for h in "${header_arr[@]}"; do
echo " <th>$h</th>"
done
echo "</tr></thead>"
echo "<tbody>"
for fila in "${filas[@]}"; do
echo " <tr>"
IFS='|' read -ra campos <<< "$fila"
for campo in "${campos[@]}"; do
echo " <td>$campo</td>"
done
echo " </tr>"
done
echo "</tbody>"
echo "</table>"
}
# report_email — Envía reporte por email con HTML body
# Uso: report_email "destino" "asunto" "body.html" [attachment.csv]
# Requiere: mutt
report_email() {
local destino="$1"
local asunto="$2"
local body_html="$3"
local attachment="${4:-}"
if [[ ! -f "$body_html" ]]; then
echo "❌ Error: archivo HTML '$body_html' no encontrado" >&2
return 1
fi
if command -v mutt &>/dev/null; then
if [[ -n "$attachment" && -f "$attachment" ]]; then
mutt -e "set content_type=text/html" \
-s "$asunto" \
-a "$attachment" \
-- "$destino" < "$body_html"
else
mutt -e "set content_type=text/html" \
-s "$asunto" \
-- "$destino" < "$body_html"
fi
local rc=$?
if [[ $rc -eq 0 ]]; then
echo "✅ Email enviado a $destino: $asunto"
else
echo "❌ Error al enviar email (código $rc)" >&2
fi
return $rc
elif command -v mailx &>/dev/null; then
# Fallback a mailx (body como texto plano)
if [[ -n "$attachment" && -f "$attachment" ]]; then
mailx -s "$asunto" -a "$attachment" "$destino" < "$body_html"
else
mailx -s "$asunto" "$destino" < "$body_html"
fi
else
echo "❌ No hay cliente de correo disponible (instala mutt o mailx)" >&2
return 1
fi
}
# gnuplot_chart — Genera gráfico PNG desde datos
# Uso: gnuplot_chart "título" "archivo_datos" "archivo_salida" [tipo]
# tipo: bar (defecto), line, pie
gnuplot_chart() {
local titulo="$1"
local datos="$2"
local salida="$3"
local tipo="${4:-bar}"
if ! command -v gnuplot &>/dev/null; then
echo "❌ gnuplot no instalado" >&2
return 1
fi
if [[ ! -f "$datos" ]]; then
echo "❌ Archivo de datos '$datos' no encontrado" >&2
return 1
fi
case "$tipo" in
bar)
gnuplot << GNUPLOT
set terminal pngcairo size 800,500 enhanced font 'Arial,10'
set output '$salida'
set title '$titulo'
set style data histogram
set style histogram cluster gap 2
set style fill solid 0.8 border -1
set boxwidth 0.9
set grid ytics
set ylabel 'Valor'
set key outside right top
plot '$datos' using 2:xtic(1) title 'Datos' linecolor rgb '#2563eb'
GNUPLOT
;;
line)
gnuplot << GNUPLOT
set terminal pngcairo size 800,500 enhanced font 'Arial,10'
set output '$salida'
set title '$titulo'
set grid
set xlabel 'Tiempo'
set ylabel 'Valor'
plot '$datos' using 1:2 with lines linewidth 2 linecolor rgb '#2563eb' title 'Serie'
GNUPLOT
;;
pie)
gnuplot << GNUPLOT
set terminal pngcairo size 600,600 enhanced font 'Arial,10'
set output '$salida'
set title '$titulo'
set size square
unset xtics
unset ytics
unset border
set style fill solid 1.0 border -1
plot '$datos' using 2:xtic(1):3 with boxes linecolor rgb variable, \
'' using 0:2:(sprintf("%s (%.0f%%)", stringcolumn(1), column(2))) with labels offset 0,0.5 notitle
GNUPLOT
;;
*)
echo "❌ Tipo no soportado: $tipo (usa bar, line, pie)" >&2
return 1
;;
esac
if [[ -f "$salida" ]]; then
echo "✅ Gráfico generado: $salida"
return 0
else
echo "❌ Error al generar gráfico" >&2
return 1
fi
}
Script completo: reportes_auto.sh
Un generador de reportes multi-formato que analiza el sistema y produce HTML, CSV, JSON, Markdown, gráfico PNG y envía por email, en ~100 líneas:
#!/bin/bash
# reportes_auto.sh — Generador de reportes multi-formato
# Uso: ./reportes_auto.sh [opciones]
# Opciones: -f html|csv|json|md|pdf|all (defecto: all)
# -e <email> (enviar reporte por email)
# -o <dir> (directorio de salida, defecto: ./reportes)
# -h (ayuda)
set -euo pipefail
# ── Configuración ──
OUTPUT_DIR="./reportes"
FORMATOS="all"
EMAIL=""
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
HOSTNAME=$(hostname)
# ── Colores ──
RST='\033[0m'; G='\033[0;32m'; Y='\033[1;33m'; R='\033[0;31m'; B='\033[0;34m'
info() { echo -e "${G}[INFO]${RST} $*"; }
warn() { echo -e "${Y}[WARN]${RST} $*" >&2; }
error() { echo -e "${R}[ERROR]${RST} $*" >&2; }
# ── Parseo de argumentos ──
uso() {
cat << EOF
Uso: $0 [opciones]
Opciones:
-f <fmt> Formatos: html, csv, json, md, pdf, all (defecto: all)
-e <email> Enviar reporte por email
-o <dir> Directorio de salida (defecto: ./reportes)
-h Mostrar esta ayuda
EOF
exit 0
}
while getopts ":f:e:o:h" opt; do
case "$opt" in
f) FORMATOS="$OPTARG" ;;
e) EMAIL="$OPTARG" ;;
o) OUTPUT_DIR="$OPTARG" ;;
h) uso ;;
:) error "-$OPTARG requiere argumento"; exit 1 ;;
?) error "Opción inválida: -$OPTARG"; exit 1 ;;
esac
done
mkdir -p "$OUTPUT_DIR"
cd "$OUTPUT_DIR"
# ── Recolección de datos ──
info "Recolectando datos del sistema..."
# Uptime
uptime_sec=$(awk '{print $1}' /proc/uptime 2>/dev/null | cut -d. -f1)
uptime_dias=$((uptime_sec / 86400))
uptime_horas=$(( (uptime_sec % 86400) / 3600 ))
# CPU y memoria
cpu=$(top -bn1 2>/dev/null | grep "Cpu(s)" | awk '{print 100 - $8}' || echo "N/A")
mem_total=$(free -m 2>/dev/null | awk '/Mem:/ {print $2}')
mem_used=$(free -m 2>/dev/null | awk '/Mem:/ {print $3}')
disk_used=$(df -h / 2>/dev/null | awk 'NR==2 {print $5}' | tr -d '%')
# Procesos
proc_total=$(ps aux 2>/dev/null | wc -l)
proc_user=$(ps -U "$(whoami)" 2>/dev/null | wc -l)
# Carga del sistema
load=$(uptime 2>/dev/null | awk -F'load average:' '{print $2}' | xargs || echo "N/A")
# ── HTML ──
if [[ "$FORMATOS" == "all" || "$FORMATOS" == "html" ]]; then
info "Generando HTML..."
cat > "reporte_${TIMESTAMP}.html" << HTML
<!DOCTYPE html>
<html lang="es">
<head><meta charset="UTF-8">
<style>
body { font-family: Arial, sans-serif; margin: 20px; background: #f4f4f4; }
.container { max-width: 800px; margin: 0 auto; background: white; padding: 20px; border-radius: 8px; }
h1 { color: #2563eb; }
table { width: 100%; border-collapse: collapse; margin: 15px 0; }
th { background: #2563eb; color: white; padding: 10px; text-align: left; }
td { padding: 8px; border-bottom: 1px solid #ddd; }
.ok { color: #16a34a; font-weight: bold; }
.warn { color: #d97706; font-weight: bold; }
.crit { color: #dc2626; font-weight: bold; }
.footer { margin-top: 20px; font-size: 0.8em; color: #666; }
</style></head>
<body>
<div class="container">
<h1>📊 Reporte del Sistema — $HOSTNAME</h1>
<p class="meta">Generado: $(date '+%d/%m/%Y %H:%M:%S')</p>
<h2>📈 Resumen</h2>
<table>
<tr><th>Métrica</th><th>Valor</th></tr>
<tr><td>Uptime</td><td>${uptime_dias}d ${uptime_horas}h</td></tr>
<tr><td>CPU (%)</td><td class="$(awk "BEGIN{print ($cpu > 80) ? \"crit\" : ($cpu > 50) ? \"warn\" : \"ok\"}")">$cpu</td></tr>
<tr><td>RAM (MB)</td><td>${mem_used:-N/A} / ${mem_total:-N/A}</td></tr>
<tr><td>Disco / (%)</td><td class="$(awk "BEGIN{print ($disk_used > 80) ? \"crit\" : ($disk_used > 60) ? \"warn\" : \"ok\"}")">${disk_used:-N/A}%</td></tr>
<tr><td>Procesos</td><td>$proc_total (tuyos: $proc_user)</td></tr>
<tr><td>Carga (1/5/15)</td><td>$load</td></tr>
</table>
<div class="footer">Reporte automático · scripts-en-bash · Capítulo 29</div>
</div></body></html>
HTML
echo "✅ reporte_${TIMESTAMP}.html"
fi
# ── CSV ──
if [[ "$FORMATOS" == "all" || "$FORMATOS" == "csv" ]]; then
info "Generando CSV..."
printf '\xEF\xBB\xBF' > "reporte_${TIMESTAMP}.csv"
echo "metrica,valor" >> "reporte_${TIMESTAMP}.csv"
echo "uptime,${uptime_dias}d ${uptime_horas}h" >> "reporte_${TIMESTAMP}.csv"
echo "cpu,$cpu" >> "reporte_${TIMESTAMP}.csv"
echo "ram_usada_mb,${mem_used:-N/A}" >> "reporte_${TIMESTAMP}.csv"
echo "ram_total_mb,${mem_total:-N/A}" >> "reporte_${TIMESTAMP}.csv"
echo "disco_pct,${disk_used:-N/A}" >> "reporte_${TIMESTAMP}.csv"
echo "procesos,$proc_total" >> "reporte_${TIMESTAMP}.csv"
echo "carga,$load" >> "reporte_${TIMESTAMP}.csv"
echo "✅ reporte_${TIMESTAMP}.csv"
fi
# ── JSON ──
if [[ "$FORMATOS" == "all" || "$FORMATOS" == "json" ]]; then
info "Generando JSON..."
if command -v jq &>/dev/null; then
jq -n \
--arg host "$HOSTNAME" \
--arg ts "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
--argjson uptime_dias "$uptime_dias" \
--argjson uptime_horas "$uptime_horas" \
--argjson cpu "${cpu:-0}" \
--argjson mem_used "${mem_used:-0}" \
--argjson mem_total "${mem_total:-0}" \
--argjson disk "${disk_used:-0}" \
--argjson procesos "$proc_total" \
--arg carga "$load" \
'{
host: $host,
timestamp: $ts,
uptime: {dias: $uptime_dias, horas: $uptime_horas},
cpu: $cpu,
memoria: {usada_mb: $mem_used, total_mb: $mem_total},
disco: "\($disk)%",
procesos: $procesos,
carga: $carga
}' > "reporte_${TIMESTAMP}.json"
echo "✅ reporte_${TIMESTAMP}.json"
else
warn "jq no disponible, omitiendo JSON"
fi
fi
# ── Markdown ──
if [[ "$FORMATOS" == "all" || "$FORMATOS" == "md" ]]; then
info "Generando Markdown..."
cat > "reporte_${TIMESTAMP}.md" << MD
# 📊 Reporte del Sistema — $HOSTNAME
Generado: $(date '+%d/%m/%Y %H:%M:%S')
## 📈 Resumen
| Métrica | Valor |
|---------|-------|
| Uptime | ${uptime_dias}d ${uptime_horas}h |
| CPU (%) | $cpu |
| RAM (MB) | ${mem_used:-N/A} / ${mem_total:-N/A} |
| Disco / (%) | ${disk_used:-N/A}% |
| Procesos | $proc_total (tuyos: $proc_user) |
| Carga (1/5/15) | $load |
---
*Reporte automático · scripts-en-bash*
MD
echo "✅ reporte_${TIMESTAMP}.md"
fi
# ── PDF (via pandoc) ──
if [[ "$FORMATOS" == "all" || "$FORMATOS" == "pdf" ]]; then
if command -v pandoc &>/dev/null && command -v wkhtmltopdf &>/dev/null; then
info "Generando PDF..."
wkhtmltopdf "reporte_${TIMESTAMP}.html" "reporte_${TIMESTAMP}.pdf" 2>/dev/null
echo "✅ reporte_${TIMESTAMP}.pdf"
else
warn "pandoc o wkhtmltopdf no disponibles, omitiendo PDF"
fi
fi
# ── Envío por email ──
if [[ -n "$EMAIL" ]]; then
info "Enviando reporte a $EMAIL..."
if command -v mutt &>/dev/null; then
mutt -e "set content_type=text/html" \
-s "📊 Reporte $HOSTNAME — $(date +%d/%m/%Y)" \
-a "reporte_${TIMESTAMP}.csv" \
-- "$EMAIL" < "reporte_${TIMESTAMP}.html"
echo "✅ Email enviado a $EMAIL"
else
warn "mutt no instalado, no se pudo enviar email"
fi
fi
info "✅ Reportes generados en: $(pwd)"
Errores comunes
Error 1: No escapar comillas en CSV
# ❌ Campo con comillas rompe el CSV
echo 'Ana "La Jefa" García,admin'
# → "Ana "La Jefa" García" → CSV inválido
# ✅ Escapar comillas duplicándolas
echo 'Ana ""La Jefa"" García,admin'
# Usar csv_escape() para manejo automático
Error 2: Olvidar el BOM UTF-8 para Excel Windows
# ❌ Excel Windows abre el CSV con caracteres extraños
echo "nombre,email" > reporte.csv
# ✅ Añadir BOM al inicio
printf '\xEF\xBB\xBF' > reporte.csv
echo "nombre,email" >> reporte.csv
Error 3: No usar comillas dobles en el heredoc para $variables
### Error 4: No usar comillas dobles en el heredoc para $variables
```bash
# ❌ Sin comillas en 'EOF', las variables NO se expanden
cat > file.html << 'EOF'
<h1>$TITULO</h1> # ← se escribe literal "$TITULO"
EOF
# ✅ Sin comillas: las variables se expanden
cat > file.html << EOF
<h1>$TITULO</h1> # ← se expande al valor
EOF
# Para contenido que mezcla expansión y literales, usa sed post-procesado
Error 4: Asumir que jq está instalado
# ❌ En un contenedor mínimo puede no estar
datos_json=$(jq -n ...) # command not found
# ✅ Verificar disponibilidad
if command -v jq &>/dev/null; then
jq -n ...
else
# Fallback: construir JSON manualmente con printf
echo '{"ok": true}'
fi
Error 5: gnuplot sin terminal pngcairo
# ❌ En servidores sin pngcairo (librería cairo no instalada)
set terminal pngcairo # Error: unknown terminal type
# ✅ Verificar terminales disponibles y usar fallback
gnuplot -e "set terminal" 2>&1 | grep -q pngcairo && {
set terminal pngcairo
} || {
set terminal png # fallback más básico
}
Error 6: No sanitizar datos del sistema para HTML
# ❌ Caracteres HTML en datos rompen la página
hostname="Server <script>alert('xss')</script>"
echo "<h1>$hostname</h1>" # ¡XSS en el reporte!
# ✅ Escapar caracteres HTML
html_escape() {
local s="$1"
s="${s//&/&}"
s="${s//</<}"
s="${s//>/>}"
s="${s//\"/"}"
s="${s//\'/'}"
echo "$s"
}
echo "<h1>$(html_escape "$hostname")</h1>"
Error 7: Enviar a sendmail sin construir MIME correctamente
# ❌ Faltan cabeceras MIME, el cliente no interpreta el body
echo "Subject: Report" | sendmail admin@example.com
# ✅ Construir mensaje MIME completo
cat << MIME | sendmail -t
From: from@ex.com
To: to@ex.com
Subject: Report
MIME-Version: 1.0
Content-Type: text/html; charset=UTF-8
<html><body><h1>Reporte</h1></body></html>
MIME
Error 8: Olvidar cerrar etiquetas HTML en tablas
# ❌ Tabla mal formada (falta </tr>, </table>)
echo "<table><tr><td>Dato"
# ✅ Siempre cerrar todas las etiquetas
echo "<table><tr><td>Dato</td></tr></table>"
Error 9: Acumular archivos sin rotación
# ❌ Los reportes se acumulan hasta llenar el disco
./generar_reporte.sh # día tras día, sin límite
# ✅ Implementar rotación al generar
MAX_REPORTES=30
while [[ $(ls -1 reporte_*.html 2>/dev/null | wc -l) -gt $MAX_REPORTES ]]; do
rm "$(ls -1t reporte_*.html 2>/dev/null | tail -1)"
done
Resumen
| Concepto | Sintaxis / Comando | Propósito |
|---|---|---|
| CSV básico | echo "campo1,campo2" | Datos separados por comas |
| CSV escape | "${campo//\"/\"\"}" | Escapar comillas en CSV |
| CSV BOM | printf '\xEF\xBB\xBF' | Compatibilidad Excel Windows |
| CSV sep. alt. | echo "campo1;campo2" | Para Excel español |
| HTML tabla | heredoc + echo <tr><td> | Tablas HTML desde datos |
| CSS inline | style="color:red" | Estilo en emails |
| Heredoc | cat << EOF > file | Plantillas multi-línea |
| jq construir | jq -n --arg k v '{k: \$k}' | JSON estructurado |
| jq validar | echo \$json | jq . | Pretty-print y validación |
| Markdown tabla | | h1 | h2 | + | --- | | Tablas para documentación |
| gnuplot bar | set style data histogram | Gráfico de barras PNG |
| gnuplot line | plot ... with lines | Series temporales PNG |
| gnuplot pie | with boxes linecolor rgb variable | Gráfico circular |
| mailx send | mailx -s "Asunto" dest < body | Correo desde terminal |
| mutt HTML | mutt -e "set content_type=text/html" | Email con body HTML |
| sendmail MIME | construir mensaje + sendmail -t | Envío SMTP directo |
| CID embed | Content-ID: <id> + <img src="cid:id"> | Imagen inline en email |
| Base64 inline | src="data:image/png;base64,..." | Imagen en HTML sin attachment |
| wkhtmltopdf | wkhtmltopdf input.html output.pdf | HTML a PDF |
| pandoc | pandoc input.md -o output.pdf | Markdown a PDF |
| pdfunite | pdfunite p1.pdf p2.pdf out.pdf | Combinar PDFs |
| awk aggregation | awk '{arr[\$1]++} END {print}' | Agregar datos de logs |
| Rotación GFS | diario/semanal/mensual + rm antiguos | No acumular reportes |
| Cron reportes | 0 6 * * * /ruta/script.sh | Programación horaria |
| Systemd timer | OnCalendar=daily | Timer moderno |
Más información
- Gnuplot demos — Ejemplos interactivos de gráficos gnuplot
- jq playground — Editor online de jq para probar consultas
- CSS inline para emails — Guía de soporte CSS en clientes de correo
- MIME multipart/related — Estándar para contenido relacionado (imágenes CID)
- Pandoc filters — Filtros Lua para transformar documentos
- wkhtmltopdf troubleshooting — Solución a problemas comunes
- Bash heredoc guide — Documentación oficial de heredocs
- GFS backup rotation — Esquema de rotación GFS
- Systemd timer examples — Ejemplos de timers systemd
- Unicode BOM — BOM UTF-8 y compatibilidad con Excel
Libros
- «Data Science at the Command Line» de Jeroen Janssens — Uso de herramientas UNIX para análisis de datos, incluyendo jq, awk y visualización
- «Classic Shell Scripting» de Arnold Robbins y Nelson H.F. Beebe — Capítulos sobre formateo de salida y reportes
- «Bash Cookbook» de Carl Albing, JP Vossen y Cameron Newham — Recetas para generación de reportes y envío de correos
- «The Linux Command Line» de William Shotts — Secciones sobre redirecciones, tuberías y procesamiento de texto
- «sed & awk» de Dale Dougherty y Arnold Robbins — Procesamiento de texto para agregar datos de logs
- «Gnuplot in Action» de Philipp K. Janert — Guía completa de gnuplot para gráficos profesionales
- «UNIX Power Tools» de Tim O’Reilly — Trucos de formateo y generación de reportes