66

I'd like to run a job from cron at 8.30 on the first Monday of every month. The cron Wikipedia page says

While normally the job is executed when the time/date specification fields all match the current time and date, there is one exception: if both "day of month" and "day of week" are restricted (not "*"), then either the "day of month" field (3) or the "day of week" field (5) must match the current day.

(my emphasis)

Does that mean I can't do the first Monday of the month, I can only do the first (or whatever) day of the month? I can't think of a way round it.

slhck
  • 235,242
Max Williams
  • 3,129

19 Answers19

59

You can put the condition into the actual crontab command (generic way):

[ "$(date '+%u')" = "1" ] && echo "It's Monday"

If your locale is en_US, you can also compare strings (initial answer):

[ "$(date '+%a')" = "Mon" ] && echo "It's Monday"

But this is not as portable.


Now, if this condition is true on one of the first seven days in a month, you have its first Monday. Note that in the crontab, the percent-syntax needs to be escaped (unless you are using cron from Alpine Linux):

0   12  1-7 *   *   [ "$(date '+\%u')" = "1" ] && echo "It's Monday"

if your locale is EN/US, you can also compare strings (initial answer):

0   12  1-7 *   *   [ "$(date '+\%a')" = "Mon" ] && echo "It's Monday"

Replace the echo command with the actual command you want to run. I found a similar approach too.

slhck
  • 235,242
29

I have a computer with locale on Spanish, so, this approach isn't working for me because mon changes to lun

Other languages would fail as well, so, I did a slight variation on the accepted answer that takes out the language barrier:

 0 9 1-7 * *   [ "$(date '+\%u')" = "1" ] && echo "¡Es lunes!"
cumanacr
  • 399
19

There is a hacky way to do this with a classic (Vixie, Debian) cron:

30 8 */100,1-7 * MON

The day-of-month field starts with a star (*), and so cron considers it "unrestricted" and uses the AND logic between the day-of-month and the day-of-week fields.

*/100 means "every 100 days starting from date 1". Since there are no months with more than 100 days, */100,1-7 effectively means "on dates 1 to 7".

Here's my article with more details: Schedule Cronjob for the First Monday of Every Month, the Funky Way

cuu508
  • 432
10

I find it easier when there's no need to handle day numbers.

Run First Monday of month:

0 2 * * 1 [ `date '+\%m'` == `date '+\%m' -d "1 week ago"` ] || /path/to/command

i.e. if the month 1 week ago is not the same as the current month then we are on the 1st day 1 (= Monday) of the month.

Similarly, for the Third Friday

0 2 * * 6 [ `date '+\%m'` == `date '+\%m' -d "3 weeks ago"` ] || /path/to/command

i.e. if the month 3 weeks ago is different to current month then we are on the 3rd day 6 (= Friday) of the month

Joel
  • 3
joel
  • 109
6

Since I interpret my cron statements using PHP and JavaScript, I can't use bash. Finally I found that it is in fact possible with just cron:

0 30 8 * 1/1 MON#1
Giacomo1968
  • 58,727
kramer65
  • 1,442
  • 4
  • 26
  • 43
6

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

4

I have scheduled a job to run on the 4th Monday of every month at 4:00 PM as follows:

0 16 22-28 * Mon [ "$(date '+\%a')" == "Mon" ] && touch /home/me/FourthMonOfMonth.txt
Indrek
  • 24,874
2

The day of month field here is */100,1-7, meaning “every 100 days starting from date 1, and also on dates 1-7”. Since there are no months with 100+ days, this again is a trick to say “on dates 1 to 7” but with a leading star. Because of the star, cron will run the command on dates 1 to 7 that are also Monday.

0 22 */100,1-7 * 2

On the cron guru example below, you can see the result and the next run date.

Giacomo1968
  • 58,727
1

I recommend to use

"$(/bin/date '+%\w')" = "1"

instead of

"$(date '+\%a')" = "Mon"

to avoid locale problem.

0
crontab 30 8 */27 * 1

At 08:30 AM, [ones] every 27 days, and on Monday (see crontab expression generator)

Day of the month is unencumbered (is *), so the either logic/exception does not apply.

SRGDVD
  • 1
0

Fewer runs per month (day test in cron schedule, not command), and has a success exit status unless the command fails (because of ||), and (I think) is easier to reason about:

11  11  *   *   1   [ $(date +\%-d) -gt 7 ] || echo "It's 11:11am on the first Monday of the month."

i.e. day-of-month was not in the first 7, or the command ran.

A last of month could be:

4   16  *   *   5   [ $(date +\%-d -d +1week) -gt 7 ] || echo "It's 4:04pm on the last Friday of the month."
Walf
  • 491
0

On Solaris 10 I had to format the condition as follows:

[ `date +\%a` = "Sat" ] && echo "It's Saturday"
Will
  • 9
0

You can try running cronjob for first seven days of month and let it execute only on Monday.

30 8 * * 1 [`date +\%d` -le 07] && <job>

Above should work for you.

simer
  • 171
-1

This usage should be most universal and avoids the locale issue.

[ `/bin/date +\%u` -eq 1 ]

the first Monday of the month at 6am would look like this in /etc/crontab

00 6 1-7 * *    root    [ `/bin/date +\%u` -eq 1 ] && /run/yourjob.sh
-1

As far as I know it is NOT possible using only crontab, however one can use a wrapper function to pick the correct day from a "first seven days of month" contab entry; see this from entry.

The wrapper script would be

#! /usr/bin/ksh
day=$(date +%d)
if ((day <= 7)) ; then
   exec somecommand
fi
exit 1

and you would need to run it (assuming it is called wrapper.sh and globally available) using the crontab entry

0 0 * * 1 wrapper.sh
elemakil
  • 297
-2

I just ran into this and here is what I came up with:

0 11 * * 1#1 - Run at 11 am on the first Monday of every month
0 11 * * 1#2 - Run at 11 am on the second Monday of every month
0 11 * * 1#3 - Run at 11 am on the third Monday of every month

If you wanted to run on Tuesday instead:

0 11 * * 2#1 - Run at 11 am on the first Tuesday of every month
0 11 * * 2#2 - Run at 11 am on the second Tuesday of every month
0 11 * * 2#3 - Run at 11 am on the third Tuesday of every month
Dominique
  • 2,373
-3

I needed the same result, but wanted to use pure cron. I hope this improves on the accepted answer.

For me I needed it to run the First Monday of each month at noon.

So this should run: At Noon, in the first 7 days of the month, on a Monday: 0 12 1-7 * 1

https://crontab.guru/#0_12_1-7_*_1

-3

I believe this solves the problem more elegantly:

30 8 1-7 * 1 /run/your/job.sh
symcbean
  • 432
-4
0 9 1-7 * 1 * 

This will work for every monday of every month.

SIBHI S
  • 115