LinuxBashAutomatizaciónTerminal

Bash scripting para automatizar tareas repetitivas

· · 11 min de lectura

Ver todo: Linux & Open Source

Si llevas un tiempo en Linux, seguramente ya has ejecutado comandos en la terminal. Sabes cómo navegar directorios, instalar paquetes, mover archivos. Pero sigue habiendo tareas que haces a mano: comprimir y copiar una carpeta antes de salir del trabajo, borrar archivos temporales que acumulan gigabytes, revisar si un servidor responde.

La pregunta no es si puedes automatizar eso. Puedes. La pregunta real es: ¿cuándo te vas a sentar a hacerlo?

Esta guía te da los fundamentos concretos para escribir Bash scripts que realmente funcionen — no snippets sueltos sin contexto, sino scripts con estructura, manejo de errores y la lógica para programarlos con cron. Usamos Bash 5.3, la versión estable actual en la mayoría de distros modernas desde mediados de 2025.

Lo esencial antes de escribir tu primer script

  • set -euo pipefail en el header de cada script es la diferencia entre bugs silenciosos y fallos detectables
  • Un script de Bash es simplemente un archivo de texto con comandos — la magia está en la estructura y el manejo de errores
  • trap permite limpiar archivos temporales y locks aunque el script falle en cualquier punto
  • cron es la forma estándar de programar ejecución automática; systemd timers es la alternativa moderna
  • ShellCheck detecta los errores más comunes antes de que tu script toque producción
  • Automatizar bien requiere pensar en los casos de falla, no solo en el caso feliz

Si quieres explorar más herramientas del ecosistema de terminal y Linux, visita Linux & Open Source en GeekShop.


Por qué Bash y no Python para automatizar

Es una pregunta legítima. Python es más legible, tiene manejo de errores más explícito y una biblioteca estándar enorme. Entonces, ¿para qué aprender Bash scripting?

El caso real

Bash gana cuando el trabajo es pegar comandos

Bash brilla en una cosa específica: encadenar herramientas de sistema. Si tu tarea implica mover archivos, llamar comandos de sistema, procesar output de otros programas, filtrar texto con grep o awk, o disparar scripts a horas específicas — Bash es más directo que Python para eso.

Además, Bash está disponible en cualquier sistema Linux o macOS sin instalar nada. Un script .sh funciona en un servidor mínimo donde Python ni siquiera está instalado. Para infraestructura, eso importa.

La regla práctica: si el script llama más de tres herramientas externas o manipula principalmente archivos y texto, Bash. Si necesitas estructuras de datos complejas, lógica de negocio o llamadas a APIs REST, Python.


La estructura base que todo script debe tener

Antes de escribir una sola línea de lógica, cada script necesita un esqueleto mínimo que lo hace predecible y seguro:

#!/usr/bin/env bash
# backup-diario.sh — Comprime y copia la carpeta de proyectos a un destino de backup
# Uso: ./backup-diario.sh [destino]
# Dependencias: tar, rsync

# Modo estricto: falla rápido y con razón
set -euo pipefail

# Variables configurables arriba, donde son fáciles de cambiar
readonly ORIGEN="${HOME}/proyectos"
readonly DESTINO="${1:-/mnt/backup}"
readonly TIMESTAMP="$(date +%Y%m%d_%H%M%S)"
readonly ARCHIVO_BACKUP="proyectos_${TIMESTAMP}.tar.gz"

# Limpieza en cualquier punto de salida (normal o error)
ARCHIVO_TEMP=""
trap 'cleanup' EXIT INT TERM

cleanup() {
  if [[ -n "${ARCHIVO_TEMP}" && -f "${ARCHIVO_TEMP}" ]]; then
    rm -f "${ARCHIVO_TEMP}"
    echo "Limpieza: archivo temporal eliminado"
  fi
}

# Función principal — la lógica va aquí
main() {
  echo "Iniciando backup: ${TIMESTAMP}"

  if [[ ! -d "${ORIGEN}" ]]; then
    echo "Error: directorio origen no existe: ${ORIGEN}" >&2
    exit 1
  fi

  ARCHIVO_TEMP="$(mktemp /tmp/backup_XXXXXX.tar.gz)"

  tar -czf "${ARCHIVO_TEMP}" -C "$(dirname "${ORIGEN}")" "$(basename "${ORIGEN}")"
  cp "${ARCHIVO_TEMP}" "${DESTINO}/${ARCHIVO_BACKUP}"

  echo "Backup completado: ${DESTINO}/${ARCHIVO_BACKUP}"
}

main "$@"

Esto puede parecer verboso para un script simple, pero cada parte tiene razón de existir. set -euo pipefail hace que el script falle inmediatamente si cualquier comando falla, si usas una variable sin definir, o si un comando en un pipe falla aunque no sea el último. trap garantiza que los archivos temporales se limpian aunque el script se interrumpa con Ctrl+C o falle en la mitad. readonly previene que sobreescribas variables importantes por accidente.


Manejo de argumentos y validación de entrada

Un script que solo funciona si le das exactamente los argumentos correctos no es un script robusto — es una trampa para tu yo de las 11pm.

Entrada robusta

Valida todo lo que viene de afuera

Los scripts bien escritos muestran uso cuando los llamas sin argumentos, validan que los archivos o directorios existan antes de operar sobre ellos, y dan mensajes de error útiles en stderr (no en stdout) cuando algo está mal.

# Función de ayuda — se muestra si faltan argumentos
usage() {
  cat <<EOF
Uso: $(basename "$0") [OPCIONES] <origen> <destino>

Opciones:
  -v    Modo verbose (muestra cada operación)
  -n    Dry-run (simula sin hacer cambios)
  -h    Muestra esta ayuda

Ejemplo:
  $(basename "$0") -v ~/documentos /mnt/usb/backup
EOF
  exit 1
}

# Verificar que se pasaron argumentos mínimos
if [[ $# -lt 2 ]]; then
  usage
fi

Siempre redirige los mensajes de error a stderr con >&2. Esto permite que quien llame al script pueda filtrar errores de output normal con 2>/dev/null o capturar solo los errores.


Logging: saber qué pasó cuando no estabas mirando

Si un script corre de madrugada por cron y falla, necesitas un registro. Sin logging, solo sabes que algo salió mal; con logging, sabes exactamente qué línea, con qué datos y a qué hora.

# Logger simple con timestamp y nivel
LOG_FILE="/var/log/mis-scripts/backup.log"

log() {
  local nivel="$1"
  local mensaje="$2"
  local timestamp
  timestamp="$(date '+%Y-%m-%d %H:%M:%S')"
  echo "[${timestamp}] [${nivel}] ${mensaje}" | tee -a "${LOG_FILE}"
}

# Uso en el script
log "INFO"  "Inicio de backup para ${ORIGEN}"
log "ERROR" "No se puede acceder a ${DESTINO}" >&2
log "OK"    "Backup completado: ${ARCHIVO_BACKUP} ($(du -sh "${ARCHIVO_TEMP}" | cut -f1))"

tee -a escribe a pantalla Y al archivo de log al mismo tiempo. Útil para scripts que corren en terminal pero también por cron.


Tabla comparativa: cron vs systemd timers

Ambos sirven para ejecutar scripts en momentos específicos. La elección depende de tu contexto:

Característica cron systemd timers
Disponibilidad Cualquier Unix/Linux Solo sistemas con systemd (mayoría de distros modernas)
Configuración Una línea en crontab Dos archivos: .service + .timer
Logging automático No (debes redirigir manualmente) Sí — journalctl -u nombre.service
Dependencias entre servicios No Sí — puede esperar que red o disco estén disponibles
Variables de entorno Limitadas (no carga .bashrc) Configurables en el archivo .service
Ejecución si se perdió un run No Sí — con Persistent=true
Curva de aprendizaje Baja Media
Ideal para Scripts rápidos, servidores legacy Tareas críticas, dependencias, logs integrados

Para la mayoría de scripts de automatización personal, cron es suficiente y más rápido de configurar. Para tareas de producción donde el logging y la fiabilidad importan, systemd timers es la opción correcta.


Programar con cron: la sintaxis que nadie recuerda de memoria

La sintaxis de cron tiene cinco campos antes del comando. De izquierda a derecha: minuto, hora, día del mes, mes, día de la semana.

# Editar el crontab del usuario actual
crontab -e

# Formato: minuto hora día-mes mes día-semana comando
# Caracteres especiales: * (cualquiera), */N (cada N), rangos con -, listas con ,

# Ejecutar backup-diario.sh todos los días a las 2:30am
30 2 * * * /home/usuario/scripts/backup-diario.sh >> /var/log/backup.log 2>&1

# Limpiar /tmp cada lunes a las 6am
0 6 * * 1 find /tmp -type f -mtime +7 -delete >> /var/log/limpieza.log 2>&1

# Cada 15 minutos, verificar espacio en disco
*/15 * * * * /home/usuario/scripts/check-disco.sh

# Primera hora del primer día de cada mes
0 1 1 * * /home/usuario/scripts/informe-mensual.sh

Detectar y notificar errores: el patrón que marca la diferencia

Un script silencioso que falla no es mejor que no tener script. Si automatizas algo importante, necesitas saber cuándo falla.

# Notificación simple por email (requiere mailutils o postfix configurado)
notificar_error() {
  local mensaje="$1"
  local hostname
  hostname="$(hostname)"
  echo "${mensaje}" | mail -s "[ERROR] Backup falló en ${hostname}" tu@email.com
}

# Usar con trap para capturar fallos del script
on_error() {
  local linea="$1"
  log "ERROR" "Script falló en línea ${linea}"
  notificar_error "El script backup-diario.sh falló en línea ${linea} a las $(date)"
}

trap 'on_error ${LINENO}' ERR

Para notificaciones sin servidor de correo, puedes usar curl para enviar un mensaje a un webhook de Slack, Discord o Telegram — la idea es la misma: cuando trap ... ERR se dispara, el script llama a tu función de notificación antes de salir.

Un script que falla silenciosamente es peor que no tener script. La automatización sin monitoreo es solo una fuente de sorpresas desagradables a destiempo.

ShellCheck: el linter que debería ser obligatorio

ShellCheck es una herramienta de análisis estático para scripts de shell. Detecta los errores más comunes antes de que el script toque producción: variables sin comillas, uso incorrecto de [ vs [[, problemas con set -e, arrays usados como strings, y docenas de patrones problemáticos más.

# Instalación
sudo apt install shellcheck        # Debian/Ubuntu
sudo dnf install ShellCheck        # Fedora/RHEL
brew install shellcheck            # macOS

# Uso básico
shellcheck mi-script.sh

# Con nivel de severidad (error, warning, info, style)
shellcheck --severity=warning mi-script.sh

# Verificar múltiples scripts
shellcheck scripts/*.sh
ShellCheck clasifica sus hallazgos por severidad y enlaza a la explicación de cada regla en shellcheck.net

Integrar ShellCheck en tu editor (VSCode tiene extensión, Vim/Neovim via ALE o null-ls) hace que los problemas aparezcan mientras escribes, no después. Si usas GitHub Actions o cualquier CI, añade shellcheck scripts/*.sh como paso obligatorio antes del deploy.


Errores comunes que cuestan tiempo


Un ejemplo completo: script de limpieza de logs

Para cerrar con algo que puedas adaptar directamente:

#!/usr/bin/env bash
# limpia-logs.sh — Borra logs de más de N días en un directorio
# Uso: ./limpia-logs.sh <directorio> [dias]

set -euo pipefail

readonly DIRECTORIO="${1:-/var/log/mi-app}"
readonly DIAS="${2:-30}"
readonly LOG_SCRIPT="/var/log/limpia-logs.log"

log() {
  echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "${LOG_SCRIPT}"
}

# Validar que el directorio existe
if [[ ! -d "${DIRECTORIO}" ]]; then
  log "ERROR: directorio no encontrado: ${DIRECTORIO}" >&2
  exit 1
fi

# Contar archivos antes de borrar (para el log)
total_antes="$(find "${DIRECTORIO}" -type f -name "*.log" -mtime +"${DIAS}" | wc -l)"

if [[ "${total_antes}" -eq 0 ]]; then
  log "INFO: no hay logs de más de ${DIAS} días en ${DIRECTORIO}"
  exit 0
fi

log "INFO: borrando ${total_antes} archivos de más de ${DIAS} días en ${DIRECTORIO}"

# -exec rm {} + es más eficiente que -exec rm {} \; (llama rm una vez con todos los archivos)
find "${DIRECTORIO}" -type f -name "*.log" -mtime +"${DIAS}" -exec rm -f {} +

log "OK: limpieza completada"

Para programarlo con cron, cada domingo a las 3am:

0 3 * * 0 /home/usuario/scripts/limpia-logs.sh /var/log/mi-app 30 >> /var/log/limpia-logs.log 2>&1

FAQ

¿Qué versión de Bash debo usar?

Bash 5.3 es la versión estable actual, lanzada en verano de 2025. Ubuntu 24.04 LTS trae Bash 5.2 por defecto; Fedora 41+ y Arch ya tienen 5.3. Para la mayoría de scripts de automatización no necesitas features específicas de 5.3 — lo que importa es que estés en Bash 5.x, no en Bash 3.x que trae macOS por defecto (que está congelado en esa versión por licencia).

Para verificar tu versión: bash --version.

¿cron o systemd timers para scripts nuevos?

Depende del contexto. Para scripts personales en tu máquina o servidor Linux moderno, systemd timers te dan logging automático con journalctl y la opción Persistent=true para recuperar ejecuciones perdidas. Para servidores legacy, VPS mínimos o compatibilidad garantizada, cron es más universal y más rápido de configurar.

¿Cómo depuro un script que funciona en terminal pero no en cron?

El 90% de los casos es una de dos cosas: el PATH de cron no incluye donde está el programa que llamas, o el script asume que está en un directorio específico. Empieza con echo $PATH al inicio del script en cron y revisa el output en el log. Luego añade cd /ruta/absoluta explícito si tu script depende del directorio de trabajo.

¿Es seguro usar rm -rf en scripts automatizados?

Solo si validas bien la variable que le pasas. El clásico accidente: rm -rf "${DIR}/" donde DIR está vacía se convierte en rm -rf /. La defensa estándar:

if [[ -z "${DIR}" ]]; then
  echo "Error: DIR no definida" >&2; exit 1
fi
rm -rf "${DIR:?Variable DIR vacía}"

La sintaxis ${VAR:?mensaje} hace que Bash falle con error si VAR está vacía o indefinida.

¿Vale la pena aprender Bash si ya sé Python?

Sí. Son complementarios. Bash es insuperable para encadenar herramientas de sistema, procesar streams de texto, manejar argumentos y archivos en pocas líneas. Python gana en lógica compleja, estructuras de datos, testing y mantenibilidad a largo plazo. En la práctica, los mejores entornos de automatización usan ambos: Bash para el glue code y Python para la lógica de negocio.


Si estás empezando desde cero con la terminal, la guía Cómo empezar en Linux sin frustrarte cubre los comandos base y la lógica del sistema de archivos que necesitas antes de escribir tu primer script. Y si quieres entender cómo los mismos principios aplican en seguridad y administración de sistemas, continúa con Seguridad básica y hacking ético: por dónde empezar.

Fuentes

La versión de Bash y sus features se apoyan en la fuente oficial de GNU:

  • GNU BashGNU Project (versión estable 5.3 y novedades)

¿Te late lo que viene?

Regístrate y entérate cuando abramos.

Avísenme cuando abran