Prima di leggere: due domande sulle lezioni precedenti
Nella lezione sulle variabili d'ambiente hai visto che una riga come
PATH=$HOME/bin:$PATH non funziona in un file di crontab, e perché. Prima
domanda: che valore ha allora PATH dentro un lavoro periodico? Seconda domanda: dove
hai letto la risposta, e con quale comando la verifichi sulla tua macchina?
La seconda domanda è quella che conta, e la risposta onesta per la prima è che non lo sai ancora. Lo scoprirai qui, e scoprirai anche che la risposta che circola in rete non corrisponde a quello che il sistema dichiara.
Il problema: un lavoro che doveva girare e non è girato
Un lavoro periodico che non parte è il guasto più silenzioso che esista. Non c'è un utente che segnala, non c'è una pagina che dà errore, non c'è un processo che muore in modo visibile. C'è un backup che non esiste, scoperto il giorno in cui serve, oppure un dato che non si aggiorna da tre settimane e nessuno se n'era accorto.
E c'è il guasto gemello, più raro e più costoso: un lavoro che è girato due volte, o che è girato in un momento diverso da quello che credevi, perché l'espressione temporale non significava quello che pensavi.
Linux offre due meccanismi per questi lavori, e non sono equivalenti. Uno è vecchio di decenni ed è ovunque; l'altro è recente, più verboso, e risolve problemi che il primo non affronta. La competenza utile non è saperne usare uno, è saper scegliere e saper verificare prima di aspettare.
La sintassi, letta dalla fonte
Il formato di una riga di crontab è documentato nel file di sistema stesso, e questa è
la documentazione migliore perché sta sulla macchina su cui lavori. Questo è il contenuto di
/etc/crontab su questo sistema, tolte le righe non pertinenti:
# Example of job definition:
# .---------------- minute (0 - 59)
# | .------------- hour (0 - 23)
# | | .---------- day of month (1 - 31)
# | | | .------- month (1 - 12) OR jan,feb,mar,apr ...
# | | | | .---- day of week (0 - 6) (Sunday=0 or 7) OR sun,mon,tue,wed,thu,fri,sat
# | | | | |
# * * * * * user-name command to be executed
17 * * * * root cd / && run-parts --report /etc/cron.hourly
Cinque campi temporali, poi l'utente, poi il comando. Una nota che la riga del giorno della settimana
dichiara e che vale la pena notare: la domenica si scrive 0 oppure 7,
quindi l'intervallo 1-5 sono i giorni lavorativi e 6-7 non è «sabato e
domenica» in modo intuitivo ma funziona perché sette è domenica.
La trappola grossa della sintassi di cron è che quando il giorno del mese e il giorno
della settimana sono entrambi specificati, le due condizioni vengono messe in
disgiunzione e non in congiunzione: il lavoro parte se è il quindici
oppure se è lunedì, non se è un lunedì quindici. È documentato in
crontab(5), e contraddice la lettura naturale di una riga con cinque campi che
altrimenti si leggono tutti come condizioni da soddisfare insieme.
L'ambiente, letto dalla fonte e non dal ricordo
La risposta alla domanda d'apertura. La pagina crontab(5) presente su questo sistema,
del pacchetto cron versione 3.0pl1-200ubuntu1, dichiara quali variabili
vengono impostate automaticamente, e l'elenco è breve: «SHELL is set to /usr/bin/sh, and LOGNAME and
HOME are set from the /etc/passwd line of the crontab's owner».
PATH non è in quell'elenco. E il file /etc/crontab su questo sistema porta
la riga del PATH commentata, con una nota accanto:
SHELL=/bin/sh
# You can also override PATH, but by default, newer versions inherit it from the environment
#PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
Quindi la frase che si legge spesso, «cron ha un PATH minimo che contiene solo /usr/bin
e /bin», su questo sistema non è confermata da nessuna delle due fonti: il manuale non
promette un PATH, e il file di sistema dice che per default lo eredita dall'ambiente. E
quella riga è un commento, quindi vale la regola che su questo progetto si applica da sempre: il
dato batte il commento.
La conseguenza operativa è più forte della risposta. Non importa quale sia il valore: se il valore
dipende dalla versione del pacchetto, da come è stato avviato il servizio e dall'ambiente di quel
momento, allora un lavoro periodico non deve dipendere dal PATH. Si scrivono i
percorsi assoluti dei comandi, e il problema smette di esistere su tutte le macchine insieme. Se
vuoi comunque sapere il valore sulla tua, il modo è misurarlo: una riga che esegue
env e ne scrive l'uscita in un file, letta la volta dopo.
Il confronto: cron e i timer di systemd
Un timer di systemd è una coppia di file: un servizio che fa il lavoro e un timer che
dice quando. Più verboso di una riga di crontab, e con quattro differenze che decidono
quando conviene.
[Unit]
Description=Ogni 10 secondi, per la lezione
[Timer]
OnBootSec=5s
OnUnitActiveSec=10s
AccuracySec=1s
Unit=lezione-timer.service
[Install]
WantedBy=timers.target
$ systemctl --user start lezione-timer.timer
$ journalctl --user -u lezione-timer.service -o cat | grep giro
giro alle 2026-10-30T17:47:53+01:00
giro alle 2026-10-30T17:48:04+01:00
Le quattro differenze. La prima è il log: l'uscita del lavoro va nel giornale e si legge con lo
stesso comando degli altri servizi, mentre cron per default manda un messaggio di posta
all'utente, che su una macchina senza posta configurata significa che l'uscita si perde. La seconda è
il tempo relativo: OnUnitActiveSec conta dalla fine dell'esecuzione precedente, quindi
due esecuzioni non si sovrappongono mai, cosa che cron non garantisce.
La terza è il recupero delle esecuzioni mancate: con Persistent=true, un lavoro che non
è girato perché la macchina era spenta viene eseguito alla riaccensione, e cron non ha
questo meccanismo. La quarta è la verificabilità, e per me è quella decisiva: l'espressione temporale
si può validare prima di aspettare.
$ systemd-analyze calendar "Mon..Fri 09:30"
Original form: Mon..Fri 09:30
Normalized form: Mon..Fri *-*-* 09:30:00
Next elapse: Mon 2026-11-02 09:30:00 CET
(in UTC): Mon 2026-11-02 08:30:00 UTC
From now: 2 days left
$ systemd-analyze calendar "Lun 09:30"
Failed to parse calendar specification 'Lun 09:30': Argomento non valido
$ echo $?
1
Lo strumento normalizza l'espressione, dice quando partirà la prossima volta, e lo dice anche in
tempo universale, che è il dato che serve quando devi coordinare due macchine in fusi diversi. Su
un'espressione non valida fallisce con codice uno. Per cron non esiste nulla di
equivalente: l'unico modo di sapere se hai scritto bene è aspettare il momento e guardare se è
partito.
L'errore di classificazione plausibile è concludere che i timer sono «meglio» e cron è
superato. Non è così, e le ragioni per cui cron resta sono concrete: una riga contro due
file, è presente su qualunque sistema anche senza systemd, e un utente senza privilegi
può installare il proprio crontab con un comando. Il criterio che uso: lavori semplici
e personali in cron, lavori di servizio in un timer, e tutto ciò che deve recuperare
un'esecuzione mancata obbligatoriamente in un timer.
Esempio svolto: un'espressione che sembra mensile e non lo è
Il problema: devi eseguire un lavoro l'ultimo giorno di ogni mese, alle tre di notte. La prima espressione che viene in mente è il giorno trentuno.
L'informazione disponibile: i mesi non hanno tutti trentuno giorni. Il ragionamento: se il campo del giorno del mese è una condizione da soddisfare, allora nei mesi di trenta giorni la condizione non si verifica mai, e il lavoro semplicemente non parte. Quante volte partirebbe davvero in un anno? Invece di contare a mano, lo chiedo allo strumento.
$ systemd-analyze calendar "*-*-31 03:00:00" --iterations=4
Next elapse: Sat 2026-10-31 03:00:00 CET
Iteration #2: Thu 2026-12-31 03:00:00 CET
Iteration #3: Sun 2027-01-31 03:00:00 CET
Iteration #4: Wed 2027-03-31 03:00:00 CEST
Guarda i salti. Dopo il 31 ottobre, la prossima volta è il 31 dicembre: novembre è saltato. Poi gennaio, poi marzo: febbraio e aprile saltati. L'espressione non significa «l'ultimo giorno del mese», significa «il trentuno», e in un anno si verifica sette volte su dodici.
La decisione che prendo: per «l'ultimo giorno del mese» non uso un numero di giorno. Con un timer
di systemd esiste una forma dedicata per l'ultimo giorno; con cron, dove
quella forma non c'è, la soluzione è girare ogni notte e far decidere allo script se oggi è l'ultimo
giorno, con un controllo sulla data di domani. L'alternativa che ho scartato è scrivere quattro
righe di crontab, una per lunghezza di mese: sarebbe corretta, e sarebbe quattro volte
più facile da sbagliare al prossimo cambiamento.
Nell'ultima riga dell'output c'è un dettaglio che vale la pena aver visto una volta: la sigla del
fuso cambia da CET a CEST, perché quell'esecuzione cade dopo il passaggio
all'ora estiva. Le espressioni temporali si valutano nel fuso locale, quindi nella notte del cambio
un lavoro programmato in un certo orario può non trovare quell'ora o trovarla due volte. Per i lavori
in cui questo conta, l'uso del tempo universale è la difesa, e lo strumento qui sopra mostra entrambi
affinché tu possa decidere.
La verifica che chiude il ragionamento: nel blocco qui sopra i salti sono il dato, e li ho letti dallo strumento invece di contarli. L'errore che avrei potuto commettere, e che è l'errore più comune di questa materia, è verificare l'espressione ragionandoci: su quattro iterazioni il ragionamento regge, su un anno no.
Il caso negativo: un comando che non ha mai funzionato
Questa riga di crontab dovrebbe archiviare i log ogni notte. È stata scritta un anno
fa, nessuno ha mai segnalato niente, e l'archivio è vuoto. Trova i tre difetti.
0 3 * * * cd /var/log && tar -czf /archivio/log-$(date +%Y-%m).tar.gz *.log
Il primo difetto è il più sottile, e lo riconosci solo se hai letto la pagina di manuale. La pagina
crontab(5) dice che «the entire command portion of the line, up to a newline or %
character, will be executed», e aggiunge che i caratteri per cento vengono trasformati in ritorni a
capo e che tutto ciò che segue il primo viene passato al comando come ingresso. Quindi
date +%Y-%m tronca il comando e il resto diventa dati. Il rimedio è scrivere
\% per ciascuno, forma che la pagina di manuale usa nei propri esempi, oppure, molto
meglio, non mettere affatto la logica nella riga di crontab.
Il secondo è l'assenza di percorsi assoluti per tar: alla luce di quello che hai letto
sopra sull'ambiente, non c'è nessuna garanzia su quale PATH quel comando riceverà, e la
differenza fra «funziona sul mio portatile» e «funziona nel lavoro notturno» è esattamente questa.
Il terzo è che l'uscita non va in nessun posto: senza un reindirizzamento, l'uscita e gli errori
finiscono in un messaggio di posta che su una macchina senza posta configurata viene scartato, e
questo spiega perché nessuno ha mai segnalato niente in un anno.
L'errore è plausibile perché la riga è corretta come comando di shell: se la incolli
in un terminale funziona. Il file di crontab non è uno script di shell, ed è la stessa
lezione della riga del PATH che hai visto nel modulo precedente. Lo riconosci da un
sintomo e uno solo: un lavoro che non ha mai prodotto né un risultato né un errore.
La correzione che risolve tutti e tre i difetti insieme non è correggerli uno per uno: è spostare il
lavoro in uno script, con set -euo pipefail come hai imparato, percorsi assoluti, e un
reindirizzamento dichiarato. La riga di crontab diventa un solo comando senza logica,
senza caratteri speciali e senza niente da sbagliare.
Esercizio 1, guidato: valida cinque espressioni
Per ciascuna di queste cinque intenzioni, scrivi l'espressione temporale e poi validala con lo strumento, non con il ragionamento. Riporta la prima e la terza esecuzione prevista.
- Ogni giorno alle 2 e 30 di notte.
- Ogni lunedì mattina alle 8.
- Il primo giorno di ogni trimestre.
- Ogni quarto d'ora.
- L'ultimo giorno di febbraio, che negli anni bisestili cade il 29.
Esercizio 2, autonomo: misura l'ambiente del tuo lavoro periodico
Installa un lavoro periodico che scrive il proprio ambiente in un file, e la volta dopo leggi quel
file. Confronta il valore di PATH con quello del tuo terminale. Criterio di successo:
hai il valore misurato, non quello che hai letto da qualche parte, e sai dire se un comando che usi
nei tuoi script sarebbe stato trovato.
Esercizio 3, trasferimento
Prendi il lavoro dell'esercizio precedente e riscrivilo come coppia timer e servizio di
systemd, usando quello che sai dalla lezione sulle unità. Poi rispondi: quale dei
difetti del caso negativo di questa lezione non può più verificarsi in questa forma,
e perché?
Soluzioni commentate
Esercizio 1. Le prime quattro sono dirette: *-*-* 02:30:00,
Mon *-*-* 08:00:00, *-01,04,07,10-01 00:00:00, e
*-*-* *:00/15:00 per il quarto d'ora. Su quest'ultima, la forma con la barra significa
«ogni quindici a partire da zero», e vale la pena validarla proprio perché la sintassi non si indovina.
La quinta è l'unica interessante, e la risposta corretta è che non si scrive come
espressione. Un'espressione che dica «il 28 oppure il 29 di febbraio» partirebbe due volte
negli anni bisestili; una che dica «il 29» salterebbe tre anni su quattro, come hai visto nei salti
del trentuno. Con i timer di systemd esiste però una forma dedicata, che conta i giorni
dalla fine del mese, e l'ho verificata sulla mia macchina:
$ systemd-analyze calendar "*-*~01 03:00:00" --iterations=4
Normalized form: *-*~01 03:00:00
Next elapse: Sat 2026-10-31 03:00:00 CET
Iteration #2: Mon 2026-11-30 03:00:00 CET
Iteration #3: Thu 2026-12-31 03:00:00 CET
Iteration #4: Sun 2027-01-31 03:00:00 CET
Trentuno ottobre, trenta novembre, trentuno dicembre: nessun mese saltato. È il
confronto diretto con i salti dell'esempio svolto, e la differenza sta tutta nella tilde. Con
cron, dove quella forma non esiste, la soluzione resta girare ogni notte e far decidere
allo script. Il punto dell'esercizio è questo: alcune intenzioni temporali non si esprimono con un
numero di giorno, e insistere a esprimerle così produce un lavoro che gira il numero sbagliato di
volte.
Esercizio 2. Qualunque valore tu abbia misurato, la risposta utile è la stessa: non
scriverlo in nessun posto come se fosse una costante. Io non ho eseguito questa misura su questa
macchina, perché installare un lavoro nel crontab di un sistema che non è mio avrebbe
modificato quel sistema, e per una lezione non si modifica la macchina di qualcun altro. Quello che
ho fatto è leggere le due fonti, il manuale e il file di sistema, e constatare che nessuna delle due
promette un valore. Da cui la regola: percorsi assoluti.
Esercizio 3. Il difetto che non può più verificarsi è il primo, il carattere per
cento troncato, e la ragione è strutturale: nel file di una unità di systemd quel
carattere non ha quel significato, e soprattutto la riga ExecStart non passa per una
shell, come hai misurato nella lezione sulle unità. Quindi non ci sono né sostituzioni di comando né
caratteri speciali da proteggere: se provassi a scriverne, verrebbero passati come argomenti
letterali.
E questo spiega perché la risposta è «il primo» e non «tutti e tre». Il secondo difetto, il comando
scritto senza percorso assoluto, può verificarsi ancora, e qui ho dovuto correggere
quello che credevo. Avevo scritto che ExecStart richiede un percorso assoluto, e sulla
versione di systemd di questa macchina non è vero: ho provato un'unità con
ExecStart=echo ciao, il comando è stato eseguito, il giornale riporta
ciao, e systemctl show -p ExecStart riporta
path=echo senza lamentarsi. Quindi il nome nudo viene cercato, e la dipendenza
dall'ambiente resta: i percorsi assoluti vanno scritti perché sono giusti, non perché qualcuno ti
fermi.
Il terzo difetto, l'uscita che si perde, non può verificarsi, perché l'uscita standard di un servizio finisce nel giornale per costruzione e non per configurazione. Quindi la risposta completa è: uno reso impossibile dal formato, uno che resta a carico di chi scrive, e uno risolto dalla raccolta automatica dei log. Vale la pena distinguere i tre casi, perché solo il primo e il terzo sono garanzie, e trattare il secondo come una garanzia è il modo in cui si riaprono i guasti già chiusi.
Che cosa adesso sai fare
Sai leggere i cinque campi di una riga di crontab dalla documentazione che sta sulla tua
macchina, e sai che giorno del mese e giorno della settimana insieme si combinano in modo
controintuitivo. Sai che il manuale non promette un PATH ai lavori periodici, e sai che
la conseguenza è scrivere percorsi assoluti invece di cercare il valore giusto.
Sai validare un'espressione temporale prima di aspettare, e hai visto con una misura
che «il trentuno» non è «l'ultimo giorno del mese». Sai scegliere fra una riga di
crontab e un timer in base a quattro differenze concrete. La verifica concreta: prendi
un lavoro periodico che esiste sulla tua macchina e controlla se la sua uscita finisce in un posto
dove la ritroveresti.