cron 如何在每月最后一天运行任务?
cron 没有"每月最后一天"表达式,标准写法是 28-31 日都触发、用 date 判断明天是否是 1 号;附 crontab 百分号转义坑、systemd 日历替代方案与 FAQ。
cron 表达式本身无法表达"月末"。惯用解法:把日期字段设为 28-31,让任务在最后四天每天都触发,命令前加一段 date 判断——只有"明天是 1 号"时才真正执行。
0 0 28-31 * * [ $(date -d "+1 day" +\%d) = "01" ] && /path/to/command
工作原理逐段拆解
0 0:每天 0 点触发。28-31:一个月的最后四天都可能成为"最后一天"(28、29、30、31 取决于月份),所以这四天都让它醒一下。[ $(date -d "+1 day" +\%d) = "01" ]:取"明天"的日号,等于01说明今天就是月末,测试通过才真正执行&&后面的命令。
最大的坑:% 必须转义
crontab 里 % 有特殊含义(换行/标准输入分隔符),必须写成 \%。 忘掉转义是最常见的失败原因——任务到点触发了,命令却被 cron 截断,日志里只留下半截语法错误。
# 错误:%d 里的 % 会被 cron 吃掉
0 0 28-31 * * [ $(date -d "+1 day" +%d) = "01" ] && cmd
# 正确
0 0 28-31 * * [ $(date -d "+1 day" +\%d) = "01" ] && cmd
替代方案
-
systemd timer:
OnCalendar=*-*~1直接表达"每月最后一天"(~表示倒数日),比 crontab hack 干净:[Timer] OnCalendar=*-*~1 00:00:00 -
封装进脚本:把判断逻辑放进脚本内部(
is_last_day && main),crontab 保持简单,逻辑更好测。
观测云对照
月末任务往往是月结、报表、归档这类"漏跑一次就补不回来"的关键作业。把脚本输出统一落到日志并用 DataKit 采集,按任务名做"心跳式"监控:每月 1 日凌晨检查前一天是否产生了成功日志,没有就立即告警。这样无论 cron 没触发、判断写错还是脚本报错,都能第一时间知道。
常见问题(FAQ)
Q:为什么任务设了但月末那天没执行?
A:先查 % 有没有转义成 \%。再看 /var/log/cron(或 journalctl -u cron)里该时间点的记录,命令有没有被截断一目了然。
Q:2 月 28 号会跑吗?闰年呢?
A:会。28-31 覆盖了所有月份的可能末日:平年 2 月在 28 号触发且"明天是 1 号"成立;闰年在 29 号成立。其余月份同理。
Q:想在"每月倒数第 N 天"跑怎么办?
A:同样思路,把 +1 day 换成 +N day,判断日号是否等于 0N。systemd 则用 OnCalendar=*-*~N。