Running cron on a specific day of the month
| Chip Burke
Cron is an great scheduling tool, but it does have its limitations. For instance, it is very easy to run something every day, month, day of the week etc. at a specific time. Practically all Linux distros also have helpers such as cron.daily directories that allow you to place scripts or symlinks in them to make cron super easy to be used/abused.
However, there is one thing that cron is not so great at … running jobs on the nth Monday, Tuesday … Saturday, or Sunday of the month. Part of the reason for writing this post is I have seen a lot of wrong, half right, or overly complicated ways to do this.
So let’s choose an example. I have been tinkering with ZFS on Linux as of late. One piece of good practice in ZFS RAID is to “scrub” your arrays periodically to sniff out silent data errors. However, one doesn’t need to do this too terribly often. I’m using enterprise SAS drives and ECC RAM in my particular situation, so running a scrub once per month is plenty. Also, ZFS does not run a scrub operation automatically or via a daemon. You have to run it manually or via cron.
In my case I decided that running a scrub every first Sunday of the month at 2AM would do the trick and keep things off hours. This is where cron shows its limitations. I found a lot of suggestions to create a crontab similar to
0 2 1-7 * 7 /sbin/zpool scrub BlkStore
On the surface, this looks pretty brilliant. Run at 2:00AM on dates that are the 1st through the 7th AND are a Sunday. The first Sunday of the month will have to be in the range of the 1st through the 7th, right? Wait… except for the fact that cron parses this as an OR and not an AND! So this will run at 2AM on the 1st, 2nd, 3rd, 4th, 5th, 6th, 7th of the month AND on every Sunday. That’s not what I want.
The trick is one can’t solve this strictly through cron syntax. We need to recall our friend the operator &&
. For those of you not familiar with &&
, &&
tells a script not to move on unless the code preceding the &&
was successful, that is, exited with an exit 0
.
So we can construct a cron like this:
0 2 1-7 * * [ `date '+\%a'`= "Sun" ] && /sbin/zpool scrub BlkStore
Please note the specific use of backticks and single quotes/apostrophes.
So what is this saying? Cron will execute this every 1st, 2nd, 3rd, 4th, 5th, 6th, 7th of the month at 2AM. However, the command I am asking cron to execute will only proceed past the &&
IF the day of the week is Sunday. So, while cron is running this more than I would like, the script won’t execute the code after the &&
unless it is Sunday.
Likewise one can use 0 2 8-14 * *
, 0 2 15-21 * *
, or 0 2 22-28 * *
for the 2nd, 3rd, or 4th weeks of the month respectively.
Note, this is the same as running date +%a
from the command line with some escape characters to allow cron to parse the command properly. The date
command has a lot of helpful FORMAT options that you can use for other esoteric scheduling tasks in this same manner. Take a look at date --help
for a complete list.
Last X day of the month (some months have 5 weeks!) is a bit more work. In this case one can use a separate script. Cron is limited to a single sub-shell making multiple backtick statements a mess. Therefore my script will look like:
#!/bin/bash
[ `date +%d` -eq `echo $(cal | awk '{print $1}' | awk '{print $NF}' | grep -v '^$' | tail -n 1)` ] && /sbin/zpool scrub BlkStore
What this does is uses Linux’s built in cal
utility to determine if today is actually the last day of its type in the month. This is saying “If today’s date matches the date of the last day X of the month, then execute past &&
. The secret is in the awk '{print $1}'
. If we look at the output of cal
we see:
$1
in the awk statement tells awk which column to print. For example, $1
is Sunday, $3
is Tuesday and $6
is Friday. Therefore, print only the column for the day of the month I want and choose the last date of that day using tail
. Then, compare it to today’s date and if they match, move past the &&
.
The cron is dead easy as we’ll let the cron execute daily at our specified time, but let the script above determine if it is the correct day of the month to run
0 2 * * * /path/to/someotherscript
To play with this in Bash you have use a simplified form like this date +%d
and cal | awk '{print $1}' |awk '{print $NF}' |grep -v '^$'| tail -n 1
So let’s say I want to test this to see if today is the last Tuesday of the month. I am writing this on November 25, 2016 which is a Friday; obviously not the last Tuesday of this month. date +%d
tells me 25 and cal | awk '{print $3}' | awk '{print $NF}' | grep -v '^$' | tail -n 1
tells me 29. 25 != 29 so today must not be the last Tuesday of November 2016! Therefore, do not pass &&
thereby never run /sbin/zpool scrub BlkStore
.