How to instruct cron to execute a job every second week?
Asked Answered
L

15

89

I would like to run a job through cron that will be executed every second Tuesday at given time of day. For every Tuesday is easy:

0 6 * * Tue

But how to make it on "every second Tuesday" (or if you prefer - every second week)? I would not like to implement any logic in the script it self, but keep the definition only in cron.

Lemar answered 8/12, 2008 at 16:18 Comment(2)
see also unix.stackexchange.com/questions/197324/…Eyas
How about running it every week and checking in your script (e.g. by using a separate file) whether it is the second week or the first? if [ "$(cat week.txt)" == "1" ]; then echo -n "0">week.txt; dostuff; fiTormentor
F
53

How about this, it does keep it in the crontab even if it isn't exactly defined in the first five fields:

0 6 * * Tue expr `date +\%W` \% 2 > /dev/null || /scripts/fortnightly.sh
Fredel answered 8/12, 2008 at 16:25 Comment(1)
My guess is that this solution has a problem on the last week of the year. Sometimes the last Friday of the year is on week 51, and some other times on week 52. So that could make you run the script two weeks in a row or once in three weeksDenunciation
W
56

Answer

Modify your Tuesday cron logic to execute every other week since the epoch.

Knowing that there are 604800 seconds in a week (ignoring DST changes and leap seconds, thank you), and using GNU date:

0 6 * * Tue expr `date +\%s` / 604800 \% 2 >/dev/null || /scripts/fortnightly.sh

Aside

Calendar arithmetic is frustrating.

@xahtep's answer is terrific but, as @Doppelganger noted in comments, it will fail on certain year boundaries. None of the date utility's "week of year" specifiers can help here. Some Tuesday in early January will inevitably repeat the week parity of the final Tuesday in the preceding year: 2016-01-05 (%V), 2018-01-02 (%U), and 2019-01-01 (%W).

Wolverine answered 9/10, 2013 at 17:21 Comment(2)
This answer, or the alternate version of this using the seconds in a day (not week) e.g. 86400 also have a limitation in certain edge cases: the adjustment in UTC time vs. your local server. Suppose I have a cron job that will run every other Tuesday at 3 am and 11pm. You could end up in a scenario that according to UTC time (seconds since epoch) that these are being treated as different days. In my case 21600 is my local offset (sec) vs. the epoch: expr \( `date +%s` - 21600 \) / 86400 % 2 and, for testing expr \( `date +%s -d "Thursday"` - 21600 \) / 86400 % 2Scare
See my answer below for a more complete solution.Scare
F
53

How about this, it does keep it in the crontab even if it isn't exactly defined in the first five fields:

0 6 * * Tue expr `date +\%W` \% 2 > /dev/null || /scripts/fortnightly.sh
Fredel answered 8/12, 2008 at 16:25 Comment(1)
My guess is that this solution has a problem on the last week of the year. Sometimes the last Friday of the year is on week 51, and some other times on week 52. So that could make you run the script two weeks in a row or once in three weeksDenunciation
H
12

Something like

0 0 1-7,15-21 * 2

Would hit the first and third Tuesday of the month.

Note: Don't use this with vixie cron (included in RedHat and SLES distros), as it makes an or between the day-of-month and day-of-week fields instead of an and.

Hopeh answered 30/6, 2021 at 9:11 Comment(1)
What are examples of a few distros that work, and some which don't? Because Ubuntu and Mac OSX both say "vixie cron" when you read the manpage with "man cron". You mention "don't use with .. RedHat". If the criterion is the word vixie, it's ruling out everything. Maybe that isn't exactly the thing to check.Caudill
E
11

Maybe a little dumb, but one could also create two cronjobs, one for every first tuesday and one for every third.

First cronjob:

0 0 8 ? 1/1 TUE#1 *

Second cronjob:

0 0 8 ? 1/1 TUE#3 *

Not sure about the syntax here, I used http://www.cronmaker.com/ to make these.

Econometrics answered 9/10, 2013 at 15:50 Comment(4)
Scheduling the job for the first and third Tuesdays of each month will miss a month's fifth Tuesday (e.g., 31-Mar-2015), and of course adding that fifth Tuesday will run the job too frequently. Separately, this answer references the cron-like Quartz scheduler rather than traditional *NIX cron.Wolverine
What does the '#' mean?Unpolite
@Asif "It allows you to specify constructs such as "the second Friday" of a given month." en.wikipedia.org/wiki/CronGoingover
@bigtex777: And which cron on which Linux distro implements that? It's certainly not POSIX.Renayrenckens
V
9

pilcrow's answer is great. However, it results in the fortnightly.sh script running every even week (since the epoch). If you need the script to run on odd weeks, you can tweak his answer a little:

0 6 * * Tue expr \( `date +\%s` / 604800 + 1 \) \% 2 > /dev/null || /scripts/fortnightly.sh

Changing the 1 to a 0 will move it back to even weeks.

Vinasse answered 20/10, 2015 at 4:22 Comment(1)
Actually, you don't need the 0 or 1. Simply change the || to &&. If the expression evaluates to 1, it will execute the command after the &&, otherwise, execute the command after the ||Intracranial
N
3

If you want to do it based on a given start date:

0 6 * * 1 expr \( `date +\%s` / 86400 - `date --date='2018-03-19' +\%s` / 86400 \) \% 14 == 0 > /dev/null && /scripts/fortnightly.sh

Should fire every other Monday beginning with 2018-03-19

Expression reads: Run at 6am on Mondays if ...

1 - Get today's date, in seconds, divided by the number of seconds in a day to convert to days sice epoch

2 - Do the same for the starting date, converting it to the number of days since epoch

3 - Get the difference between the two

4 - divide by 14 and check the remainder

5- If the remainder is zero you are on the two-week cycle

Nunatak answered 15/11, 2018 at 19:20 Comment(0)
S
1

I discovered some additional limitations of above approaches that can fail in some edge cases. For instance, consider:

@xahtep and @Doppelganger discussed issues using %W on certain year boundaries above.

@pilcrow's answer gets around this to some degree, however it too will fail on certain boundaries. Answers in this and or other related topics use the number of seconds in a day (vs. week), which also fail on certain boundaries for the same reasons.

This is because these approaches rely on UTC time (date +%s). Consider a case where we're running a job at 1am and 10pm every 2nd Tuesday.

Suppose GMT-2:

  • 1am local time = 11pm UTC yesterday
  • 10pm local time = 8pm UTC today

If we are only checking a single time each day, this will not be an issue, but if we are checking multiple times -- or if we are close to UTC time and daylight savings occurs, the script wouldn't consider these to be the same day.

To get around this, we need to calculate an offset from UTC based on our local timezone not UTC. I couldn't find a simple way to do this in BASH, so I developed a solution that uses a quick one liner in Perl to compute the offset from UTC in seconds.

This script takes advantage of date +%z, which outputs the local timezone.

Bash script:

TZ_OFFSET=$( date +%z | perl -ne '$_ =~ /([+-])(\d{2})(\d{2})/; print eval($1."60**2") * ($2 + $3/60);' )
DAY_PARITY=$(( ( `date +%s` + ${TZ_OFFSET} ) / 86400 % 2 ))

then, to determine whether the day is even or odd:

if [ ${DAY_PARITY} -eq 1 ]; then
...
else
...
fi
Scare answered 21/12, 2016 at 5:19 Comment(6)
Date does NOT use UTC time. It uses the time zone set on your system.Intracranial
@ScottieH please read the solution carefully. It adjust the date output to UTC time no matter what the system timezone is.Scare
Have you actually SEEN this behavior? Cron on my systems doesn't work the way you describe in your edge case scenario.Intracranial
date +%s returns a unix epoch time. This is a standardized format, in UTC, irrespective of any local timezone setting. Because of that, it will have edge cases depending on what your server timezone is. For example, daylight savings in the UK does not occur at the same time of year as daylight savings in the US. Also, several places in the US do not observe daylight savings, whereas, UTC does observe daylight savings. Converting unix epoch time to a local time is the goal, but there will be edge cases depending on what time you are running the script and your TZ offset to UTC.Scare
Your view of the OPS question is different than mine. To get a job to run every other Tuesday is much simpler that what you are trying to accomplish. $0.02Intracranial
@ScottieH your answer (like many of the others) looks simpler and will probably work in 99.9% of cases. But, there are edge cases due to time zone differences which can cause unexpected behavior or cron jobs to be missed. If the job is run every 2 weeks on the same day, the solution I proposed would work.Scare
I
1

There are many good answers here. Based upon the comments, I see quite a bit of confusion and frustration. My intention with this answer is to not only answer the OPs question How to instruct cron to execute a job every second week?, but also clear up some confusion for folks who may read this in the future.

TL;DR:
The crontab entry look like this:

\<minute\> \<hour\> * * \<Day of Week\> expr \\( $( date+\\%s ) \\/ 604800 \\% 2 \\) > /dev/null && \<command to run on odd weeks\> || \<command to run on even weeks\>

Crontab Entries:
All crontab entries [made with crontab -e] have this format, per the man page:
1: The Minute of the hour to execute [command]
Range: 0-59
2: The Hour of the Day to execute [command]
Range: 0-23 3: The Day of the Month to execute [command]
Range: 1-31
4: The Month to execute [command]
Range: 1-12
5: The Day of Week to execute [command]
Range: Check your man page, could be 0-6, 1-7 or 0-7
6: command to execute
Note, there are also @ values, which I will not discuss here.

Some versions of *nix allow for expressions:
*/2 = every even number in range
1 + */2 = every odd number in range
1,15 = First & 15th
2-8 = Second through the Eighth (inclusive)

Some versions allow for the Human Readable words also, such as February or Wednesday.

For Month, */2 will execute on Feb, Apr, Jun, Aug, Oct, Dec.

For Days of Week, */2 will run every even day -- check your man page to see how the Days of Week are numbered. Tue/2 is NOT a valid expression.

Human Readable (Calendar) Woes
Some constants you need to know:
There are 365.2464 days in a year (For convenience, we'll round to 365).
There are 12 months in a year.
There are 7 days in a week.
There 86,400 seconds in a day.

Therefore,
1 month is 4-1/3 weeks
[i.e. 3 months is 13 weeks]
1 year is 52-1/7 weeks

The bane of calendar math:
Semi-Monthly = every half month = 2x/month = 24 times per year.
Bi-weekly = every other week (fortnightly) = 26 times per year.
Note: These terms are often mis-used.
Some years have 51 weeks, some have 53, most have 52. If My cron runs every odd week ( date +%W mod 2), and the year has 51 or 53 weeks, it will also run the following week, which is week 1 of the new year. Conversely, if my cron runs every even week, it will skip 2 weeks. Not what I want.

CRON can support semi-monthly, it cannot support bi-weekly! Semi-monthly:
The first of the month will always fall between the 1st & 7th. The second half of the week will always occur between the 15th and the 21st.
Semi-monthly would have 2 values, one in the first half and the other in the second half of the month. Such as:
2,16

Unix Time
At a very high level, *nix time is 2 values:
date +%s = Number of Seconds since Epoch (01/01/1970 00:00:00)
date +%N = fractional seconds (in Nano seconds)
Therefore, time in *nix is date +%s.%N

*Nix uses epoch time. The /etc/shadow file contains the date of last password change. It is the integer portion of %s divided by 86,400.

Factino
The GPS satellites measure time as "Number of weeks since epoch time" and "(fractional) seconds into the week.
Note: Epoch weeks are independent of years. It does not matter if the year has 51, 52, or 53 weeks. Epoch weeks never roll over.

Bi-Weekly Time Algorithm
In *Nix date +%W is week number of the year, not epoch week. *nix does not have an epoch week value, but it can be computed.
Epoch Week = Integer( Epoch Seconds / Seconds_per_Day / Days_per_Week )
Fortnightly = Epoch_Week Modulo 2
The Fortnightly value will always be 0 or 1, thus, running the command when Fortnightly = 0 runs on every even week, and 1 = every odd week.

Computing Fortnightly)
The first way (bc):
date +"%s / 604800 % 2" | bc
This will generate "[Epoch Seconds] / 604800 % 2"
That answer is then sent to bc, a basic calculator, which does the math and echoes the answer to STDOUT and any errors to STDERR.
The return code is 0.

The second way (expr):
In this case, send the expression to expr and let it do the math expr $( date +%s ) / 604800 % 2
The expr command does the math, echoes the answer to STDOUT, errors to STDERR. The return code is 1 if the answer is 0, otherwise, the return code is 0.
In this manner, I don't care what the answer is, only the return code. Consider this:

$ expr 1 % 2 && echo Odd || echo Even
1
Odd 
$ expr 2 % 2 && echo Odd || echo Even 
0 
Even

Since the result doesn't matter, I can re-direct STDOUT to /dev/null. In this case, I'll leave STDERR incase something unexpected happens, I'll see the error message.

Crontab Entry
In crontab, the percent sign and the parenthesis have special meanings, so they need to be escaped with a backslash (\).
First, I need to decide if I want to run on Even weeks or odd weeks. Or, perhaps, run Command_O on Odd weeks and command_E on even weeks. Then, I need to escape all the special characters.
Since the expr only evaluates WEEKS, it is necessary to specify a particular day of the week (and time) to evaluate, such as every Tuesday at 3:15pm. Thus, the first 5 files (the Time Entry) of my crontab entry will be:
15 3 * * TUE
The first part of my cron command will be the expr command, with STDOUT sent to null:

expr \\( $( date+\\%s ) \\/ 604800 \\% 2 \\) > /dev/null

The second part will be the evaluator, && or ||
The Third part will be the command (or script) I want to run. Which looks like this:

expr \\( $( date+\\%s ) \\/ 604800 \\% 2 \\) > /dev/null && \<command_O\> || \<command_E\>

Hopefully, that will clarify some of the confusion

Intracranial answered 26/8, 2021 at 23:3 Comment(3)
This post has the same errors as many of the others. See my post. If your server is running in UTC time, this solution will work. However, for any other timezone, there is a risk of running into an edge case due to daylight savings... and the fact that a) not everywhere observes daylight savings, and b) daylight savings rules are different for different countries (e.g., the month/week are not sync'd). Consider epochs 1616025600 and 1616025599. If your cron time is on the boundary, and you are experiencing a daylight saving event, you can produce the wrong result.Scare
NOT TRUE! There are 86,400 seconds in one day. 2 weeks contains 1,209,600 seconds. Thus, every other week is simply a passing of time, which has NOTHING TO DO with my time zone. If, by chance, I need my cron jobs synchronized over a geographically diverse network, then you have a point. But, that is not the OPs question. The question is (effectively): How do I execute a cron job every 1,209,600 seconds. /DoneIntracranial
I think there is a subtle, but critical point here that you are missing. No one is debating the number of seconds in a day. Cron jobs are however referenced in terms like "every Tues @ 11pm". Due to daylight savings, and differences between UTC and each individual time zone daylight savings, the "effective" difference between unix epochs can be unexpected. Consider: 1615708800 = 3/14/21 12:00AM in US/Pacific time. 1615791600 = 3/15/21 12:00AM in US/Pacific time. The difference between the unix epochs is only 82,800 seconds.... yet according to a cron job 24 hours should have passed.Scare
C
0

Try this

0 0 1-7,15-21 * 2

This is run 2 times a month, both on Tuesdays and at least a week apart.

Calm answered 30/6, 2021 at 9:25 Comment(0)
C
0

30 4 */2 * 1

4:30 in the morning every 2nd day of a month if it's Monday

Chatterton answered 22/11, 2023 at 13:32 Comment(0)
H
-1

If you want every tuesday of second week only:

00 06 * * 2#2

Hokkaido answered 23/11, 2016 at 12:43 Comment(1)
Which cron on which Linux supports that? The standard cron on Linux uses # as comment delimiter.Renayrenckens
G
-4

Cron provides an 'every other' syntax "/2". Just follow the appropriate time unit field with "/2" and it will execute the cronjob 'every other time'. In your case...

0 6 * * Tue/2

The above should execute every other Tuesday.

Galilean answered 22/2, 2013 at 11:11 Comment(3)
I've tested this command, and it runs every Tuesday, Thursday, and Saturday; not every other Tuesday.Armes
Have you tested 0 6 * * Tue/14 by any chance ?Incident
pity - Tue/2 is not running on my system :(Altman
G
-4

Syntax "/2" is not goes along with weekday. So my added to the smart above is simply use 1,15 on MonthDay filed.

0 6 1,15 * * /scripts/fornightly.sh > /dev/null 2>&1

Gauntlett answered 6/5, 2013 at 8:47 Comment(2)
Note that this runs on the 1st and 15th of each month, which is not fortnightly (every two weeks). Sometimes there are more than 4 weeks in a month. What you have here is "twice per month" and not "every other week".Shortage
Hmm. I still can't figure out the solution for a similar request: a job to run at midday every alternate Monday throughout the year, regardless of the month. 0 12 * * 1/2 says "bad day-of-week".Kenleigh
M
-5

Why not something like

0 0 1-7,15-21,29-31 * 5

Is that appropriate?

Microcurie answered 26/11, 2013 at 19:59 Comment(4)
This will only run the first, third, and fifth week of every month.Microcurie
But if changed to 0 0 15-21 * 5 this would appear to do the trick.Picador
Re-read the man page for crontab(5). Your format will run on every day for the first week, every day on the third week, and every day of the last week, plus any day that happens to be a Friday. That's a lot of runs.Shortage
Agree with @ChristopherSchultzFenestella
H
-5

0 0 */14 * *

Runs At 00:00 on every 14th day-of-month

Heyman answered 7/2, 2020 at 0:15 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.