cron 表达不了的日历规则
cron 的五个时间字段表达不了“每月最后一天”“每月第一个工作日”这类规则,它只能按固定的日或星期匹配。常见解法有三种:写一行每天都跑的判断(用 date 判断明天是不是 1 号,等价于今天是月末);用 28 到 31 日的多值匹配后在脚本里做幂等去重;或者改用 systemd timer 的 OnCalendar,其中 ~ 表示当月最后一天。把判断逻辑放进脚本最通用,也不受系统特性限制。
基本信息
| 适用平台 | 任意 cron(判断式与多值式);systemd 的 ~ 语义需要 236 及以上版本 |
|---|---|
| 核心手法 | date 判断、多值加判断、OnCalendar |
| 关键命令 | date -d tomorrow +%d、date +%d |
| 跨平台差异 | GNU date 用 -d,BSD/macOS 用 -v1d |
| 硬性要求 | 多值触发时脚本必须幂等 |
典型使用场景
- 每月最后一天出账单或月度报表
- 月末做归档、月结与上月数据清理
- 每月第一个工作日执行对账
- 季度末做容量评估与汇总
- 任何按日历边界而非固定日期触发的任务
三种写法
三种写法
# 每天 8 点跑,仅在“明天是 1 号”时执行(即今天是月末)
# crontab 中的百分号需要转义
0 8 * * * [ "$(date -d tomorrow +\%d)" = 01 ] && /opt/scripts/month-end.sh
# 多值覆盖 28 到 31 日,脚本内部再做月末判断与幂等控制
0 8 28-31 * * [ "$(date -d tomorrow +\%d)" = 01 ] && /opt/scripts/month-end.sh
# 手动验证判断逻辑(不改 crontab)
date -d tomorrow +%d
[ "$(date -d tomorrow +%d)" = 01 ] && echo '今天是月末'
# systemd timer 的写法(需要 236 及以上版本)
# OnCalendar=*-*-~1 08:00:00 # ~ 表示当月最后一天三种解法对照
| 解法 | 怎么写 | 优缺点 |
|---|---|---|
| 每日判断式 | 每天都跑,脚本内判断明天是否为 1 号 | 简单通用;每天启动一次但通常立刻退出 |
| 多值匹配式 | 日字段写 28-31 再加判断 | 触发次数少;必须幂等,否则可能一个月跑多次 |
| OnCalendar ~ 语义 | systemd timer 直接表达最后一天 | 语法简洁;依赖 systemd 版本 |
| 判断依据 | date -d tomorrow +%d 是否为 01 | GNU date 特性,跨平台需换写法 |
| 幂等保障 | 脚本内加锁或状态标记 | 多值触发时的必备措施 |
同类需求(每月第一个周一、每月第几个工作日)思路一致:让 cron 负责按天或按周唤醒,把日历判断放进脚本,这样最通用,也不受 systemd 版本限制。
date -d 是 GNU coreutils 的写法,BSD 与 macOS 上要用 date -v+1d:跨平台脚本必须区分,否则判断恒为假,任务配置看着完全正确却从不执行。