DEBUG.WIKI
V.PS:让项目靠近用户,亚太、欧洲、北美 KVM VPS
IPPeak:从目标地区访问网页,住宅代理、SEO 监测、广告验证
ZgoCloud:多地区部署,按需选节点;日本、美国、香港、德国

GNU date 在月底计算上个月,结果仍是本月

GNU date 在月底减一个月可能规范化回本月。用本月 15 日作为锚点取得上一日历月年月,并核对业务时区、短月与跨年边界。

月底运行 date -d '1 month ago' +%Y%m,结果仍是本月。例如在 3 月 31 日执行,预期得到 2 月,输出却可能仍以 3 月开头。将表达式换成 last month 也不能避开这个问题。

GNU date 的相对月份计算不是“找到上一个月份并取其最后一个有效日期”。月长不同,保留当前日号再减一个月可能得到不存在的日期,随后规范化又落回本月。下面只处理“取得上一日历月的年月”这一需求,不把它推广成订阅到期日或月底结算日算法。

先把当前日期固定下来观察

不要靠等待月底来检查脚本,可以提供一个明确日期:

TZ=UTC0 date --date='2023-03-31 -1 month' +%F

按 GNU date 的相对日期规则,该例预期为 2023-03-03。它相当于遇到了无法直接表示的 2 月 31 日,再向后规范化。闰年与非闰年的落点可能不同;问题并不是每个月都固定偏移三天。

把输出格式改成 %Y%m 只隐藏了日号,不会改变日期计算。因此错误日期依然会得到 202303,适合按月命名的格式也无法修复月份本身。

本文使用 GNU coreutils 的 date --date 语法。先用 date --version 确认实现;macOS 自带 BSD date 的选项不同,不能把这里的 -d 命令原样当作跨平台写法。

用本月 15 日作为计算锚点

每个月都有 15 日。只需要上一日历月的年月时,先确定业务所用时区中的当前年月,再将日号固定为 15,之后减一个月:

#!/bin/sh
set -eu

current_month=$(TZ=Asia/Shanghai date +%Y-%m)
previous_month=$(TZ=UTC0 date --date="${current_month}-15 -1 month" +%Y%m)
printf '%s\n' "$previous_month"

Asia/Shanghai 是本例的业务时区,应按报表或目录命名规则替换。第一次调用只读取一次当前年月,避免在跨月瞬间分别读取年、月、日而拼出不一致的输入。

第二次调用把已经选定的年月当作日历字段,在 UTC0 下计算。它不是把业务时区的某个时间戳转换成 UTC;目的只是避开夏令时或其他时钟调整对中间日期的影响。因为最终只取年月,15 日不会越过较短月份的末尾。

不要直接把第一次调用也改成 UTC,而不确认业务要求。同一瞬间,上海可能已经进入新月份,UTC 仍在上一个月;那样会算出另一套月份边界。

用固定年月检查跨年和短月

可把上面的两个日期调用替换为明确输入,验证月份映射:

TZ=UTC0 date --date='2023-03-15 -1 month' +%Y%m
TZ=UTC0 date --date='2024-03-15 -1 month' +%Y%m
TZ=UTC0 date --date='2024-01-15 -1 month' +%Y%m

这三条命令的预期输出分别为 202302、202402、202312。前两项覆盖平年和闰年的 2 月,最后一项覆盖年界。这里检查的是输出年月,不要求结果日期等于上个月最后一天。

如果真正需要“一个月前的同一天”,必须先规定不存在的日号如何处理:截到月底、向后规范化或拒绝该输入,会产生不同结果。15 日锚点只适用于本篇的月份选择,不能保留原始日号,也不能直接承担这些业务规则。

同样,减去固定的 30 天并不等于减去一个日历月。用它替代 -1 month,会在不同月长下得到另一种偏差。

接回文件任务前只检查输出

先把目标月份打印到日志,检查月底、月初和跨年的预期,再让后续任务使用它。不要把尚未验证的月份变量直接拼进删除、移动或覆盖命令;一次选错月份可能影响仍需保留的文件。

脚本中的 set -e 用于让这里的日期赋值失败时停止,避免后续拿空值继续处理;它不替代对业务月份的检查。脚本用到的时区数据和 GNU date 应在实际任务环境中可用。

保留修改前脚本和月份选择记录。回退代码只能恢复旧的计算方式,不能撤销已经按错误月份删除或覆盖的数据;这类结果需要依据任务日志和备份恢复。