Skip to content
 

Last-day-of-month cron job

This is a classic problem. Most crons do not seem to have native support to specify "last day of every month", so if you need to run a job on such date you have to resort to some trick.

Do you really need it?

Of course, the first question to ask is "is it really necessary to run it on the last day of the month, or would just running monthly be good enough"? If your answer is the latter, then you can just run it on the first day of each month (or any other day between the 1st and the 28th included, for that matter):

0 23 1 * * /your/script.sh

Remember that the system crontab (usually /etc/crontab) has an extra field before the actual command to specify the user to run the command as.

GNU systems

Ok, you really need it to run on the last day of the month. If you're on a GNU system like Linux, or a system that has GNU date available, you're in good luck. The GNU implementation of the date utility has many more features than mandated by the standard. Of particular interest here is the -d (or --date) switch, which allows the specification of a date other than today's in a variety of formats.

How does this help? It does because it gives us a way to check if today is the last day of the month: if tomorrow's day of month is 1, then today must be the last day of some month, whatever it may be. So how to get tomorrow's date with GNU date? With this code:

$ date
Mon Mar 15 14:03:44 GMT 2010
$ date -d tomorrow
Tue Mar 16 14:03:50 GMT 2010

(instead of "tomorrow", you can also say "'+1 day'" or other equivalent specification)
Specifically, we don't care about the full date, and just need the day, so we can do

$ date +%d -d tomorrow
16

Now, let's get back to our crontab:

0 23 28-31 * * [ "$(/bin/date +\%d -d tomorrow)" = "01" ] && /your/script.sh

The entry need only be executed on days from 28 to 31, as it would be pointless to run it on other days. And of course, you can also move the date check directly into the script, perhaps as the very first thing it does.

This is a simple solution that works well and shifts all the hard work of calculating "tomorrow" (including leap years and the other subtleties of date arithmetic) to date. What if you don't have the luxury of GNU date in your system?

Non-GNU systems

Well, if you're reading here it means you probably have to implement the date check yourself. But since we're lazy, let's see if there's something we can exploit. Perl, for example, allows you to get tomorrow's day of month with something like

$ perl -e 'print ((localtime(time+86400))[3] . "\n")'
16

which again we can exploit in crontab:

0 23 28-31 * * [ "$(/usr/bin/perl -e 'print ((localtime(time+86400)[3])')" = "1" ] && /your/script.sh

(we don't need the newline, since it would be removed by command substitution anyway, and we check against "1", not "01", as that's what localtime() returns)
Perl is available on most systems, so this might be a possible solution for you.
Another, more adventurous, option is to parse the output of cal and see if the current day is the last of the current month. However, you have to experiment because there is no standard output format defined for cal; the good news is that once you find something that works for your particular platform, it's likely that you can keep using it safely. This seems to work for the output of the cal program provided by util-linux:

0 23 28-31 * * [ "$(/bin/date +\%d)" = "$(/usr/bin/cal | /usr/bin/awk 'NR>2&&NF{s=$NF}END{print s}')" ] && /your/script.sh

If everything else fails, your last resort is probably to implement the check yourself. It's mostly straightforward, except for the fact that leap years should be considered to correctly recognize the last day of February. Here is a simple function that checks if the provided year is leap:

is_leap() {
  # echoes 1 if $1 is leap, 0 otherwise
  if [ $(($1 %4)) -eq 0 ] && [ $(($1 % 100)) -ne 0 ] || [ $(($1 % 400)) -eq 0 ]; then
    echo 1
  else
    echo 0
  fi
}

The following code snippet will then check whether today is the last day of the month:

day=$(date +%d)
month=$(date +%m)
year=$(date +%Y)

case $month in
  01|03|05|07|08|10|12)
    last=31
    ;;
  04|06|09|11)
    last=30
    ;;
  02)
    last=$((28 + $(is_leap $year) ))
    ;;
esac

if [ "$day" = "$last" ]; then
  # do your stuff here
fi

7 Comments

  1. kanchan says:

    Below is not working for me on linux centos machine.

    Added below line /etc/crontab

    0 23 28-31 * * root [ "$(/bin/date +\%d -d tomorrow)" = "01" ] && php /var/www/html/test/myfile.php

    Please help

    • waldner says:

      In which way it does not work? Does the script run at all? You should be able to get some info from the cron log (don't know where it is on CentOS, may be /var/log/cron.log or /var/log/daemon.log or yet something else). Also make sure your cron support the 28-31 syntax (it should).

  2. Chad says:

    In the GNU cron tab entry, I had to escape the % on a RHEL system.
    0 23 28-31 * * [ "$(/bin/date +\%d -d tomorrow)" = "01" ] && /your/script.sh

    • waldner says:

      You are right, percent signs need to be escaped in crontab files (I was even bitten recently by this). Thanks, I've updated the text.

  3. An excellent discussion of this topic!

    Here is a C-Variant of the above Perl-Code.
    The cmd-part of the crontab-line will look like
    is-last-day-month && my-cron-job.sh

    #include <time.h>

    int main()
    {
    time_t t;

    t = time(NULL) + 86400;
    return localtime(&t)->tm_mday == 1;
    }

  4. Paul says:

    Typo above with the one liner in the Non-GNU systems section -

    $perl -e 'print ((localtime(time+86400)[3] . "\n")'
    syntax error at -e line 1, near ")["
    Execution of -e aborted due to compilation errors.

    The following works for me...

    $ perl -e 'print ((localtime(time+86400))[3] . "\n")'
    4