This answer expands on @ChiragPansheriya's answer to this same question.
tl;dr
Do a quick test of your cron implementation by pasting this into your crontab:
# For 1st Monday (or Tuesday, etc) of month, use */32,1-7 for days of month
# For 2nd Monday (or Tuesday, etc) of month, use */32,8-14 for days of month
# For 3rd Monday (or Tuesday, etc) of month, use */32,15-21 for days of month
# For 4th Monday (or Tuesday, etc) of month, use */32,22-28 for days of month
# For 5th Monday (or Tuesday, etc) of month, use */32,29-31 for days of month
# Explanation: https://superuser.com/a/1813556
* * */32,1-7 * 0 echo "First Sunday" >>cron_out.txt
* * */32,8-14 * 0 echo "Second Sunday" >>cron_out.txt
* * */32,15-21 * 0 echo "Third Sunday" >>cron_out.txt
* * */32,22-28 * 0 echo "Fourth Sunday" >>cron_out.txt
* * */32,29-31 * 0 echo "Fifth Sunday" >>cron_out.txt
* * */32,1-7 * 1 echo "First Monday" >>cron_out.txt
* * */32,8-14 * 1 echo "Second Monday" >>cron_out.txt
* * */32,15-21 * 1 echo "Third Monday" >>cron_out.txt
* * */32,22-28 * 1 echo "Fourth Monday" >>cron_out.txt
* * */32,29-31 * 1 echo "Fifth Monday" >>cron_out.txt
* * */32,1-7 * 2 echo "First Tuesday" >>cron_out.txt
* * */32,8-14 * 2 echo "Second Tuesday" >>cron_out.txt
* * */32,15-21 * 2 echo "Third Tuesday" >>cron_out.txt
* * */32,22-28 * 2 echo "Fourth Tuesday" >>cron_out.txt
* * */32,29-31 * 2 echo "Fifth Tuesday" >>cron_out.txt
* * */32,1-7 * 3 echo "First Wednesday" >>cron_out.txt
* * */32,8-14 * 3 echo "Second Wednesday" >>cron_out.txt
* * */32,15-21 * 3 echo "Third Wednesday" >>cron_out.txt
* * */32,22-28 * 3 echo "Fourth Wednesday" >>cron_out.txt
* * */32,29-31 * 3 echo "Fifth Wednesday" >>cron_out.txt
* * */32,1-7 * 4 echo "First Thursday" >>cron_out.txt
* * */32,8-14 * 4 echo "Second Thursday" >>cron_out.txt
* * */32,15-21 * 4 echo "Third Thursday" >>cron_out.txt
* * */32,22-28 * 4 echo "Fourth Thursday" >>cron_out.txt
* * */32,29-31 * 4 echo "Fifth Thursday" >>cron_out.txt
* * */32,1-7 * 5 echo "First Friday" >>cron_out.txt
* * */32,8-14 * 5 echo "Second Friday" >>cron_out.txt
* * */32,15-21 * 5 echo "Third Friday" >>cron_out.txt
* * */32,22-28 * 5 echo "Fourth Friday" >>cron_out.txt
* * */32,29-31 * 5 echo "Fifth Friday" >>cron_out.txt
* * */32,1-7 * 6 echo "First Saturday" >>cron_out.txt
* * */32,8-14 * 6 echo "Second Saturday" >>cron_out.txt
* * */32,15-21 * 6 echo "Third Saturday" >>cron_out.txt
* * */32,22-28 * 6 echo "Fourth Saturday" >>cron_out.txt
* * */32,29-31 * 6 echo "Fifth Saturday" >>cron_out.txt
Explanation
One might wonder:
The */32
element in the days of month field is always false (no month has more than 31 days), so why include it?
It turns out that, if the first character of the days of month or days of week fields is an asterisk (*
), cron switches from taking the union of the days of month and days of week fields, to taking their intersection. The */32
element is there simply to trigger this change in behavior.
This is hinted at in the man page for cronie née vixie-cron:
Note: The day of a command's execution can be specified in the following two fields — 'day of month', and 'day of week'. If both fields are restricted (i.e., do not contain the "*" character), the command will be run when either field matches the current time. For example,
"30 4 1,15 * 5" would cause a command to be run at 4:30 am on the 1st and 15th of each month, plus every Friday.
— https://www.mankier.com/5/crontab
The converse is that, if day of month or day of week do contain the *
character, the command will be run only when both fields match the current time.
You can see how the presence of the leading *
character in the days of month field changes crontab.guru's interpretation:
crontab entry specification |
crontab guru's interpretation |
0 8 1-7 * 1 |
At 08:00 on every day-of-month from 1 through 7 and on Monday. |
0 8 */32,1-7 * 1 |
At 08:00 on every 32nd day-of-month and every day-of-month from 1 through 7 if it's on Monday. |
Examining the source code
The phrase used in the man page, "contain the *
character," is ambiguous. Does it mean, consist solely of the *
character? Or does it mean, contain the *
character at any point within the field? Consulting the source code for vixie cron (entry.c), we see that the DOM_STAR
and DOW_STAR
flags get set if the first character of their respective fields is an asterisk (*
).
/* DOM (days of month)
*/
if (ch == '*')
e->flags |= DOM_STAR; ①
ch = get_list(e->dom, FIRST_DOM, LAST_DOM,
PPC_NULL, ch, file);
if (ch == EOF) {
ecode = e_dom;
goto eof;
}
[...]
/* DOW (days of week)
*/
if (ch == '*')
e->flags |= DOW_STAR; ②
ch = get_list(e->dow, FIRST_DOW, LAST_DOW,
DowNames, ch, file);
if (ch == EOF) {
ecode = e_dow;
goto eof;
}
① The DOM_STAR
flag gets set if the first character of the days of month field is a *
character
② The DOW_STAR
flag gets set if the first character of the days of week field is a *
character
Later, in cron.c
, if either of those two flags are set, cron switches the logical operator between the days of month and days of week fields from an OR to an AND:
/* the dom/dow situation is odd. '* * 1,15 * Sun' will run on the
* first and fifteenth AND every Sunday; '* * * * Sun' will run *only*
* on Sundays; '* * 1,15 * *' will run *only* the 1st and 15th. this
* is why we keep 'e->dow_star' and 'e->dom_star'. yes, it's bizarre.
* like many bizarre things, it's the standard.
*/
for (u = db->head; u != NULL; u = u->next) {
for (e = u->crontab; e != NULL; e = e->next) {
Debug(DSCH|DEXT, ("user [%s:%d:%d:...] cmd=\"%s\"\n",
env_get("LOGNAME", e->envp),
e->uid, e->gid, e->cmd))
if (bit_test(e->minute, minute) &&
bit_test(e->hour, hour) &&
bit_test(e->month, month) &&
( ((e->flags & DOM_STAR) || (e->flags & DOW_STAR)) ①
? (bit_test(e->dow,dow) && bit_test(e->dom,dom)) ②
: (bit_test(e->dow,dow) || bit_test(e->dom,dom)))) { ③
if ((doNonWild && !(e->flags & (MIN_STAR|HR_STAR)))
|| (doWild && (e->flags & (MIN_STAR|HR_STAR))))
job_add(e, u);
}
}
}
① The DOM_STAR
and DOW_STAR
flags are tested
② OR logical operator causes a union of the days of month and days of week fields
③ AND logical operator causes an intersection of the days of month and days of week fields
We've seen here that the "contain the *
character" test mentioned in the man page, is actually a test of whether the *
character is the first character of the field.
Caution: This behavior is not mandated by POSIX, as far as I can tell. (See https://pubs.opengroup.org/onlinepubs/9699919799/utilities/crontab.html). YMMV depending on your cron implementation. I can confirm that the technique described in this answer works on cronie (a vixie-cron derivative). It is also understood by crontab.guru (example: 0 8 */32,1-7 * 1
).
Some background information on this behavior is included in the FEATURES file in the vixie cron source code:
-- the dom/dow situation is odd. '* * 1,15 * Sun' will run on the
first and fifteenth AND every Sunday; '* * * * Sun' will run *only*
on Sundays; '* * 1,15 * *' will run *only* the 1st and 15th. this
is why we keep 'e->dow_star' and 'e->dom_star'. I didn't think up
this behaviour; it's how cron has always worked but the documentation
hasn't been very clear. I have been told that some AT&T crons do not
act this way and do the more reasonable thing, which is (IMHO) to "or"
the various field-matches together. In that sense this cron may not
be completely similar to some AT&T crons.
Comparison
Here's a comparison of how the two approaches look in a crontab:
0 8 1-7 * * [ "$(date '+\%u')" = "1" ] && echo "First Monday"
0 8 */32,1-7 * 1 echo "First Monday"
(The first line shows the approach of the accepted answer).
Related discussions