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 😛