29

Generación de reportes en Bash

Vistas: 0
Tutorial: Scripts en Bash
Generación de reportes en Bash

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, sed y 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:

  1. BOM (Byte Order Mark): Excel no detecta UTF-8 sin BOM para CSV. Añadir \xEF\xBB\xBF al inicio fuerza UTF-8 en Excel para Windows.
  2. 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.
  3. 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 con printf y 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

ConceptoSintaxis / ComandoPropósito
CSV básicoecho "campo1,campo2"Datos separados por comas
CSV escape"${campo//\"/\"\"}"Escapar comillas en CSV
CSV BOMprintf '\xEF\xBB\xBF'Compatibilidad Excel Windows
CSV sep. alt.echo "campo1;campo2"Para Excel español
HTML tablaheredoc + echo <tr><td>Tablas HTML desde datos
CSS inlinestyle="color:red"Estilo en emails
Heredoccat << EOF > filePlantillas multi-línea
jq construirjq -n --arg k v '{k: \$k}'JSON estructurado
jq validarecho \$json | jq .Pretty-print y validación
Markdown tabla| h1 | h2 | + | --- |Tablas para documentación
gnuplot barset style data histogramGráfico de barras PNG
gnuplot lineplot ... with linesSeries temporales PNG
gnuplot piewith boxes linecolor rgb variableGráfico circular
mailx sendmailx -s "Asunto" dest < bodyCorreo desde terminal
mutt HTMLmutt -e "set content_type=text/html"Email con body HTML
sendmail MIMEconstruir mensaje + sendmail -tEnvío SMTP directo
CID embedContent-ID: <id> + <img src="cid:id">Imagen inline en email
Base64 inlinesrc="data:image/png;base64,..."Imagen en HTML sin attachment
wkhtmltopdfwkhtmltopdf input.html output.pdfHTML a PDF
pandocpandoc input.md -o output.pdfMarkdown a PDF
pdfunitepdfunite p1.pdf p2.pdf out.pdfCombinar PDFs
awk aggregationawk '{arr[\$1]++} END {print}'Agregar datos de logs
Rotación GFSdiario/semanal/mensual + rm antiguosNo acumular reportes
Cron reportes0 6 * * * /ruta/script.shProgramación horaria
Systemd timerOnCalendar=dailyTimer moderno

Más información

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

Deja una respuesta