25

Automatización con Cron y Systemd

Vistas: 0
Tutorial: Scripts en Bash
Automatización con Cron y Systemd

Un servidor sin automatización es como un reloj sin cuerda: funciona, pero solo mientras alguien esté allí para darle cuerda. Los scripts que has escrito en los capítulos anteriores — respaldos, monitorización, limpieza de logs, generación de reportes — necesitan ejecutarse sin intervención humana, en horarios precisos y con garantías de ejecución.

Aquí entran cron y systemd-timer:

  • Cron existe desde 1975. Es el programador de tareas más conocido, más simple y más ubicuo. Está en cada servidor Linux, en cada contenedor, en cada instalación mínima.
  • Systemd-Timer es el reemplazo moderno integrado en systemd. Ofrece más control, mejor logging, dependencias entre unidades, y capacidades que cron no tiene: despertar el sistema, ejecutar tras un evento, calendar syntax más expresiva.

En este capítulo aprenderás a:

  • Programar tareas con cron, desde lo básico hasta patrones profesionales
  • Crear timers de systemd con unidades .service y .timer
  • Manejar logging, errores y notificaciones en ambos sistemas
  • Decidir cuándo usar cada uno según el contexto
  • Construir herramientas que gestionen tareas programadas automáticamente

Filosofía del capítulo: no se trata de elegir un ganador. Cron y systemd-timer coexisten y se complementan. Cron es simple, directo y funciona en cualquier sitio. Systemd-timer es potente, expresivo y se integra con el ecosistema systemd. Sabrás cuándo usar cada uno.

Cron — El clásico de la automatización

Sintaxis de cron

Cada línea en un crontab describe una tarea periódica con cinco campos de tiempo seguidos del comando:

┌───────── minuto (0-59)
│ ┌──────── hora (0-23)
│ │ ┌────── día del mes (1-31)
│ │ │ ┌──── mes (1-12, o nombres: jan, feb, mar...)
│ │ │ │ ┌── día de la semana (0-7, 0=domingo, 7=domingo; o nombres: sun, mon...)
│ │ │ │ │
* * * * * comando a ejecutar

Ejemplos básicos:

# Cada minuto
* * * * * /home/user/script.sh

# Cada día a las 2:30 AM
30 2 * * * /home/user/backup.sh

# Cada lunes a las 9:00 AM
0 9 * * 1 /home/user/reporte.sh

# Primer día de cada mes a medianoche
0 0 1 * * /home/user/facturacion.sh

# Cada 15 minutos
*/15 * * * * /home/user/healthcheck.sh

# Cada hora en el minuto 0
0 * * * * /home/user/rotar_logs.sh

Operadores especiales:

OperadorSignificadoEjemplo
*Todos los valores* * * * * = cada minuto
*/nCada n unidades*/5 * * * * = cada 5 minutos
a,b,cLista de valores0,30 * * * * = minutos 0 y 30
a-bRango9-17 * * * * = cada hora de 9 a 17
a-b/nRango con paso9-17/2 * * * * = cada 2 horas entre 9 y 17

@reboot, @daily, @hourly — Atajos named

Cron ofrece atajos named que simplifican los patrones más comunes:

AtajoEquivalenteCuándo se ejecuta
@rebootAl arrancar el sistema
@yearly / @annually0 0 1 1 *1 de enero a medianoche
@monthly0 0 1 * *Primer día del mes a medianoche
@weekly0 0 * * 0Domingo a medianoche
@daily0 0 * * *Cada día a medianoche
@hourly0 * * * *Cada hora en el minuto 0
# Ejecutar script al arrancar
@reboot /home/user/iniciar_servicio.sh

# Backup diario
@daily /home/user/backup_diario.sh

# Limpieza semanal
@weekly /home/user/limpiar_temporales.sh

⚠️ Atención: @reboot no tiene equivalente en systemd-timer. Si necesitas ejecutar algo al arranque, cron es la solución más simple. En systemd usarías una unidad .service con WantedBy=multi-user.target.

crontab -e, crontab -l, crontab -r

La herramienta crontab gestiona los archivos crontab de cada usuario:

# Editar tu crontab personal
crontab -e

# Listar tus tareas programadas
crontab -l

# Eliminar tu crontab (¡cuidado!)
crontab -r

# Editar el crontab de otro usuario (root)
sudo crontab -u www-data -e

# Listar tareas de otro usuario
sudo crontab -u postgres -l

¿Qué editor usa? crontab -e respeta la variable EDITOR:

export EDITOR=nano
crontab -e   # Usa nano

# O para siempre:
echo 'export EDITOR=vim' >> ~/.bashrc

Formato del archivo crontab: Cada línea es una tarea. Las líneas que empiezan con # son comentarios. Las variables de entorno se pueden definir antes de las tareas:

# ─── Variables de entorno ───
PATH=/usr/local/bin:/usr/bin:/bin
SHELL=/bin/bash
MAILTO=admin@example.com

# ─── Tareas ───
# Backup nocturno
30 2 * * * /home/user/backup.sh

# Monitorización cada hora
0 * * * * /home/user/healthcheck.sh

El entorno de cron — PATH, SHELL, HOME

Ésta es la causa #1 de errores en cron. Cuando cron ejecuta un script, NO carga tu .bashrc, .bash_profile ni .profile. El entorno es mínimo:

# Lo que cron NO tiene por defecto:
# - PATH personalizado (~/.local/bin, /home/user/.nvm/...)
# - Variables de entorno (DB_HOST, API_KEY, ...)
# - Alias
# - Funciones del shell

# Lo que cron SÍ tiene (usualmente):
PATH=/usr/bin:/bin
SHELL=/bin/sh   # ¡No bash!
HOME=/home/user
LOGNAME=user

Solución profesional — nunca confíes en el entorno:

# ❌ Falla porque PATH no incluye /usr/local/bin
# (suponiendo que el script usa comandos allí)
0 3 * * * /home/user/mi_script.sh

# ✅ Fijar PATH explícitamente al inicio del crontab
PATH=/usr/local/bin:/usr/bin:/bin
SHELL=/bin/bash
0 3 * * * /home/user/mi_script.sh

# ✅ O mejor: fijar PATH dentro del propio script
# En mi_script.sh:
#!/bin/bash
export PATH="/usr/local/bin:/usr/bin:/bin"

Regla de oro: en tu script, siempre declara PATH completo al inicio. Así el script funciona igual desde cron, desde terminal o desde otro script.

Logging en cron — syslog, MAILTO, logs personalizados

Cron registra sus ejecuciones en el syslog del sistema:

# Ver ejecuciones de cron
grep CRON /var/log/syslog

# Con journalctl (sistemas con systemd)
journalctl -u cron -n 50

# Ver solo errores de cron
journalctl -u cron -p err

MAILTO — notificaciones por correo:

# En el crontab:
MAILTO=admin@example.com

# Si una tarea produce salida (stdout o stderr),
# cron envía esa salida por correo a MAILTO.
# Si no hay salida, no envía correo.

# Desactivar correo:
MAILTO=""

# Enviar a múltiples destinatarios:
MAILTO="admin1@e.com,admin2@e.com"

Logs personalizados — la práctica profesional:

# Redirigir toda la salida a un archivo de log
0 3 * * * /home/user/backup.sh >> /var/log/backup.log 2>&1

# Con timestamp por ejecución
0 3 * * * /home/user/backup.sh >> "/var/log/backup/backup_$(date +\%F).log" 2>&1

# ⚠️ ¡El % en crontab hay que escaparlo! Se escribe \%
# Sin escapar, cron interpreta % como salto de línea

Logging estructurado desde el script:

#!/bin/bash
# En tu script, no en el crontab
log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"
}

log "Iniciando backup..."
# ... operaciones ...
log "Backup completado"

Patrones comunes de cron

Backup diario con rotación:

# Crontab
PATH=/usr/local/bin:/usr/bin:/bin
30 2 * * * /home/user/backup_diario.sh >> /var/log/backup.log 2>&1

Monitorización cada 5 minutos:

*/5 * * * * /home/user/healthcheck.sh >> /var/log/healthcheck.log 2>&1

Rotación de logs cada hora:

0 * * * * /home/user/rotar_logs.sh

Limpieza semanal de temporales:

0 6 * * 0 /home/user/limpiar_temporales.sh

Sincronización con servidor remoto:

*/30 * * * * /usr/bin/rsync -az /datos/ usuario@remoto:/backups/

Limitaciones de cron

Cron es simple, pero tiene limitaciones importantes:

  1. No hay dependencias — no puedes decir «ejecuta esto tras aquello»
  2. No hay logging nativo — dependes de syslog o redirecciones manuales
  3. Entorno mínimo — causa infinita de bugs silenciosos
  4. Sin persistencia — si el sistema estaba apagado, la tarea se pierde
  5. Sin jitter controlado — no hay forma de esparcir aleatoriamente la ejecución
  6. No puede despertar el sistema — si la máquina está dormida, la tarea no se ejecuta
  7. Resolución de 1 minuto — no puedes programar tareas cada 30 segundos

Systemd-Timer resuelve todas estas limitaciones.

Systemd-Timer — El sucesor moderno

¿Qué es un timer de systemd?

Un timer de systemd es una unidad que dispara la ejecución de una unidad de servicio (.service) asociada. Funciona con dos archivos:

  • algo.service — define qué ejecutar (el script, el comando)
  • algo.timer — define cuándo ejecutarlo (el calendario, frecuencia)

Ambos llevan el mismo nombre base y se colocan en /etc/systemd/system/.

Ventajas frente a cron:

CaracterísticaCronSystemd-Timer
DependenciasNoSí (After=, Requires=)
LoggingRedirección manualjournalctl nativo
Persistencia tras apagadoNoSí (Persistent=true)
Despertar sistemaNoSí (WakeSystem=true)
Jitter aleatorioNoSí (RandomizedDelaySec=)
Precisión configurableNoSí (AccuracySec=)
EntornoMínimoHereda del sistema
Transient oneshotsNoSí (systemd-run)

Crear una unidad .service

La unidad .service define qué ejecutar. Es un archivo de texto en /etc/systemd/system/.

Ejemplo: backup-diario.service

[Unit]
Description=Backup diario de la base de datos
Documentation=https://example.com/backup
Wants=network-online.target
After=network-online.target

[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup_db.sh
User=postgres
Group=postgres
Environment=DB_HOST=localhost
Environment=DB_NAME=produccion
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target

Explicación:

CampoDescripción
Type=oneshotEl servicio ejecuta una acción y termina (no es un daemon)
ExecStart=Comando a ejecutar (ruta absoluta siempre)
User= / Group=¿Con qué usuario se ejecuta?
Environment=Variables de entorno para el script
StandardOutput=journal / StandardError=journalLa salida va al log de systemd

Para servicios que sí son daemon (como un servidor web), usarías Type=simple o Type=forking.

Crear una unidad .timer

El archivo .timer define cuándo ejecutar el servicio asociado. Debe tener el mismo nombre base que el .service.

Ejemplo: backup-diario.timer

[Unit]
Description=Timer para backup diario
Requires=backup-diario.service

[Timer]
OnCalendar=daily
Persistent=true
RandomizedDelaySec=1h
AccuracySec=1m

[Install]
WantedBy=timers.target

Explicación de las opciones del timer:

CampoDescripción
OnCalendar=Especifica cuándo ejecutar (formato calendario)
Persistent=trueSi el sistema estaba apagado, ejecuta al encender
RandomizedDelaySec=1hAñade hasta 1 hora de retardo aleatorio
AccuracySec=1mPrecisión de 1 minuto (menos preciso = más eficiente)
OnBootSec=Ejecuta N tiempo tras el arranque
OnUnitActiveSec=Ejecuta N tiempo tras la última activación
WakeSystem=trueDespierta el sistema si está dormido

Calendar syntax — OnCalendar=

La sintaxis de OnCalendar= es más expresiva que cron. Sigue el formato:

OnCalendar=<día_de_semana> <fecha/hora>

Ejemplos de calendar syntax:

# Diario a las 3:30 AM
OnCalendar=daily
# Equivale a: *-*-* 00:00:00

# A las 3:30 AM cada día
OnCalendar=*-*-* 03:30:00

# Cada lunes a las 9 AM
OnCalendar=Mon *-*-* 09:00:00

# Cada hora
OnCalendar=hourly
# Equivale a: *-*-* *:00:00

# Cada 15 minutos
OnCalendar=*:0/15

# Primer día de cada mes a medianoche
OnCalendar=*-*-01 00:00:00

# Días laborables a las 8 AM
OnCalendar=Mon..Fri *-*-* 08:00:00

# Cada 2 horas
OnCalendar=*:0/2

# Varias especificaciones (se evalúan como OR)
OnCalendar=Mon *-*-* 09:00:00
OnCalendar=Fri *-*-* 17:00:00
# Se ejecuta lunes a las 9 y viernes a las 5

Validar calendar syntax:

# Ver cuándo se disparará un timer
systemd-analyze calendar "Mon *-*-* 09:00:00"
# → next el Mon 2026-06-15 09:00:00 UTC
# → luego cada 7 días

# Ver el timer en formato crontab-like
systemd-analyze calendar "Mon..Fri *-*-* 08:00:00" --iterations 3

# Probar una expresión antes de escribirla
systemd-analyze verify /etc/systemd/system/mi-timer.timer

Persistent=true — No perder ejecuciones

Una de las mayores ventajas sobre cron: si el sistema estaba apagado cuando debía ejecutarse la tarea, con Persistent=true se ejecutará inmediatamente al encender.

[Timer]
OnCalendar=daily
Persistent=true

Si el servidor se apagó a las 23:00 y el timer debía ejecutarse a las 3:00, al arrancar al día siguiente a las 8:00, el timer se dispara inmediatamente.

⚠️ Cuidado: si el sistema estuvo apagado varios días, se ejecutará una sola vez, no una por cada ejecución perdida.

WakeSystem=true — Despertar el sistema

¿Quieres que el servidor se encienda para hacer un backup nocturno y se vuelva a dormir? WakeSystem=true usa rtcwake internamente:

[Timer]
OnCalendar=*-*-* 03:00:00
WakeSystem=true

Requiere que el hardware soporte despertar por RTC (reloj en tiempo real). Muy útil en servidores de bajo consumo o estaciones de trabajo.

RandomizedDelaySec y AccuracySec — Jitter y precisión

RandomizedDelaySec: añade un retardo aleatorio para evitar que todas las tareas se ejecuten simultáneamente (el «thundering herd»):

[Timer]
OnCalendar=daily
RandomizedDelaySec=1h  # Ejecuta entre las 00:00 y la 01:00

AccuracySec: controla cuán precisa debe ser la ejecución. Mayor precisión = más despertar del sistema:

[Timer]
OnCalendar=hourly
AccuracySec=1s    # ⚠️ Alta precisión, más consumo
AccuracySec=1h    # ✅ Baja precisión, máximo ahorro (por defecto 1min)

Para tareas que no necesitan precisión de segundos (backups, limpieza), AccuracySec=1h reduce significativamente el consumo energético.

Gestionar timers con systemctl

# Activar e iniciar un timer
sudo systemctl enable backup-diario.timer
sudo systemctl start backup-diario.timer

# Ver estado
sudo systemctl status backup-diario.timer
sudo systemctl status backup-diario.service

# Listar todos los timers activos
systemctl list-timers --all

# Ver solo timers del usuario
systemctl --user list-timers

# Desactivar
sudo systemctl stop backup-diario.timer
sudo systemctl disable backup-diario.timer

# Recargar cambios tras editar unidades
sudo systemctl daemon-reload

# Ejecutar el servicio manualmente (sin esperar al timer)
sudo systemctl start backup-diario.service

# Ver próxima ejecución
systemctl list-timers backup-diario.timer

Salida de systemctl list-timers:

NEXT                         LEFT        LAST                         PASSED    UNIT                    ACTIVATES
Mon 2026-06-15 03:00:00 UTC 12h left    Sun 2026-06-14 03:00:05 UTC  11h ago   backup-diario.timer     backup-diario.service
Tue 2026-06-16 02:00:00 UTC 1 day 11h   Mon 2026-06-15 02:00:04 UTC  39min ago limpieza-log.timer      limpieza-log.service

journalctl — Ver el log del timer

Systemd registra automáticamente todo lo que hace el timer y el servicio:

# Ver todas las ejecuciones del timer
journalctl -u backup-diario.timer -n 50

# Ver la salida del servicio
journalctl -u backup-diario.service -n 50

# Seguir en tiempo real
journalctl -u backup-diario.service -f

# Ver solo errores
journalctl -u backup-diario.service -p err

# Ver ejecuciones de ayer
journalctl -u backup-diario.service --since yesterday --until today

# Formato detallado
journalctl -u backup-diario.service -o verbose

Timers transitorios con systemd-run

No necesitas crear archivos para timers de un solo uso o pruebas:

# Ejecutar un script cada hora durante 3 horas (sin crear unidades)
sudo systemd-run --on-calendar='*:0/1' --unit=test-timer /home/user/script.sh

# Ejecutar una vez tras 5 minutos
sudo systemd-run --on-active=5m /home/user/test.sh

# Ver timers transitorios
systemctl list-timers --all | grep run-

Los timers creados con systemd-run son transitorios: desaparecen al reiniciar. Ideales para pruebas o tareas ad-hoc.

Cron vs Systemd-Timer — ¿Cuándo usar cada uno?

SituaciónRecomendaciónMotivo
Script simple, entorno mínimoCronMenos archivos, menos conceptos
Necesito logging nativoSystemd-Timerjournalctl sin configurar
El sistema se apaga por las nochesSystemd-TimerPersistent=true
tarea @reboot (al arrancar)CronAtajo directo, sin unidades
Necesito dependencias (red, disco, etc.)Systemd-TimerAfter=, Requires=
Varias frecuencias para misma tareaSystemd-TimerMúltiples .timer → un .service
Entorno heredado, servidor sin systemdCronNo hay alternativa
Precisión de segundosSystemd-TimerAccuracySec=1s
Despertar sistema dormidoSystemd-TimerWakeSystem=true
Una sola máquina, tarea simpleCualquieraEl que conozcas mejor
Infraestructura como código, reproducibleSystemd-TimerArchivos versionables

Regla práctica: si tu tarea cabe en una línea de crontab y no necesita logging sofisticado, usa cron. Si necesitas logging, persistencia, dependencias o precisión, usa systemd-timer.

Funciones profesionales

Funciones reutilizables para tu biblioteca personal (~/.bashrc o librería aparte):

# ──────────────────────────────────────────────
# Funciones profesionales para automatización
# ──────────────────────────────────────────────

# install_cron_job — Añade una tarea a crontab si no existe
# Uso: install_cron_job <expresión_tiempo> <comando> [identificador]
install_cron_job() {
    local time_expr="$1"
    local command="$2"
    local identifier="${3:-$command}"

    local crontab_line="${time_expr} ${command}"

    if crontab -l 2>/dev/null | grep -Fq "$identifier"; then
        echo "✓ Tarea ya existe: $identifier"
        return 0
    fi

    (crontab -l 2>/dev/null; echo "$crontab_line") | crontab -
    echo "✓ Tarea añadida: $identifier"
}

# remove_cron_job — Elimina una tarea del crontab por identificador
# Uso: remove_cron_job <identificador>
remove_cron_job() {
    local identifier="$1"

    if ! crontab -l 2>/dev/null | grep -Fq "$identifier"; then
        echo "⚠ Tarea no encontrada: $identifier"
        return 1
    fi

    crontab -l 2>/dev/null | grep -Fv "$identifier" | crontab -
    echo "✓ Tarea eliminada: $identifier"
}

# install_systemd_timer — Crea e instala un timer systemd
# Uso: install_systemd_timer <nombre> <script> <calendar> [user]
install_systemd_timer() {
    local name="$1"
    local script="$2"
    local calendar="$3"
    local user="${4:-$USER}"

    local unit_dir="/etc/systemd/system"

    # Crear .service
    sudo tee "${unit_dir}/${name}.service" > /dev/null <<SERVICEEOF
[Unit]
Description=Servicio para ${name}

[Service]
Type=oneshot
ExecStart=${script}
User=${user}
StandardOutput=journal
StandardError=journal
SERVICEEOF

    # Crear .timer
    sudo tee "${unit_dir}/${name}.timer" > /dev/null <<TIMEREOF
[Unit]
Description=Timer para ${name}
Requires=${name}.service

[Timer]
OnCalendar=${calendar}
Persistent=true
RandomizedDelaySec=30m

[Install]
WantedBy=timers.target
TIMEREOF

    sudo systemctl daemon-reload
    sudo systemctl enable --now "${name}.timer"

    echo "✓ Timer instalado: ${name}.timer (${calendar})"
    systemctl list-timers "${name}.timer" --no-pager 2>/dev/null
}

# list_all_timers — Muestra todos los timers con formato legible
# Uso: list_all_timers
list_all_timers() {
    echo "=== Timers de systemd ==="
    systemctl list-timers --all --no-pager 2>/dev/null || echo "(no hay timers o no hay systemd)"

    echo ""
    echo "=== Tareas de cron ==="
    if crontab -l 2>/dev/null; then
        :
    else
        echo "(no hay tareas cron para $USER)"
    fi
}

Script completo: gestor_tareas_programadas.sh

Un gestor unificado de tareas programadas que soporta tanto cron como systemd-timer, en ~100 líneas:

#!/bin/bash
# gestor_tareas_programadas.sh — Gestiona tareas cron y systemd-timer
# Uso: ./gestor_tareas_programadas.sh <cmd> [args]
# Comandos: add-cron, add-timer, list, remove, log, test
set -euo pipefail

GRN='\033[0;32m'; YLW='\033[1;33m'; RED='\033[0;31m'; NC='\033[0m'
ok()  { echo -e "${GRN}✓${NC} $*"; }
warn(){ echo -e "${YLW}⚠${NC} $*" >&2; }
err() { echo -e "${RED}✗${NC} $*" >&2; }

# ── Validar script ──
validar_script() {
    local s="$1"
    if [[ ! -f "$s" ]]; then err "Script '$s' no existe"; return 1; fi
    if [[ ! -x "$s" ]]; then warn "Script '$s' no ejecutable — usa chmod +x"; return 1; fi
    ok "Script válido: $s"
}

# ── add-cron — Añadir tarea cron ──
cmd_add_cron() {
    local expr="${1:?Uso: add-cron <expr> <script> [id]}"
    local script="${2:?Falta ruta del script}"
    local id="${3:-$script}"; validar_script "$script" || return 1
    crontab -l 2>/dev/null | grep -Fq "$id" && { warn "'$id' ya existe"; return 1; }
    (crontab -l 2>/dev/null; echo "$expr $script >> /var/log/cron_$(basename "$script").log 2>&1") | crontab -
    ok "Cron añadido: $id ($expr)"
}

# ── add-timer — Añadir timer systemd ──
cmd_add_timer() {
    local name="${1:?Uso: add-timer <nombre> <script> <cal>}"
    local script="${2:?Falta ruta del script}"
    local cal="${3:?Falta calendario (ej: daily)}"; validar_script "$script" || return 1
    local d="/etc/systemd/system"
    [[ -f "$d/$name.timer" ]] && { warn "Timer '$name' ya existe"; return 1; }
    sudo tee "$d/$name.service" > /dev/null <<EOF
[Unit]
Description=Servicio para $name
[Service]
Type=oneshot
ExecStart=$script
User=$USER
StandardOutput=journal
StandardError=journal
EOF
    sudo tee "$d/$name.timer" > /dev/null <<EOF
[Unit]
Description=Timer para $name
Requires=$name.service
[Timer]
OnCalendar=$cal
Persistent=true
RandomizedDelaySec=15m
AccuracySec=1min
[Install]
WantedBy=timers.target
EOF
    sudo systemctl daemon-reload && sudo systemctl enable --now "$name.timer"
    ok "Timer añadido: $name ($cal)"
}

# ── list — Listar todas las tareas ──
cmd_list() {
    echo "═══ CRON ═══"
    crontab -l 2>/dev/null || warn "No hay tareas cron para $USER"
    echo "═══ SYSTEMD-TIMER ═══"
    if command -v systemctl &>/dev/null; then
        systemctl list-timers --all --no-pager 2>/dev/null | head -20
    else
        warn "systemd no disponible"
    fi
}

# ── remove — Eliminar tarea por identificador ──
cmd_remove() {
    local id="${1:?Falta identificador}"
    if crontab -l 2>/dev/null | grep -Fq "$id"; then
        crontab -l 2>/dev/null | grep -Fv "$id" | crontab -
        ok "Tarea cron '$id' eliminada"; return 0
    fi
    if [[ -f "/etc/systemd/system/$id.timer" ]]; then
        sudo systemctl disable --now "$id.timer" 2>/dev/null || true
        sudo rm -f "/etc/systemd/system/$id.timer" "/etc/systemd/system/$id.service"
        sudo systemctl daemon-reload
        ok "Timer '$id' eliminado"; return 0
    fi
    err "No se encontró '$id'"
    return 1
}

# ── log — Ver log de tarea ──
cmd_log() {
    local id="${1:?Falta identificador}"
    local lf="/var/log/cron_${id}.log"
    [[ -f "$lf" ]] && { echo "=== $lf ==="; tail -20 "$lf"; return 0; }
    systemctl list-timers --all --no-pager 2>/dev/null | grep -q "$id" \
        && { journalctl -u "${id}.service" -n 30 --no-pager; return 0; }
    err "No se encontró log para '$id'"; return 1
}

# ── test — Validar y probar script ──
cmd_test() {
    local script="${1:?Falta ruta del script}"; validar_script "$script" || return 1
    bash -n "$script" && ok "Sintaxis correcta" || { err "Error de sintaxis"; return 1; }
    echo -n "¿Ejecutar script? (s/N): "; read -r r
    [[ "$r" =~ ^[sS]$ ]] && bash "$script"
}

# ── Main ──
main() {
    local cmd="${1:-help}"; shift 2>/dev/null || true
    case "$cmd" in
        add-cron) cmd_add_cron "$@" ;; add-timer) cmd_add_timer "$@" ;;
        list) cmd_list ;; remove) cmd_remove "$@" ;;
        log) cmd_log "$@" ;; test) cmd_test "$@" ;;
        help|*)
            echo "Uso: $0 <comando> [args]"
            echo "  add-cron <expr> <script> [id]    — Añadir tarea cron"
            echo "  add-timer <nombre> <script> <cal> — Añadir timer systemd"
            echo "  list                              — Listar tareas"
            echo "  remove <id>                       — Eliminar tarea"
            echo "  log <id>                          — Ver log"
            echo "  test <script>                     — Validar script"
            echo "Ej: $0 add-cron '30 2 * * *' /home/user/backup.sh backup"
            echo "    $0 add-timer backup /home/user/backup.sh daily"
            echo "    $0 list"
    esac
}
main "$@"

Cómo usarlo:

chmod +x gestor_tareas_programadas.sh

# Añadir tarea cron (backup diario a las 2:30 AM)
./gestor_tareas_programadas.sh add-cron '30 2 * * *' /home/user/backup.sh backup

# Añadir timer systemd (cada hora)
./gestor_tareas_programadas.sh add-timer healthcheck /home/user/healthcheck.sh hourly

# Listar todas las tareas
./gestor_tareas_programadas.sh list

# Ver log de una tarea
./gestor_tareas_programadas.sh log backup

# Probar script antes de añadirlo
./gestor_tareas_programadas.sh test /home/user/nuevo_script.sh

# Eliminar tarea
./gestor_tareas_programadas.sh remove backup

Errores comunes (10)

Error 1: Script funciona en terminal pero falla en cron

# ❌ El script usa comandos que no están en el PATH de cron
# En script.sh (funciona en terminal, falla en cron):
#!/bin/bash
docker exec mycontainer backup.sh  # docker no está en PATH de cron

# ✅ Fijar PATH al inicio del script
#!/bin/bash
export PATH="/usr/local/bin:/usr/bin:/bin"
# O usar rutas absolutas:
/usr/bin/docker exec mycontainer /usr/local/bin/backup.sh

Error 2: No escapar el símbolo % en crontab

# ❌ Cron interpreta % como salto de línea
0 3 * * * /home/user/backup.sh > /var/log/backup_$(date +%Y%m%d).log

# ✅ Escapar con \%
0 3 * * * /home/user/backup.sh > /var/log/backup_$(date +\%Y\%m\%d).log

# ✅ Mejor: poner el date dentro del script, no en el crontab

Error 3: No redirigir salida y no saber por qué falló

# ❌ La salida se pierde (o va a MAILTO si está configurado)
0 3 * * * /home/user/script.sh

# ✅ Redirigir a log
0 3 * * * /home/user/script.sh >> /var/log/script.log 2>&1

Error 4: Confundir el shell de cron

# ❌ El shell por defecto de cron es /bin/sh, no /bin/bash
# Si el script usa sintaxis específica de bash ([[ ]], arrays, etc.)
0 3 * * * /home/user/script_con_arrays.sh  # Falla en /bin/sh

# ✅ Fijar SHELL en crontab
SHELL=/bin/bash
0 3 * * * /home/user/script_con_arrays.sh

# ✅ O mejor: usar shebang correcto y hacer el script ejecutable
# El shebang #!/bin/bash prevalece sobre SHELL

Error 5: systemd — Olvidar hacer daemon-reload tras editar unidades

# ❌ Editas .service o .timer y systemctl no reconoce los cambios
sudo systemctl start mi-timer.timer
# Failed to start mi-timer.timer: Unit mi-timer.timer not found.

# ✅ Siempre recargar tras editar
sudo systemctl daemon-reload
sudo systemctl start mi-timer.timer

Error 6: No usar rutas absolutas en ExecStart de systemd

# ❌ Ruta relativa o comando sin ruta completa
[Service]
ExecStart=mi_script.sh  # ¡FALLA!

# ✅ Ruta absoluta siempre
[Service]
ExecStart=/usr/local/bin/mi_script.sh

Error 7: Timer systemd no se activa por error en OnCalendar

# ❌ Sintaxis incorrecta (timer nunca se dispara)
[Timer]
OnCalendar=everyday  # No es válido

# ✅ Sintaxis correcta
[Timer]
OnCalendar=daily
# O:
OnCalendar=*-*-* 00:00:00

# ✅ Validar antes
systemd-analyze calendar "daily"

Error 8: Confundir enable con start en systemd

# ❌ Solo enable (activa al arranque, pero no ahora)
sudo systemctl enable mi-timer.timer
# El timer no está activo hasta el próximo reinicio

# ✅ Hacer ambos: enable --now
sudo systemctl enable --now mi-timer.timer

# O por separado:
sudo systemctl enable mi-timer.timer
sudo systemctl start mi-timer.timer

Error 9: Asumir que cron respeta la zona horaria del usuario

# ❌ Cron usa la zona horaria del sistema (/etc/timezone), no la del usuario
# El usuario configura TZ=CUBA, pero cron ejecuta en UTC

# ✅ Fijar TZ explícitamente en el script o crontab
TZ=America/Mexico_City
0 9 * * * /home/user/script.sh

# ✅ O al inicio del script:
#!/bin/bash
export TZ="America/Mexico_City"

Error 10: No eliminar timers transitorios de systemd-run

# ❌ Los timers creados con systemd-run son transitorios pero
#    pueden acumularse si no se limpian
sudo systemd-run --on-calendar='*:0/5' --unit=test-timer /bin/true

# ✅ Eliminar el timer transitorio cuando ya no se necesita
sudo systemctl stop test-timer.timer 2>/dev/null || true
sudo systemctl reset-failed test-timer.timer 2>/dev/null || true

# ✅ Ver qué timers transitorios hay
systemctl list-timers --all | grep run-

Conclusiones

ConceptoSintaxis / ComandoPropósito
Cron expresión* * * * * (min hora día mes díaSem)Programar tareas con 5 campos de tiempo
crontab -ecrontab -eEditar tareas del usuario actual
crontab -lcrontab -lListar tareas del usuario
@reboot@reboot /ruta/scriptEjecutar script al arranque
@daily@daily /ruta/scriptEjecutar cada día a medianoche
@hourly@hourly /ruta/scriptEjecutar cada hora
MAILTOMAILTO=user@dom.comEnviar salida de tareas por correo
Timer .service[Service] Type=oneshot ExecStart=Define qué ejecutar
Timer .timer[Timer] OnCalendar=dailyDefine cuándo ejecutar
systemctl enable –nowenable --now timer.timerActivar timer (ahora y al arranque)
systemctl list-timerslist-timers --allListar todos los timers
journalctl -ujournalctl -u servicio.serviceVer log de un servicio
OnCalendar=dailydaily, hourly, weekly, monthlyAtajos predefinidos de calendario
OnCalendar completoMon *-*-* 09:00:00Sintaxis completa de calendario
Persistent=truePersistent=trueEjecutar tareas perdidas por apagado
WakeSystem=trueWakeSystem=trueDespertar sistema dormido
RandomizedDelaySecRandomizedDelaySec=1hAñadir retardo aleatorio
AccuracySecAccuracySec=1mPrecisión de la ejecución
systemd-analyze calendarsystemd-analyze calendar "daily"Validar y calcular próximas ejecuciones
systemd-runsystemd-run --on-calendar=...Timer transitorio (sin archivos)
daemon-reloadsystemctl daemon-reloadRecargar unidades tras editar

Más información,

  • «The Linux Programming Interface» de Michael Kerrisk — Capítulo de señalización y tiempos de espera, base conceptual para timers
  • «UNIX and Linux System Administration Handbook» de Nemeth et al. — Capítulo sobre automatización y cron
  • «Bash Cookbook» de Carl Albing — Recetas de automatización con cron
  • «Systemd for Administrators» de Lennart Poettering — Serie de artículos que cubren timers en profundidad
  • «Linux System Programming» de Robert Love — Conceptos subyacentes de tiempo y eventos en Linux

Deja una respuesta