El control de flujo en Bash tiene una particularidad que lo diferencia de casi cualquier otro lenguaje: la condición de un if no es una expresión booleana, sino un comando. Lo que evalúa Bash es el exit code de ese comando — 0 significa verdadero, cualquier otro valor significa falso. Esto no es una rareza del lenguaje; es coherente con toda la filosofía Unix: cada herramienta devuelve 0 en éxito y distinto de 0 en error. Cuando escribes if grep -q "error" /var/log/syslog, estás usando grep como condición directamente, sin comparar nada.
El operador [[ expresión ]] es un built-in de Bash diseñado para hacer tests de forma segura. No lo confundas con [, que en realidad es un comando externo (pruébalo: type [ vs type [[). La diferencia práctica es que [[ no hace word splitting sobre las variables, lo que significa que [[ -z $variable ]] funciona aunque $variable esté vacía o contenga espacios, mientras que [ -z $variable ] puede romperse. Además, [[ soporta && y || internamente, comparaciones con regex mediante =~, y patrones glob con ==. Úsalo siempre que trabajes en Bash puro; reserva [ solo si necesitas compatibilidad POSIX con sh.
¿Cuándo aplica todo esto? Prácticamente en cualquier script de administración: verificar si existe un archivo antes de procesarlo, iterar sobre una lista de hosts, validar parámetros de entrada. Lo que se rompe si no entiendes el modelo es predecible: variables con espacios que parten en múltiples palabras, condiciones que se evalúan al revés porque confundes el exit code, o bucles for que no expanden globs porque pusiste comillas donde no debías.
#!/usr/bin/env bash
# Script de rotación y auditoría de logs en /var/log/app/
set -euo pipefail
LOG_DIR="/var/log/app"
ARCHIVE_DIR="/var/log/app/archive"
MAX_SIZE_MB=10
NOTIFY_EMAIL="ops@ejemplo.com"
# Crea el directorio de archivo si no existe
if [[ ! -d "$ARCHIVE_DIR" ]]; then
mkdir -p "$ARCHIVE_DIR"
fi
# Verifica que el directorio de logs sea accesible antes de continuar
if [[ ! -r "$LOG_DIR" ]]; then
echo "Error: no se puede leer $LOG_DIR" >&2
exit 1
fi
procesar_log() {
local archivo="$1"
local nombre
nombre=$(basename "$archivo")
local tamaño_mb
# Divide entre 1024^2 para pasar de bytes a MB
tamaño_mb=$(( $(stat -c '%s' "$archivo") / 1048576 ))
# =~ compara contra expresión regular extendida; la captura queda en BASH_REMATCH
if [[ "$nombre" =~ ^app-([0-9]{4}-[0-9]{2}-[0-9]{2})\.log$ ]]; then
local fecha="${BASH_REMATCH[1]}"
echo "Procesando log con fecha: $fecha"
else
echo "Advertencia: nombre de archivo inesperado: $nombre" >&2
fi
# Operadores numéricos: -gt compara enteros, no cadenas
if [[ "$tamaño_mb" -gt "$MAX_SIZE_MB" ]]; then
gzip -c "$archivo" > "$ARCHIVE_DIR/${nombre}.gz"
rm "$archivo"
echo "Archivado: $nombre (${tamaño_mb} MB)"
elif [[ "$tamaño_mb" -gt 0 ]]; then
echo "OK: $nombre (${tamaño_mb} MB, sin archivar)"
else
echo "Vacío, ignorando: $nombre"
fi
}
# Glob *.log — sin comillas para que Bash expanda; falla si no hay coincidencias
# con nullglob activo el bucle simplemente no se ejecuta
shopt -s nullglob
archivos=( "$LOG_DIR"/*.log )
shopt -u nullglob
if [[ "${#archivos[@]}" -eq 0 ]]; then
echo "No hay archivos .log en $LOG_DIR"
exit 0
fi
for archivo in "${archivos[@]}"; do
# -f confirma que es un archivo regular, no un symlink roto ni un directorio
if [[ -f "$archivo" ]]; then
procesar_log "$archivo"
fi
done
# Resumen de archivos generados hoy usando rango numérico y aritmética
echo ""
echo "Archivos comprimidos generados en las últimas 24h:"
contador=0
while IFS= read -r linea; do
[[ -z "$linea" ]] && continue
(( contador++ ))
echo " $linea"
done < <(find "$ARCHIVE_DIR" -name "*.gz" -mtime -1 -printf "%f\n")
# case es más legible que una cadena de if/elif para valores discretos
case "$contador" in
0)
echo "Ninguno. Todo estaba bajo el límite."
;;
1)
echo "Un archivo archivado. Revisión recomendada."
;;
[2-9]|[1-9][0-9])
echo "Varios archivos archivados ($contador). Considera aumentar MAX_SIZE_MB."
;;
*)
# Envía alerta solo si sendmail está disponible
if command -v sendmail &>/dev/null; then
echo "Alerta: $contador archivos comprimidos hoy" \
| sendmail "$NOTIFY_EMAIL"
fi
echo "Demasiados archivos archivados: $contador"
;;
esac
Lo que está pasando en cada parte
La condición como comando aparece desde la primera línea funcional: if [[ ! -d "$ARCHIVE_DIR" ]]. Aquí [[ es el comando; devuelve 0 si el directorio no existe (! invierte el resultado). Fíjate también en if command -v sendmail &>/dev/null más abajo — sin [[ en absoluto, porque command -v ya devuelve 0 si el binario existe.
Las comillas alrededor de variables dentro de [[ son técnicamente opcionales ahí (a diferencia de [), pero mantenerlas es un hábito útil: el día que copies esa condición a un script POSIX, no te romperá nada.
El regex con =~ en [[ "$nombre" =~ ^app-([0-9]{4}-... ]] no va entre comillas en el lado derecho — si lo entrecomillas, Bash lo trata como cadena literal, no como regex. La captura completa queda en BASH_REMATCH[0] y los grupos en BASH_REMATCH[1], [2], etc.
El operador -gt en [[ "$tamaño_mb" -gt "$MAX_SIZE_MB" ]] hace comparación entera. Si usaras > dentro de [[, estarías comparando lexicográficamente — "9" > "10" sería verdadero, lo cual es un bug clásico.
El glob con nullglob merece atención. Sin él, si no hay archivos .log, el glob literal *.log no se expande y el array contiene la cadena "*.log" — el bucle for iteraría una vez sobre ese valor. Activar nullglob produce un array vacío, que luego detectamos con "${#archivos[@]}" -eq 0. Lo desactivamos con shopt -u nullglob para no afectar al resto del script.
El bucle while con sustitución de proceso while ... done < <(find ...) es la forma correcta de leer la salida de un comando línea a línea sin abrir una subshell. Si escribieras find ... | while read, el contador se incrementaría dentro de una subshell y su valor se perdería al salir del pipe — uno de los bugs más frecuentes en scripts Bash de nivel intermedio.
El case con patrón [2-9]|[1-9][0-9] ilustra que los patrones de case son globs, no regex: [2-9] es un rango de caracteres, | separa alternativas. Para números grandes tendrías que añadir más patrones o usar un if aritmético en el bloque *).
N° 74