Not all versions of Cron are created equal.
I had a need to run a specific command on the first Sunday of each month at 3am. FreeFormatter.com suggested the following recipe:
0 0 3 ? * 2#1 *
At 03:00:00am, on the 1st Monday of the month, every month.
In order, they are:
- Second (
0
): Run every 0th second. - Minute (
0
): Run every 0th minute. - Hour (
3
): Run only at 3am. - Day of week (
?
): Used to say Sunday, or Monday, or Tuesday, etc. Here,?
means "not specified", which is used to signal that there's another mechanism used to pick the day on which to run. - Month (
*
): Run every month. - Day of week selector (
2#1
): On the2
nd day of the week (Monday), for the first1
instances (first one of the month). - Year (
*
): Run every year.
The bad news
Unfortunately, the Cron I was working with didn't like either the number of columns in the expression, or the Day of week selector, "2#1
". So to simulate the desired effect, I used date
and a more vanilla Cron expression from crontab.guru:
0 3 1-7 * *
At 03:00am, on every day-of-month, from day 1 through day 7.
In order, they are:
- Minute (
0
): Run every 0th minute. - Hour (
3
): Run only at 3am. - Day of month (
1-7
): Run only on the first 7 days of the month. - Month (
*
): Run every month. - Day of week (
*
): This is a meaningless parameter in our example. Why? Because it isOR
'd with the expression. So even if I had specified0 3 1-7 * MON
, it would read:
At 03:00 on every day-of-month from day 1 through day 7 and on Monday.
Shell testing to the rescue
The hack then used is to AND
two commands together, one being my desired command, and the other being the date
tool to calculate which date of the week I want:
$ [ $(date +%A) = 'Monday' ] && echo foo # Only foo on Monday
foo
So the usage of the second, simplified Cron expression above, and the date
tool used to pick the day of the week, we're able to put it all together:
0 3 1-7 * * [ $(date +\%A) = 'Monday' ] && echo foo
(Note the escaping "\
" character required within the Cron file itself.)
This works because if the shell test ("[
" and "]
" characters) doesn't equal "Monday
", then it returns false (really it exists non-zero), and we won't execute our desired command, echo foo
.
I didn't need rescuing...
After reflecting on my hacky brilliance, I thought I'd try a different syntax with my Cron:
0 3 2#1 * *
At 03:00am, on the first Monday, of each month, of each day.
- Minute (
0
): Run every 0th minute. - Hour (
3
): Run only at 3am. - Day of month (
2#1
): Run only on the 1st Monday of the month. - Day of week (
*
): Again, another meaningless expression.
So turns out I didn't need to use date
, but it's still a good trick to know.
loljkjkjk
Upon review on my Cron logs, it appears that the #1
bit of the day-of-month column had no affect:
Oct 2 03:00:01 mumble CRON[1256]: (root) CMD ([ $(date +%A) = 'Monday' ] && echo foo)
foo
didn't happen because it essentially only targeted the second day of the month.
So, two options:
- Restrict the days-of-month to be
1-7
(and continue filtering for"Monday"
), or - Run every day and restrict using a second
date
call:
# Only foo on a Monday whose day of month is less than 8 -- meaning the first week
$ [ $(date +%A) = 'Monday' ] && [ $(date +%d) -lt 8 ] && echo foo
Either way, the original idea to use Cron hackery was useful to investigate. I'll probably end up using option 1 to reduce the complexity of the subsequent commands, leaving me with:
0 3 1-7 * * [ $(date +\%A) = 'Monday' ] && echo foo