I have been tracking the rides of the Berlin Critical Mass to create small visualisations of where the route was taking people for a while now. To do so, I wrote a little script that uses the Critical Maps API and takes bi-minutely snapshots.
To make sure that I had the data to visualise, I needed to remember to start the script on every last Friday of the month. Sometimes this was not possible since I was out of town with no access to my server where I kept the script.
I wanted to automate this and planned to set up a small cron job to start the script for me. After a little research I learned that the cron syntax does not allow for “every last Friday of the month”. Instead, what people would mostly do as a workaround is to set up cron to run their scripts every Friday and then to exit early in the script if it was not in fact the last Friday of the month.
This did not seem so nice to me, so I kept looking for better options.
I read somewhere that systemd’s timer feature allowed one to define jobs in intervals like the one I was looking for. I initially felt like the setup would be too complicated but then realised that the system is quite nice. I’m going to share what I learned in the rest of this aritcle (this is mostly from the documentation on suse.com).
Setting up a timer in systemd actually requires one to set up a service first. Mine looks like this:
[Unit] Description=Critical Tracks Recorder [Service] ExecStart=python3 /home/knut/critical-tracks/main.py
This file is called
critical-tracks.service and resides in
Along with the service, I also needed to set up a timer. The configuration is not much more complex and looks like this.
[Unit] Description=Critical Tracks monthly timer [Timer] OnCalendar=Fri *-*~07/1 18:00:00 [Install] WantedBy=multi-user.target
This file is called
critical-tracks.timer and resides in
The matching between service and timer is achieved by them sharing the same name so the only complicated part of the config
OnCalendar section. Details on the setup can be found by running
man 7 systemd.time which includes this section:
A date specification may use “~” to indicate the last day(s) in a month. For example, “*-02~03” means “the third last day in February,” and “Mon *-05~07/1” means “the last Monday in May.”
This looked quite like what I wanted already and just required some more tweaking to match my exact use case. Luckily, there
also is a handy executable called
sytemd-analyze which lets one test the calendar pattern.
systemd-analyze calendar --iterations 12 "Fri *-*~7/1 18:00:00"
Original form: Fri *-*~7/1 18:00:00 Normalized form: Fri *-*~07/1 18:00:00 Next elapse: Fri 2023-10-27 18:00:00 CEST (in UTC): Fri 2023-10-27 16:00:00 UTC From now: 1 week 3 days left Iter. #2: Fri 2023-11-24 18:00:00 CET (in UTC): Fri 2023-11-24 17:00:00 UTC From now: 1 month 8 days left Iter. #3: Fri 2023-12-29 18:00:00 CET (in UTC): Fri 2023-12-29 17:00:00 UTC From now: 2 months 13 days left Iter. #4: Fri 2024-01-26 18:00:00 CET (in UTC): Fri 2024-01-26 17:00:00 UTC From now: 3 months 10 days left Iter. #5: Fri 2024-02-23 18:00:00 CET (in UTC): Fri 2024-02-23 17:00:00 UTC From now: 4 months 8 days left Iter. #6: Fri 2024-03-29 18:00:00 CET (in UTC): Fri 2024-03-29 17:00:00 UTC From now: 5 months 12 days left Iter. #7: Fri 2024-04-26 18:00:00 CEST (in UTC): Fri 2024-04-26 16:00:00 UTC From now: 6 months 10 days left Iter. #8: Fri 2024-05-31 18:00:00 CEST (in UTC): Fri 2024-05-31 16:00:00 UTC From now: 7 months 14 days left Iter. #9: Fri 2024-06-28 18:00:00 CEST (in UTC): Fri 2024-06-28 16:00:00 UTC From now: 8 months 12 days left Iter. #10: Fri 2024-07-26 18:00:00 CEST (in UTC): Fri 2024-07-26 16:00:00 UTC From now: 9 months 10 days left Iter. #11: Fri 2024-08-30 18:00:00 CEST (in UTC): Fri 2024-08-30 16:00:00 UTC From now: 10 months 14 days left Iter. #12: Fri 2024-09-27 18:00:00 CEST (in UTC): Fri 2024-09-27 16:00:00 UTC From now: 11 months 12 days left
which I confirmed where the dates that I wanted.
I wanted to fully understand the syntax though and it took me a bit to understand the last part. The documentation says:
In the date and time specifications, any component may be specified as “*” in which case any value will match. Alternatively, each component can be specified as a list of values separated by commas. Values may be suffixed with “/” and a repetition value, which indicates that the value itself and the value plus all multiples of the repetition value are matched.
I tried around a bit and finally understood that the
~ made it so that we were counting from the end of the month. That is, instead of adding, the
would subtract from the n-th last day in multiples of the value after the slash.
To start easy,
systemd-analyze calendar --iterations 3 "Fri *-*~1 18:00:00" for example would only give us Fridays that were exactly the last day of the month:
Original form: Fri *-*~1 18:00:00 Normalized form: Fri *-*~01 18:00:00 Next elapse: Fri 2024-05-31 18:00:00 CEST (in UTC): Fri 2024-05-31 16:00:00 UTC From now: 7 months 14 days left Iter. #2: Fri 2025-01-31 18:00:00 CET (in UTC): Fri 2025-01-31 17:00:00 UTC From now: 1 year 3 months left Iter. #3: Fri 2025-02-28 18:00:00 CET (in UTC): Fri 2025-02-28 17:00:00 UTC From now: 1 year 4 months left
"Fri *-*~2 18:00:00" would give us Fridays on the second last day of the month.
/1 suffix made it so that we could also change the last value by increments of that value.
"Fri *-*~2/1 18:00:00"
would now give us Fridays on the last or the second-last day of the month. One could also choose non-1 increments for really complicated setups
"Fri *-*~03/2 18:00:00" which would translate to “Fridays that are on the third (3) last or the last (3 - 2*1) day of the month”.
This would be much more than I needed though. I could get just what I wanted from
Fri *-*~7/1 18:00:00 that is the last Friday on the seventh or fewer
last of day of the month.
I would now only have to enable the timer by running:
systemctl enable critical-tracks.timer systemctl start critical-tracks.timer
I could check that the timer was registered for the correct day by listing all timers:
NEXT LEFT LAST PASSED UNIT ACTIVATES Tue 2023-10-17 00:00:00 CEST 4h 42min left Mon 2023-10-16 00:00:01 CEST 19h ago dpkg-db-backup.timer dpkg-db-backup.service <snip> Fri 2023-10-27 18:00:00 CEST 1 week 3 days left n/a n/a critical-tracks.timer critical-tracks.service
And that was it I think. I’ll see at the end of the month but my learning of the day was that setting up timers in systemd might be much easier than I had initially feared.