8

I want to execute a shell script literally after every 3 days. Using crontab with 01 00 */3 * *won't actually fulfil the condition, because it would run on 31st and then again on the 1st day of a month. The */3 syntax is the same as saying 1,4,7 ... 25,28,31.

There should be ways to make the script itself check the conditions and exit if 3 days haven't passed. So crontab executes the script everyday, but the script itself would check if 3 days have passed.

I even found some code, but it gave me syntax error, any help would be appreciated.

if (! (date("z") % 3)) {
     exit;
}

main.sh: line 1: syntax error near unexpected token `"z"'
main.sh: line 1: `if (! (date("z") % 3)) {'
  • 4
    What do you mean, exactly? How does */3 not work? "if 3 days haven't passed": three days since what? Please edit your question and clarify. – terdon Sep 25 '16 at 12:57
  • 4
    literally literally? This seems like an x/y question and you might want to actually talk about what you're trying to do. Are you trying to stop crontab from running the script? – Journeyman Geek Sep 25 '16 at 12:57
  • 1
    Edited the question to explain why crontab solution won't work. – Taavi Sep 25 '16 at 13:07
  • Something like date("z") % 3 == 0 would suffer from a similar problem: the condition will be false for the four days between December 29th and January 3rd, unless that December was part of a leap year. – Rhymoid Sep 25 '16 at 22:15
12

To immediately abort and exit a script if the last execution was not yet at least a specific time ago, you could use this method which requires an external file that stores the last execution date and time.

Add these lines to the top of your Bash script:

#!/bin/bash

# File that stores the last execution date in plain text:
datefile=/path/to/your/datefile

# Minimum delay between two script executions, in seconds. 
seconds=$((60*60*24*3))

# Test if datefile exists and compare the difference between the stored date 
# and now with the given minimum delay in seconds. 
# Exit with error code 1 if the minimum delay is not exceeded yet.
if test -f "$datefile" ; then
    if test "$(($(date "+%s")-$(date -f "$datefile" "+%s")))" -lt "$seconds" ; then
        echo "This script may not yet be started again."
        exit 1
    fi
fi

# Store the current date and time in datefile
date -R > "$datefile"

# Insert your normal script here:

Don't forget to set a meaningful value as datefile= and adapt the value of seconds= to your needs ($((60*60*24*3)) evaluates to 3 days).


If you don't want a separate file, you could also store the last execution time in the modification time stamp of your script. That means however that making any changes to your script file will reset the 3 counter and be treated like if the script was successfully running.

To implement that, add the snippet below to the top of your script file:

#!/bin/bash

# Minimum delay between two script executions, in seconds. 
seconds=$((60*60*24*3))

# Compare the difference between this script's modification time stamp 
# and the current date with the given minimum delay in seconds. 
# Exit with error code 1 if the minimum delay is not exceeded yet.
if test "$(($(date "+%s")-$(date -r "$0" "+%s")))" -lt "$seconds" ; then
    echo "This script may not yet be started again."
    exit 1
fi

# Store the current date as modification time stamp of this script file
touch -m -- "$0"

# Insert your normal script here:

Again, don't forget to adapt the value of seconds= to your needs ($((60*60*24*3)) evaluates to 3 days).

| improve this answer | |
  • Yes, recording the last successful invocation of a particular program requires some kind of external data store, and the file system is an obvious choice for that. – Kilian Foth Sep 26 '16 at 6:46
  • Don't even need to store the date - just touch the file each time and check it's time stamp. – djsmiley2kStaysInside Sep 26 '16 at 11:20
  • @djsmiley2k Yes, you are right. If the risk that modifying the script file manually causes the delay to reset is acceptable, one can also use the modification time stamp. I added that to my answer. – Byte Commander Sep 26 '16 at 11:33
  • This can be golfed slightly by saving the current timestamp to the file(instead of human readable date), so you can then get the elapsed time with $[ $(date +%s) - $(< lastrun) ]. If the script is ran from cron once a day, I might add some slack to the required time interval, so that if the execution of the script gets delayed by a couple of seconds, the next time will not skip a full day. That is check every day if 71 hours have passed oslt. – ilkkachu Sep 26 '16 at 11:43
  • This wizardry with datefile actually worked, thank you very much! – Taavi Sep 28 '16 at 22:08
12

Cron really is the wrong tool for this. There's actually a commonly used and underloved tool called at which might work. at's mainly designed for interactive use and I'm sure that someone would find a better way to do this.

In my case I'd have the script I am running listed in testjobs.txt, and include a line that reads.

As an example, I would have this as testjobs.txt

echo "cat" >> foo.txt
date >> foo.txt
at now + 3 days < testjobs.txt

I have two innocent commands, which might be your shellscripts. I run echo to make sure I have a deterministic output, and date to confirm the command runs as needed. When at runs this command, it will finish by adding a new job to at for 3 days. (I tested with one minute - which works)

I'm pretty sure I'll be called out for the manner I've abused at, but its a handy tool for scheduling a command to be run at a time or x days after a previous command.

| improve this answer | |
  • 3
    +1 as at does seem the better approach here.The issue with this approach is that any time you manually run the script, you add another set of every-3-day at tasks. – Dewi Morgan Sep 25 '16 at 15:20
  • 1
    +1, but another issue (in addition to the one pointed at by @DewiMorgan) is that if one script fails, all subsequent script will not be launched (if the at is after the failure point), until one realises that the script had failed and relaunch it. This can be bad, or sometimes good (ex: fails because a condition is no longer there : it's good that it doesn't retry in 3 days?). And there is a slight drift every time (a few milliseconds if at is at the top, or potentially much more if at is near the bottom of a long-execution script) – Olivier Dulac Sep 26 '16 at 9:47
  • At can send emails.... Which might be a solution for failure. atq and atrm would allow for culling errant jobs maybe? In theory you could script a specific date for at but that seems inelegant and handhold. – Journeyman Geek Sep 26 '16 at 10:04
  • So have a cronjob that checks for the existence of the next run in at; if there isn't one, add one for 3 days and warn (or run it immediately, and check again). – djsmiley2kStaysInside Sep 26 '16 at 11:21
3

First, the code fragment above is invalid Bash syntax, looks like Perl. Second, the z parameter to date causes it to output the numeric time zone. +%j is the day number. You need:

if [[ ! $(( $(date +%j) % 3 )) ]] ;then
     exit
fi

But you're still going to see strangeness at year-end:

$ for i in 364 365  1 ; do echo "Day $i, $(( $i % 3 ))"; done
Day 364, 1
Day 365, 2
Day 1, 1

You might have better luck with keeping a count in a file, and testing/updating that.

| improve this answer | |
  • 1
    Perl doesn't have a date() function builtin, but that looks a bit like date() in PHP (where z is "The day of the year (starting from 0)") – ilkkachu Sep 26 '16 at 11:54
2

If you can just leave the script running perpetually you could do:

while true; do

[inert code here]

sleep 259200
done

This loop is always true, so it will always execute the code, then wait for three days before starting the loop again.

| improve this answer | |
  • Why not while true? – Jonathan Leffler Sep 25 '16 at 14:31
  • Hah! Good catch. I haven't needed to use that in a long time, I forgot it existed lol – mkingsbu Sep 25 '16 at 14:32
  • 2
    As the at solution, this will drift by the execution time of the script each run. Of course that can be worked around by saving the time when the script starts and sleeping until 3 days from that. – ilkkachu Sep 26 '16 at 11:56
2

You can use anacron instead of cron, it is designed exactly to do what you need. From the manpage:

Anacron can be used to execute commands periodically, with a frequency specified in days. Unlike cron(8), it does not assume that the machine is running continuously. Hence, it can be used on machines that aren’t running 24 hours a day, to control daily, weekly, and monthly jobs that are usually controlled by cron.

When executed, Anacron reads a list of jobs from a configuration file, normally /etc/anacrontab (see anacrontab(5)). This file contains the list of jobs that Anacron controls. Each job entry specifies a period in days, a delay in minutes, a unique job identifier, and a shell command.

For each job, Anacron checks whether this job has been executed in the last n days, where n is the period specified for that job. If not, Anacron runs the job’s shell command, after waiting for the number of minutes specified as the delay parameter.

After the command exits, Anacron records the date in a special timestamp file for that job, so it can know when to execute it again. Only the date is used for the time calculations. The hour is not used.

| improve this answer | |
  • Nice, i will sure test Anacron. Wonder why it is so underrated if it can do such magic – Taavi Sep 26 '16 at 13:51
  • The real question: why has cron not been replaced with something better by now? fcron exists and I think systemd is working on its own solution, but I'm not up to date with the current state of affairs. – Twinkles Sep 26 '16 at 14:33

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.