Skip to content

Instantly share code, notes, and snippets.

@Birdie0
Last active September 15, 2023 12:47
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Birdie0/253501106d22bc06fd7ed53799526ad5 to your computer and use it in GitHub Desktop.
Save Birdie0/253501106d22bc06fd7ed53799526ad5 to your computer and use it in GitHub Desktop.
end of month cron

End-of-month cron

Super one-line alternative


If for some reason you want to run cron in last day of month you would likely need 3 separate cron schedule expressions:

run 31st at months with 31 days

0 0 31 1,3,5,7,8,10,12 * some-command
# or
0 0 31 JAN,MAR,MAY,JUL,AUG,OCT,DEC * some-command

run 30th at months with 30 days

0 0 30 4,6,9,11 * some-command
# or
0 0 30 APR,JUN,SEP,NOV * some-command

run february 29th if it's leap year, otherwise run february 28th

Updated

We can use date -d tomorrow +%d command which will return 1 if tomorrow is the day of next month:

0 0 28,29 2 * [ $(date -d tomorrow +%d) -eq 1 ] && some-command
# or
0 0 28,29 FEB * [ $(date -d tomorrow +%d) -eq 1 ] && some-command

Original solution

This one is going to be tricky as cron runs in sh instead of bash, we are going to use arithmetic expansion here $((2 + 2)). Difference from bash version that we can't put two expressions in one as , is not available.

# sets $year to current year
year=$(date +%Y)
# year is leap if divisible by 400 or divisible by 4 but at the same time not divisible by 100
# arithmetic expansion returns 1 on true and 0 on false
# will run on leap year
[ $((year % 400 == 0 || year % 4 == 0 && year % 100 != 0)) -eq 1 ]
# will run on non-leap year
[ $((year % 400 == 0 || year % 4 == 0 && year % 100 != 0)) -eq 0 ]
# so, instead of making 2 separate cron tasks for 28th and 29th we can do a bit different condition:
# since expansion returns 1 on leap year adding 28 to it will result in 29
# and on non-leap year, 0 + 28 = 28, comparing sum with current day will
[ $(((year % 400 == 0 || year % 4 == 0 && year % 100 != 0) + 28)) -eq $(date +%d) ]

Here's result: (in cron % have to be escaped \%)

0 0 28,29 2 * year=$(date +\%Y); [ $(((year \% 400 == 0 || year \% 4 == 0 && year \% 100 != 0) + 28)) -eq $(date +\%d) ] && some-command
# or
0 0 28,29 FEB * year=$(date +\%Y); [ $(((year \% 400 == 0 || year \% 4 == 0 && year \% 100 != 0) + 28)) -eq $(date +\%d) ] && some-command

Super one-line alternative

Using previous approach all the previous cron expressions can be replaced with one:

0 0 28-31 * * [ $(date -d tomorrow +%d) -eq 1 ] && some-command

Since running it every day would be useless, setting range to 28-31 would be the most optimal in scope on one expression.

Super two-line alternative

0 0 28,29 2 * [ $(date -d tomorrow +%d) -eq 1 ] && some-command
0 0 30-31 1,3-12 * [ $(date -d tomorrow +%d) -eq 1 ] && some-command
#or
0 0 28,29 FEB * [ $(date -d tomorrow +%d) -eq 1 ] && some-command
0 0 30-31 JAN,MAR-DEC * [ $(date -d tomorrow +%d) -eq 1 ] && some-command
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment