Quiztime: Ein paar einfache Fragen zu cron. Doch vorne weg: cron ist bekannt. Ja klar, oder? Na gut, dann noch mal kurz das Format der crontab aufgefrischt. Hier die Spalten der Reihe nach (Keine Optionen, wirklich nur die Einträge für geplante Aufgaben):
1. Minute 2. Stunde 3. Tag im Monat 4. Monat 5. Tag im Monat 6. Benutzer außer es ist die crontab eines Benutzers 7. Kommando mit Argumenten
An 6. gedacht? Unter /etc/cron… steht in Spalte 6 der Benutzer unter dem der Job auszuführen ist. Bei einer crontab eines Benutzers fehlt diese Spalte. Wozu auch, ist ja diesem Benutzer zugeordnet. Alles klar? Dann mal los.
Rahmenbedingungen:
Sofern nicht anders angegeben wird Mittags um Punkt 12:00 Uhr der Befehl mytask
ausgeführt. Es handelt sich um die crontab eines Benutzers – es muss also kein Benutzer mit angegeben werden.
Frage 1: Ein Job zu jeder vollen, zweiten Stunde
Aha. Alle zwei Stunden also. Na dann mal los:
0 */2 * * * mytask
Und, gewusst? Ja, bis jetzt war es noch leicht. Falls nicht ganz sicher hier schon mal der Hinweis, dass man 5 crontab
nützliche Hinweise liefern kann 😛
Oder, als Link zu einer Online-Doku: http://manpages.ubuntu.com/manpages/bionic/man5/crontab.5.html.
Frage 2: Ein Job nur Sonntags
0 12 * * 0 mytask
Die Schwierigkeit hierbei ist lediglich sich zu merken, dass 0 oder 7 für Sonntag steht. Oder, und das ginge ebenfalls: Einfach sun
verwenden. Das merkt sich sogar leichter.
Frage 3: Wann wird dieser Job ausgeführt?
0 */3 8-12,14 * mon mytask
Ok, hier ist mal einiges ausgefüllt. Also mal von vorne beginnen: Zu jeder vollen 3. Stunde. Vom 8. bis 12. sowie am 14. eines jeden Monats. Und was ist mit dem Montag? Muss es heißen „8. bis 12. sowie 14. falls es Montag ist“ oder müsste es heißen „8. bis 12. sowie 14. und Montags“?
Richtig ist Variante 2: Das wird tatsächlich jeden Montag und im Zeitraum vom 8. bis 12. sowie am 14. eines jeden Monats zu jeder 3. vollen Stunde ausgeführt. Ein kurzer Blick in die Manpage zur Erinnerung:
The day of a command’s execution can be specified by two fields — day of month, and day of week. If both fields are restricted (i.e., don’t start with *), the command will be run when either field matches the current time.
Und, gewusst? Gut, dann geht es gleich weiter mit der nächsten Frage.
Frage 4: Ein Job am ersten im Monat sofern das ein Sonntag ist
Siehe Frage 3: Die einfache Variante mit setzen des Tages auf den 1. und den Wochentag auf Montag geht leider nicht. Wäre ja zu schön gewesen. Und nun? Naja, muss ein Hilfsskript her:
0 12 1 * * test $(date +\%u) -eq 0 && mytask
Ob man jetzt test
oder [
schreibt ist letztendlich egal. Funktionieren tut beides. Aber was macht das Skript? Die eigentlichen cron-Zeiten würden mytask
am ersten eines jeden Monats um 12:00 Uhr ausführen. date +%u
gibt eine Zahl für den Wochentag aus, wie gehabt 0 für Sonntag. Der Rest ist dann normales vorgehen an der Shell: Prüfe ob der aktuelle Wochentag gleich 0, also Sonntag ist. Falls ja führe via &&
-Verknüpfung mytask
aus.
Bleibt noch der \
beim +%u
: Den braucht man in der crontab da ein %-Zeichen in ein newline umgewandelt wird. Hierfür nochmals ein kurzer Blick in die manpage:
Percent-signs (%) in the command, unless escaped with backslash (\), will be changed into newline characters, and all data after the first % will be sent to the command as standard input.
Ebenfalls ist der Satz direkt danach interessant:
There is no way to split a single command line onto multiple lines, like the shell’s trailing „\“.
Auch wenn es nur bedingt übersichtlich ist: Ein Befehl in der crontab muss immer in einer Zeile stehen.
Das war noch zu leicht? Ok, weiter geht es mit Frage Nummer 5.
Frage 5: Ein Job am zweiten Sonntag im Monat
Ok, das mit dem Sonntag hatten wir bereits. Wird wohl wieder über test
laufen. Aber der zweite Sonntag im Monat? Naja, der erste Sonntag im Monat ist irgendwo zwischen dem 1. und 7., also ist der zweite irgendwo zwischen dem 8. und 14. im Monat. Und da haben wir es bereits wieder:
0 12 8-14 * * [ $(date +\%u) = 0 ] && mytask
Die Logik funktioniert ebenso wunderbar mit dem 3. und 4. Sonntag im Monat. Doch halt, ich seh da ein Problem…
Frage 6: Ein Task am letzten Sonntag im Monat
Na das musste ja kommen. Wie gemein. Vor allem, dass es Monate gibt die 5 Sonntage haben können. Es hat halt leider nicht jeder Monat nur 28 Tage… Vom Grundsatz her ist das klar, wird wohl wieder über test
laufen. Nur wie?
Neben test
braucht es hierfür noch ein weiteres, überall vorhandenes kleines Tool. Und zwar den Kalender ncal
. Damit die Wochentage zum filtern immer in der gleichen Sprache angegeben sind setzen wir in der Shell vorher noch die Sprache. Somit ergibt sich folgender beispielhafter Aufruf:
steffen@gmn ~ # LANG=C ncal November 2020 Su 1 8 15 22 29 Mo 2 9 16 23 30 Tu 3 10 17 24 We 4 11 18 25 Th 5 12 19 26 Fr 6 13 20 27 Sa 7 14 21 28 steffen@gmn ~ #
Jetzt klarer wie es funktioniert? Na ganz einfach: Mittels grep
die Zeile für Sonntag ermitteln und via sed
die letzte Spalte herausgeschnitten. Und wie gehabt diesen Wert mit date
vergleichen. Nur dieses mal nicht den Wochentag sondern den Tag im Monat:
0 12 * * sun [ "$(date +\%e)" = "$(LANG=C ncal | sed -n 's/^Su .* \([0-9]\+\) *$/\1/p')" ] && mytask
Und eigentlich ist es egal ob ich die Spalte mit dem Wochentag in cron ausfülle oder auch nicht. Geprüft wird ohnehin auf den Tag im Monat. Nur minimiert der Tagesfilter das unnötige ausführen des test
Skriptes an den anderen Wochentagen.
Frage 7: Ein Job am letzten Tag im Monat
War klar, dass das kommt. Oder? Pragmatischer Ansatz und einfachste Lösung: Lasse das Skript am 1. im Monat laufen und übergebe als Stichtag gestern. Das sollte in den meisten Fällen reichen.
Doch anders? Naja, eine einfache Lösung wäre diese hier:
0 12 30 4,6,9,11 * mytask 0 12 31 1,3,5,7,8,10,12 * mytask 0 12 28 2 * mytask
Für die Tage wo man weiß wie viele Tage der Monat hat ist das leicht. Nur in Schaltjahren wäre der Task einen Tag zu früh. Wenn das nichts macht wäre das eine mögliche Lösung. Dann wäre die Variante mit der Ausführung am ersten aber sicherlich ebenfalls machbar. Doof so? Dann doch wieder anders. Und ja klar, läuft wieder über test
und date.
Nur auf was testet man? Naja, eins hat jeder letzte Tag im Monat gemeinsam: Morgen ist der Erste:
0 12 28-31 * * [ $(date --date=tomorrow +\%d) = 01 ] && mytask
Zum Glück kann man date
sagen was für ein Datum man meint. Und da gehen Begriffe wie yesterday
oder eben das hier verwendete tomorrow
. Ebenfalls ist 28-31 bei dem Filter für den Tag im Monat optional. Es würde auch ohne gehen. So wird der eigentliche Task hingegen wirklich nur an den Tagen ausgeführt die ein Monatsende sein könnten.
Natürlich wäre auch ein „Mischbetrieb“ denkbar. Für alle Monate nach dem ersten Beispiel und nur für den 28. oder 29. Februar die zuletzt gezeigte Variante. Das überlasse ich jetzt jedem selbst zu entscheiden was für einen selbst eleganter sein mag: Mehrere Einträge in der crontab gegenüber einem der etwas öfters zumindest bis zum test
anläuft.
Unterm Strich ist die Einzeilenlösung jetzt doch gut kurz geworden. War ja gar nicht so gemein wie gedacht. Aber einen habe ich noch. Weiter geht es also mit…
Frage 8: Ein Job als Dienst beim hochfahren des Rechners starten
Öhm – wie jetzt? Beim hochfahren des Rechners? Ja, das geht. Nochmals der Blick in die manpage falls gerade gedanklich nicht zur Hand:
Instead of the first five fields, one of eight special strings may appear:
string meaning
------ -------
@reboot Run once, at startup.
@yearly Run once a year, "0 0 1 1 *".
@annually (same as @yearly)
@monthly Run once a month, "0 0 1 * *".
@weekly Run once a week, "0 0 * * 0".
@daily Run once a day, "0 0 * * *".
@midnight (same as @daily)
@hourly Run once an hour, "0 * * * *".
Please note that startup, as far as @reboot is concerned, is the time when the cron(8) daemon startup. In particular, it may be before some system daemons, or other facilities, were startup. This is due to the boot order sequence of the machine.
Ok, das ist bedingt der Systemstart. Richtiger wäre der Startzeitpunkt von cron
. Der ist aber allerdings meist gleichzusetzen mit dem Systemstart. Eventuell muss man bedenken, dass zu der Zeit noch nicht alles im System gestartet hat. Soll uns aber für dieses Beispiel egal sein. Tun wir so als wäre hier alles ok.
Also jetzt… ach Moment… als Dienst? Aber cron startet einen Task und der beendet sich im Anschluss daran. Da ist nichts mit „und lasse weiter laufen“. Wie startet man in der Shell einen Dienst ohne das dieser beim beenden der Shell mit beendet wird?
Klar es gibt Dienste die startet man via Befehl, dieser Befehl beendet sich und der Dienst läuft im Hintergrund weiter. Was ist aber wenn mytask
einfach nur ein anderes Skript mit einer Endlosschleife ist? Hier ist etwas Kenntnis der Shell hilfreich: Via &
kann man einen Prozess in den Hintergrund schieben. Und wenn man einen Prozess im Hintergrund nochmals in den Hintergrund schiebt, ja dann wir der Prozess von der Elternshell los gelöst. Aha. Also zweimal mit &
arbeiten. Und hier ist das Ergebnis:
@reboot (mytask &)&
Finale
Hand aufs Herz? Alles gewusst? Ja? Das ist ja super. Nur zu gut, dass mit systemd timer
künftig wieder alles anders wird: https://wiki.archlinux.org/index.php/Systemd/Timers. Ja, stetiger Wandel hält die kleinen grauen Zellen auf Trab 😛