Cron syntax hacks

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 the 2nd day of the week (Monday), for the first 1 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 is OR'd with the expression. So even if I had specified 0 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:

  1. Restrict the days-of-month to be 1-7 (and continue filtering for "Monday"), or
  2. 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