El toolkit deja de esperar tus órdenes. Hasta ahora, informe-diario.sh solo existía si alguien se acordaba de lanzarlo y estado-servicio.sh se ejecutaba cuando ya sospechabas que algo iba mal —es decir, tarde—. cron es el demonio que lleva desde los años setenta resolviendo exactamente eso: un proceso que se despierta cada minuto, mira una tabla de horarios y ejecuta lo que toca. Es simple, está en todas partes y funciona. Pero tiene una particularidad que hace tropezar a todo el mundo la primera vez: el entorno en el que ejecuta tus scripts no se parece al de tu terminal. Esta lección cubre el modelo de cron, su sintaxis campo a campo y, sobre todo, cómo depurar el clásico «en mi terminal funciona».
Contenido
- El modelo de
crony dónde viven las tareas crontab -l,-e,-ry el peligro del teclado- La sintaxis de los cinco campos
- Listas, rangos, pasos y atajos
- El entorno mínimo de cron
- El
%,MAILTOy la captura de la salida - Depurar una tarea de cron
- Solapamiento, permisos y zona horaria
- Parientes de cron:
at,anacrony los timers - Aplicación: el toolkit en el crontab
- El modelo de
cron y dónde viven las tareas
cron y dónde viven las tareascron es un demonio: arranca con el sistema y se queda vivo. Su ciclo es sencillo: se despierta al principio de cada minuto, lee las tablas de tareas (crontabs), lanza los comandos cuya especificación horaria coincide con ese minuto y se vuelve a dormir. De ahí salen dos consecuencias. La primera: la granularidad mínima es el minuto; «cada 30 segundos» no existe en cron (para eso están los timers de 07-05). La segunda: cron no espera a que la ejecución anterior termine, así que las tareas se pueden solapar (apartado 8).
Compruébalo con lo de 06-03: systemctl is-active cron debe devolver active (crond en la familia Red Hat). Si el demonio no corre, tus tareas no se ejecutan y nadie te avisa. Ahora la primera confusión: no hay un crontab, hay varios sitios y no comparten formato.
| Ubicación | Quién la edita | ¿Campo de usuario? | Uso típico |
|---|---|---|---|
crontab -e (de usuario) |
Cada usuario, el suyo | No | Tareas de un servicio que corre con tu usuario |
/etc/crontab |
root, a mano |
Sí | Tareas del sistema (hoy poco usado) |
/etc/cron.d/fichero |
root, un fichero por paquete |
Sí | Lo que instalan los paquetes y lo que despliegas tú |
/etc/cron.{hourly,daily,weekly,monthly}/ |
root |
No aplica | Scripts ejecutables sueltos, sin horario propio |
La diferencia clave está en la columna del medio: los crontabs del sistema llevan un campo extra con el usuario, justo después de los cinco de tiempo. Los de usuario no, porque ya se sabe quién es.
30 6 * * * /home/veloz/veloz-ops/bin/informe-diario.sh # crontab de usuario
30 6 * * * veloz /home/veloz/veloz-ops/bin/informe-diario.sh # /etc/cron.d/velozPoner una línea de usuario en /etc/cron.d/ es un error común: cron interpretará /home/veloz/... como el nombre del usuario y fallará. Los directorios cron.daily y compañía son otro caso: ahí no hay horarios, hay scripts ejecutables (sin extensión .sh y con permiso de ejecución) que el sistema lanza una vez al día, a la hora que decida /etc/crontab o anacron.
crontab -l, -e, -r y el peligro del teclado
crontab -l, -e, -r y el peligro del tecladoTu crontab personal se gestiona siempre con el comando crontab, nunca editando ficheros a mano.
| Comando | Qué hace |
|---|---|
crontab -l |
Lista tu crontab por la salida estándar |
crontab -e |
Lo abre en $EDITOR y lo valida al guardar |
crontab -r |
Lo borra entero, sin preguntar |
crontab fichero |
Sustituye tu crontab por el contenido de ese fichero |
crontab -u veloz -l |
El de otro usuario (requiere root) |
-l y -r son teclas contiguas en QWERTY, y no hay papelera. Dos hábitos evitan el desastre:
crontab -l > ~/veloz-ops/etc/crontab.bak # copia antes de tocar nada
crontab ~/veloz-ops/etc/crontab.veloz # instalar desde fichero versionadoLa segunda línea es la buena práctica de verdad: mantén el crontab como un fichero de texto en tu repositorio (Git en 08-04) e instálalo desde ahí. Se guarda realmente en /var/spool/cron/crontabs/<usuario> (Debian/Ubuntu), pero no lo edites ahí: crontab -e además valida la sintaxis y avisa al demonio.
- La sintaxis de los cinco campos
Cada línea empieza con cinco campos separados por espacios, de la unidad más pequeña a la más grande:
minuto(0-59) hora(0-23) diaMes(1-31) mes(1-12 o jan-dec) diaSemana(0-7, 0 y 7 = domingo)
30 6 * * * /ruta/al/comandoEl asterisco significa «cualquier valor», así que 30 6 * * * se lee: minuto 30, hora 6, cualquier día, cualquier mes, cualquier día de la semana → todos los días a las 06:30. Un detalle que sorprende: cuando día del mes y día de semana están los dos restringidos (ninguno es *), cron los combina con un O lógico. 0 0 13 * 5 no es «los viernes 13», es «todos los días 13 y además todos los viernes»; para «viernes 13» hay que poner la condición dentro del comando.
- Listas, rangos, pasos y atajos
Cada campo admite cuatro construcciones, mezclables:
| Construcción | Sintaxis | En el campo del minuto |
|---|---|---|
| Valor | 15 |
Solo el minuto 15 |
| Lista | 1,15,45 |
Los minutos 1, 15 y 45 |
| Rango | 10-20 |
Del 10 al 20, todos |
| Paso | */10 |
0, 10, 20, 30, 40, 50 |
| Paso sobre rango | 0-30/5 |
0, 5, 10, 15, 20, 25, 30 |
| Combinación | 0,30,45-50 |
0, 30, 45, 46, 47, 48, 49, 50 |
*/N no significa «cada N minutos desde ahora», sino «los valores divisibles entre N». Por eso */7 da 0, 7, … 56 y luego salta a 0: entre dos ejecuciones a caballo de la hora pasan solo 4 minutos. Con divisores de 60 (2, 3, 5, 10, 15, 20, 30) el reparto es exacto.
| Especificación | Cuándo se ejecuta |
|---|---|
*/10 * * * * |
Cada 10 minutos |
0 * * * * |
En punto, cada hora |
30 6 * * * |
Todos los días a las 06:30 |
0 3 * * 0 |
Los domingos a las 03:00 |
0 9 * * 1-5 |
De lunes a viernes a las 09:00 |
*/15 8-20 * * 1-5 |
Cada 15 min, de 8 a 20 h, laborables |
0 2 1 * * |
El día 1 de cada mes a las 02:00 |
0 0 1 1,4,7,10 * |
El primer día de cada trimestre |
Existen además atajos: @yearly (0 0 1 1 *), @monthly (0 0 1 * *), @weekly (0 0 * * 0), @daily (0 0 * * *), @hourly (0 * * * *) y @reboot, que se ejecuta al arrancar cron. Son legibles, pero todos disparan a la misma hora: si diez servidores hacen @daily, los diez arrancan a las 00:00 clavadas y compiten por red y disco. En producción es mejor 17 3 * * * con una hora escogida. Y @reboot se dispara cuando arranca el demonio, no cuando el sistema está listo: si tu script necesita la red, puede llegar demasiado pronto (lo resuelven las dependencias de systemd, 07-05).
- El entorno mínimo de cron
Este es el apartado de la lección: cuando un script funciona en tu terminal y falla en cron, la respuesta casi siempre está aquí.
Cron no abre un shell de inicio de sesión ni interactivo. Repasa 01-04: no se lee ~/.bashrc, ni ~/.bash_profile, ni /etc/profile.
| Variable | En tu terminal | En cron |
|---|---|---|
PATH |
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:… |
/usr/bin:/bin |
SHELL |
/bin/bash |
/bin/sh |
HOME |
/home/veloz |
/home/veloz (sí está) |
LANG, LC_* |
Tu configuración regional | Sin definir |
| Todo lo demás | Lo que ponga tu .bashrc |
Nada |
Tres consecuencias prácticas: un jq instalado en /usr/local/bin no se encuentra; un alias o función de tu .bashrc no existe; y SHELL=/bin/sh significa que la línea del crontab se interpreta con sh, sin [[ ]], arrays ni <( ) (dentro de tu script sí los tienes, porque manda su shebang). El experimento más útil que puedes hacer es programar * * * * * env > /tmp/entorno-cron.txt y comparar con tu env.
Hay tres soluciones, las tres legítimas:
# 1. Rutas absolutas en todo (la mas robusta)
30 6 * * * /home/veloz/veloz-ops/bin/informe-diario.sh
# 2. Definir el entorno en el propio crontab, arriba del todo
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
30 6 * * * informe-diario.sh
# 3. Cargar la configuracion explicitamente antes de ejecutar
30 6 * * * . /home/veloz/veloz-ops/etc/veloz-ops.conf && /home/veloz/veloz-ops/bin/informe-diario.shLas asignaciones del crontab (opción 2) son variables de entorno normales, se aplican a todas las tareas de esa tabla y no admiten expansión: PATH=$PATH:/opt/bin no hace lo que esperas, hay que escribir el valor completo.
- El
%, MAILTO y la captura de la salida
%, MAILTO y la captura de la salidaDentro de una línea de crontab, el primer % marca el final del comando y todo lo que viene detrás se envía como entrada estándar; los siguientes se convierten en saltos de línea. Es una trampa perfecta porque % aparece en todas las cadenas de formato de date:
0 2 * * * tar czf /respaldos/datos-$(date +%Y-%m-%d).tar.gz /srv/veloz/datos # MAL: se corta
0 2 * * * tar czf /respaldos/datos-$(date +\%Y-\%m-\%d).tar.gz /srv/veloz/datos # BIEN: escapadoLa forma de no acordarse nunca de esta regla es no poner lógica en el crontab: una línea debería ser una ruta absoluta, sus opciones y una redirección; nada más.
Sobre la salida: si la tarea escribe algo por stdout o stderr, cron intenta enviártelo por correo local. En un servidor sin agente de correo, ese mensaje se pierde y tus errores desaparecen. [email protected] fija el destino y MAILTO="" desactiva el correo. El patrón correcto en un servidor moderno es un log propio:
30 6 * * * /home/veloz/veloz-ops/bin/informe-diario.sh >> /home/veloz/veloz-ops/logs/informe.log 2>&1Repasa 02-04: >> añade sin truncar y 2>&1 después lleva también los errores al mismo fichero. Y la mala práctica que verás en todas partes: >/dev/null 2>&1 a ciegas silencia la tarea por completo, así que el día que falle no habrá correo, ni log, ni rastro. Solo es aceptable cuando el script ya escribe su propio registro (07-04), y aun así conviene dejar 2>> a un fichero de errores.
- Depurar una tarea de cron
Cuando algo programado no funciona, sigue este orden.
¿Se ha intentado ejecutar? Cron registra cada lanzamiento en el log del sistema:
Si esa línea no aparece, el problema es la especificación horaria o el crontab, no tu script; si aparece, está dentro.
Reproduce el entorno. env -i lo limpia por completo y te deja ejecutar como lo haría cron:
Si aquí falla y en tu terminal no, ya tienes el diagnóstico: dependes de algo del entorno, normalmente un binario en /usr/local/bin. Después, mira el log de la tarea (si no lo redirigiste, hazlo ahora) y, si hace falta, sube el detalle con set -x y el PS4 personalizado de 05-03, o llamando al script con bash -x.
- Solapamiento, permisos y zona horaria
Solapamiento. Cron no comprueba si la ejecución anterior sigue viva. Programa estado-servicio.sh cada 10 minutos, deja que la API se cuelgue 40, y tendrás cuatro procesos peleándose por el mismo log. La solución la conoces desde 05-02:
*/10 * * * * /usr/bin/flock -n /var/lock/veloz-estado.lock /home/veloz/veloz-ops/bin/estado-servicio.sh >> /ruta/estado.log 2>&1-n significa «si el candado está tomado, sal inmediatamente», así la ejecución solapada se descarta en lugar de acumularse. En 07-02 verás la versión completa, con el candado dentro del propio script.
Permisos. Si existe /etc/cron.allow, solo los usuarios listados pueden tener crontab; si no existe pero sí /etc/cron.deny, pueden todos menos los listados. Un «you are not allowed to use this program» se explica siempre por estos dos ficheros.
Zona horaria y cambio de hora. Cron usa la del sistema (timedatectl), así que la misma línea se ejecuta a horas distintas en servidores de zonas distintas: la práctica habitual es ponerlos todos en UTC. Y en primavera, cuando el reloj salta de las 02:00 a las 03:00, una tarea a las 02:30 no se ejecuta ese día; en otoño puede ejecutarse dos veces. Regla práctica: no programes nada entre las 02:00 y las 03:00 si te importa que ocurra exactamente una vez —y si la tarea es idempotente (07-02), esto deja de ser un problema—.
- Parientes de cron:
at, anacron y los timers
at, anacron y los timersat ejecuta un comando una sola vez, en un momento concreto: echo '/ruta/respaldo.sh' | at 23:00, con at -l para listar y atrm N para cancelar. Es ideal para «lanza esto cuando termine la ventana de mantenimiento» sin dejar nada permanente. anacron, por su parte, resuelve un problema de diseño de cron: si la máquina está apagada a la hora prevista, la ejecución se pierde. Anacron no trabaja con horas sino con periodos en días y, al arrancar, comprueba cuánto hace que se ejecutó cada tarea. Por eso es lo normal en portátiles, y por eso en muchos sistemas los directorios cron.daily los dispara realmente anacron.
| Herramienta | Repetitiva | Recupera lo perdido | Granularidad | Complejidad |
|---|---|---|---|---|
cron |
Sí | No | Minuto | Muy baja |
anacron |
Sí | Sí | Día | Baja |
at |
No (una vez) | No | Minuto | Muy baja |
| Timer de systemd | Sí | Sí (Persistent=true) |
Segundo | Media |
Los timers cubren todo lo anterior y añaden dependencias, registro centralizado y aislamiento, a cambio de más ficheros. Son el tema de 07-05.
- Aplicación: el toolkit en el crontab
Este es el crontab de srv-veloz-01, guardado como fichero versionado en ~/veloz-ops/etc/crontab.veloz:
# Crontab de operaciones de Veloz Envios — srv-veloz-01
# Instalar con: crontab ~/veloz-ops/etc/crontab.veloz
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
MAILTO=""
VELOZ_OPS=/home/veloz/veloz-ops
# Informe diario del CSV de envios — todos los dias a las 06:30
30 6 * * * $VELOZ_OPS/bin/informe-diario.sh >> $VELOZ_OPS/logs/informe.log 2>&1
# Estado de la veloz-api — cada 10 minutos, sin solaparse
*/10 * * * * /usr/bin/flock -n /var/lock/veloz-estado.lock $VELOZ_OPS/bin/estado-servicio.sh >> $VELOZ_OPS/logs/estado.log 2>&1Cada decisión responde a algo de esta lección. SHELL=/bin/bash para que las líneas se interpreten con Bash. PATH completo porque jq vive en /usr/local/bin. MAILTO="" porque el servidor no tiene correo y ya redirigimos a un log. VELOZ_OPS como variable propia, porque cron sí expande dentro de los comandos las variables definidas en el crontab. 30 6 y no @daily: hora escogida, sin competir con todo lo demás a medianoche. */10 porque 10 es divisor de 60 y el reparto es exacto. flock -n solo en el segundo, que es el que puede tardar más que su intervalo. >> … 2>&1 en ambos, nunca /dev/null. Y ninguna lógica ni % en las líneas: fechas y decisiones viven dentro de los scripts.
Se instala con crontab -l > ~/veloz-ops/etc/crontab.bak (red de seguridad) seguido de crontab ~/veloz-ops/etc/crontab.veloz, y al día siguiente se verifica con journalctl -u cron --since "06:00". Esos dos logs crecerán sin parar: la rotación con logrotate se resuelve en 07-04.
Errores Comunes y Consejos
- Rutas relativas. Cron ejecuta desde
$HOMEcon unPATHmínimo:./informe-diario.shno funciona. Ruta absoluta siempre. - Suponer que se lee
.bashrc. No se lee: ni alias, ni funciones, ni elPATHque añadiste ahí. - Olvidar
2>&1, o usar>/dev/null 2>&1por costumbre. Lo primero deja los errores fuera del log; lo segundo silencia el único aviso que ibas a recibir. - El
%sin escapar. Corta el comando en seco. Mejor: no pongasdateen el crontab. - Confundir crontab de usuario con
/etc/cron.d/. El segundo lleva campo de usuario; el primero no. - Falta el salto de línea final, o el script no es ejecutable. Algunas versiones ignoran la última línea sin
\n; y sinchmod +x(02-03) hay que invocarlo como/bin/bash /ruta/script.sh. crontab -ren lugar de-l. Copia antes de tocar nada y mantén el crontab en un fichero versionado.- Consejo: cuando estrenes una tarea, prográmala primero cada 2 minutos con salida a un log, comprueba que funciona y solo entonces ponle su horario definitivo. Esperar 24 horas para descubrir un error de ruta es tiempo perdido.
Ejercicios
Ejercicio 1. Traduce a sintaxis de cron: (a) cada 5 minutos entre las 07:00 y las 21:59 de lunes a sábado; (b) el primer día de cada mes a las 04:15; (c) cada media hora todos los días; (d) los lunes, miércoles y viernes a las 22:00.
Ejercicio 2. Esta línea «no funciona». Encuentra los cuatro problemas y reescríbela:
Ejercicio 3. Escribe una función veloz_cron_instala para lib/comun.sh que instale un fichero de crontab de forma segura: guarda copia del actual, valida que el nuevo existe y no está vacío, lo instala y verifica que quedó instalado, restaurando la copia si algo falla.
Soluciones
Solución 1.
| Caso | Especificación | Explicación |
|---|---|---|
| (a) | */5 7-21 * * 1-6 |
El rango 7-21 cubre hasta las 21:59 porque incluye toda la hora 21 |
| (b) | 15 4 1 * * |
Minuto 15, hora 4, día 1 |
| (c) | 0,30 * * * * |
También vale */30; la lista explícita se lee mejor |
| (d) | 0 22 * * 1,3,5 |
Lista en el campo de día de semana |
El fallo típico en (a) es escribir 7-22 pensando «hasta las 21», lo que añadiría las ejecuciones de 22:00 a 22:55.
Solución 2. Los problemas: (1) ~ no se expande de forma fiable en el crontab; (2) % sin escapar, que corta el comando en date +; (3) > /dev/null 2>&1 silencia el respaldo, así que si falla nadie se entera; (4) ruta relativa dependiente de un cd que, si falla, deja el && sin ejecutarse en silencio.
La fecha del destino se calcula dentro del script, que es donde debe estar: así el crontab no tiene lógica, no hay % que escapar y el script se prueba a mano igual que se ejecuta programado.
Solución 3.
# veloz_cron_instala — instala un crontab con copia de seguridad y verificacion.
veloz_cron_instala() {
local nuevo="${1:?falta el fichero de crontab}" copia
[[ -s $nuevo ]] || { veloz_log_error "crontab '$nuevo' no existe o esta vacio"; return 66; }
copia=$(mktemp "${TMPDIR:-/tmp}/crontab-copia.XXXXXX") || return 74
crontab -l > "$copia" 2>/dev/null || : # sin crontab previo no es un error
if ! crontab "$nuevo"; then
veloz_log_error "cron rechazo '$nuevo'; el anterior sigue intacto"
rm -f "$copia"; return 65
fi
if ! diff -q <(crontab -l) "$nuevo" >/dev/null; then
veloz_log_error "verificacion fallida; restaurando copia anterior"
crontab "$copia"; rm -f "$copia"; return 74
fi
veloz_log_info "crontab instalado desde '$nuevo'"
rm -f "$copia"
}Tres decisiones merecen comentario. [[ -s ]] comprueba en una sola prueba que el fichero existe y no está vacío (03-04): instalar un crontab vacío equivale a un crontab -r accidental. El || : tras crontab -l neutraliza el código distinto de cero que devuelve cuando el usuario aún no tiene crontab, que no es un fallo; sin él, set -e (05-03) abortaría la función. Y la verificación con diff -q <(crontab -l) usa la sustitución de proceso de 05-05 para comparar lo que quedó realmente instalado con lo que querías, en vez de fiarse del código de salida de crontab.
Conclusión
cron se despierta cada minuto, lee unas tablas y ejecuta lo que toca. Las tareas viven en el crontab de usuario (sin campo de usuario), en /etc/crontab y /etc/cron.d/ (con él) o como scripts sueltos en /etc/cron.{hourly,daily,weekly,monthly}. Los cinco campos son minuto, hora, día del mes, mes y día de semana, y admiten valores, listas 1,15, rangos 1-5, pasos */10 y combinaciones; los atajos @daily o @reboot son legibles pero concentran la carga en horas redondas. Gestiona la tabla con crontab -l, -e y crontab fichero, con mucho cuidado con -r, que borra sin preguntar. Lo que de verdad separa una tarea que funciona de una que no es el entorno: cron no lee ~/.bashrc, da un PATH de dos directorios y SHELL=/bin/sh, y de ahí salen casi todos los «en mi terminal funcionaba»; se arregla con rutas absolutas, definiendo PATH en el crontab o cargando la configuración explícitamente. Añade el % que hay que escapar, redirigir siempre con >> fichero 2>&1 en vez de tirar la salida a /dev/null, envolver con flock -n lo que pueda solaparse y depurar en orden: primero journalctl -u cron para saber si se intentó, después env -i para reproducir el entorno.
Ya tienes el informe a las 06:30 y la comprobación de estado cada 10 minutos ejecutándose solas. Pero hay una diferencia enorme entre programar un script y tener un script que se pueda ejecutar sin nadie delante. ¿Qué pasa si pregunta algo por teclado y no hay teclado? ¿Si se ejecuta dos veces por un reintento? ¿Si se queda colgado esperando a un servidor que no contesta? ¿Cómo sabes, a la mañana siguiente, si hizo su trabajo? En 07-02 convertimos eso en siete propiedades concretas —no interactiva, idempotente, exclusiva, observable, acotada, segura ante fallos y configurable— y las implementamos una a una sobre informe-diario.sh, incluido el --dry-run que te dejará probar una tarea automática sin miedo.
Curso de Programación en Bash
Módulo 1: Introducción a Bash
- ¿Qué es Bash?
- Configurando tu Entorno
- Navegación Básica en la Línea de Comandos
- Entendiendo el Shell
- Encontrar Ayuda: man, help y --help
Módulo 2: Comandos Básicos de Bash
- Operaciones con Archivos y Directorios
- Comandos de Procesamiento de Texto
- Permisos y Propiedad de Archivos
- Redirección y Tuberías
- Comodines y Expansión de Rutas
- Historial y Atajos de Teclado
Módulo 3: Fundamentos de Scripting
- Creando y Ejecutando un Script
- Variables y Constantes
- Operadores Básicos
- Sentencias Condicionales
- Argumentos y Entrada del Usuario
- Comillas, Expansión y Sustitución
Módulo 4: Scripting Intermedio
- Bucles en Bash
- Funciones en Bash
- Arrays y Arrays Asociativos
- Manipulación de Cadenas
- La Sentencia case y los Menús Interactivos
- Aritmética y Cálculos Numéricos
Módulo 5: Técnicas Avanzadas de Scripting
- Operaciones Avanzadas con Archivos
- Gestión de Procesos
- Manejo de Errores y Depuración
- Expresiones Regulares
- Entrada/Salida Avanzada: Descriptores y Here-Documents
- Scripts Modulares y Librerías Reutilizables
Módulo 6: Trabajando con Herramientas Externas
Módulo 7: Automatización y Programación
- Trabajos Cron
- Automatizando Tareas
- Scripts de Respaldo y Restauración
- Monitoreo y Registro
- Servicios y Temporizadores con systemd
- Automatización Remota con SSH
Módulo 8: Mejores Prácticas y Optimización
- Escribiendo Código Legible
- Optimizando Scripts en Bash
- Consideraciones de Seguridad
- Control de Versiones con Git
- Análisis Estático con ShellCheck y shfmt
- Pruebas Automatizadas con Bats
- Portabilidad: POSIX sh frente a Bashismos
