拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Shell脚本日期范围遍历的三种写法与五个高频坑

Shell脚本日期范围遍历的三种写法与五个高频坑

简介:一份面向Linux/Unix系统运维与脚本开发者的Shell实用技巧资料,主题聚焦“遍历日期范围”这一常见自动化需求。资料展示了一个完整脚本示例,通过接收开始日期与结束日期两个参数,借助date -d将日期转为Unix时间戳,再配合外层安全边界与内层递减循环,实现从起始日回溯到结束日的日期序列输出;其中%F与%s格式化选项、循环边界设定、跳出条件等关键点均有清晰说明,可直接迁移到日志分析、定时任务、批量按日期取数等场景中,对于需要按日批量调取接口、生成报表或归档日志的读者尤其有用。压缩包共1个文件,为PDF电子文档,大小约27KB,篇幅精简、要点集中,便于随时查阅。该资源已吸引2025人学习下载,适合刚接触Shell日期处理或希望提升脚本效率的读者快速上手。通过学习这份资料,能掌握日期范围遍历的基础写法,理解时间戳比较与循环控制逻辑,并参照示例调整循环次数和日期格式,应用到自己的自动化脚本中。

1. Shell脚本遍历一个日期范围实例:先把“循环日期”和“循环次数”分开想

Shell脚本遍历一个日期范围实例,平时看就是一个 for 循环套一个日期变量,真正写起来却要面对三个问题:日期序列怎么连续生成、循环到月底月底怎么进位、每一步拿到的日期格式是不是稳定。我见过不少从 2024-12-30 遍历到 2025-01-02 的脚本,跑出来的序列变成 2024-12-31、2024-12-32,然后后排接口 400 错误,排查的时候才发现是日期加法写成了字符串拼接。日期范围遍历在日志补拉、报表重算、接口批量取数里几乎每周都会出现,适合正在维护定时任务、经常做数据补齐的工程师。这里用一个可复现的按日遍历实例,把三种写法、五个坑和函数封装一次讲透。

2. 日期序列怎么生成:三种遍历写法的原理与选型理由

日期范围遍历的核心不在循环本身,而在“怎么把日期变成可以安全递增的序列”。Shell 里没有内置的日期数组,常见的做法是借用外部命令 date 和时间戳来完成累加。不同写法适合不同场景:跨度小用偏移量,通用脚本用字符串日期循环,跨月跨年或者范围很大时建议直接用秒级时间戳计数。

2.1 用 seq 配合 date -d 生成偏移日期:短跨度最好读

最常见的一种入门写法,先生成从 0 到 N 的偏移量,再以某个基准日期为基础做加法:

for offset in $(seq 0 9); do day=$(date -d "2024-01-01 + $offset days" +%F) echo "$day" done

这段脚本的逻辑是先用seq 0 9生成 0 到 9 的整数序列,然后每次循环用 GNU date 的-d参数计算基准日期加上 N 天后的日期,再用+%F输出成YYYY-MM-DD格式。+%F等价于+%Y-%m-%d,是写脚本时更简洁的写法。

这个方案的可读性在三种写法里是最高的,变量名里带 offset,别人接手时一眼能看懂。缺点是每轮循环都要 fork 一次 date 进程,跨度只有几天或几十天时无所谓,如果循环几百次就有明显的开销。另外这种方式要求你的 date 必须是 GNU 版本,macOS 自带的 BSD date 不认识date -d,后面会单独讲兼容问题。

它的适用边界很清晰:临时脚本、范围在 30 天以内、只按天遍历。一旦出现“从当月 25 号遍历到下月 5 号”这种需求,虽然命令本身没有问题,但心里要清楚它真正依赖的是 date 的日历计算能力,而不是脚本自己在跨月。

2.2 用 while 做“日期 +1 天”循环:不依赖时间戳的最常见写法

第二种写法是维护一个字符串日期变量,每轮让日期自增一天,直到超过结束日期。这里循环用 while,而不是 for i in,因为循环次数事先看不到:

start="2024-01-28" end="2024-02-03" cur="$start" while [[ "$cur" < "$end" ]] || [[ "$cur" == "$end" ]]; do echo "$cur" cur=$(date -d "$cur + 1 day" +%F) done

这段脚本从 start 开始逐天输出,直到 cur 等于 end。核心是最后一行:把当前日期字符串拼进date -d "$cur + 1 day",由 date 计算下一天,再格式化成同样的YYYY-MM-DD。因为输出格式固定,字符串做字典序比较也是安全的,2024-02-03与2024-02-03能正确判定相等。

这种写法最贴近手写日历的思考方式,逻辑直白,缺点是对 date 命令的解析能力要求比较高。date -d支持YYYY-MM-DD + N days这种表达式,是 GNU date 的扩展能力,跨平台时会踩坑。另一个容易翻车的地方是终止条件写错,常见错误是写成while [[ "$cur" < "$end" ]],少了一个相等判断,最后一天会漏掉。

它适合大多数 Linux 服务器上的通用脚本,也是我平时最常用的写法。比起 seq 偏移量,它能处理任意长度的连续日期,脚本里不需要预先算好偏移总量。

2.3 用时间戳当计数器:跨月、闰年和大跨度更稳

第三种写法不在字符串上做加法,而是把起止日期都转成 Unix 时间戳,循环每轮给时间戳加 86400 秒,再转换回日期输出:

start_ts=$(date -d "2024-01-28" +%s) end_ts=$(date -d "2024-02-03" +%s) for ((cur_ts = start_ts; cur_ts <= end_ts; cur_ts += 86400)); do echo "$(date -d "@$cur_ts" +%F)" done

+%s输出自 1970-01-01 至今的秒数,@$cur_ts是 GNU date 里把秒数转换回日期时间的标准写法。循环从 start_ts 走到 end_ts,步长是 86400 秒,正好是一天的时间长度。

时间戳写法的最大好处是避开了月份进位、闰年、日期格式不统一这些问题,条件比较变成纯整数大小判断,排错直观。代价是代码可读性略差,新读者要理解@是时间戳前缀。时区方面要注意 86400 秒并不总是等于本地时间的一天,遇到夏令时切换,日期虽然不会错,但一天可能从 00:00 变成 01:00;只输出%F日期部分没问题,如果同时输出时分秒就要小心。

三种写法不是互斥关系,我一般按这个原则选:临时小范围用 seq,通用脚本用 while 字符串日期,跨年或批量补数据用时间戳。下面的完整实例用时间戳实现,因为它最能覆盖边界。

3. 一个完整的按日遍历实例:补拉十天接口数据并落盘

日期遍历在真实项目里很少孤零零出现,通常是“按天拉接口、按天读日志、按天重跑任务”这类操作的一部分。这里用一个补拉接口数据的例子演示完整流程:给定起止日期,每天拉取一个 JSON 文件,失败自动重试,已经存在的文件跳过。

3.1 起止日期怎么来:参数、默认值和“动态昨天”

脚本最好支持从命令行传参,同时给一个合理默认值,避免改代码才能换日期范围:

#!/usr/bin/env bash set -Eeuo pipefail start="${1:-2024-01-01}" end="${2:-2024-01-10}" outdir="${3:-./data}" mkdir -p "$outdir" echo "range: $start -> $end"

这里的${1:-2024-01-01}意思是:如果没有传入第一个参数,就用2024-01-01作为默认值。同理第二个参数默认结束日期,第三个参数默认输出目录。set -Eeuo pipefail是 Bash 脚本的严格模式,遇到未定义变量、管道失败、单条命令失败都会立即退出,避免补数脚本在错误状态下继续跑。

我自己的习惯是默认结束日期用“昨天”而不是“今天”,因为当天数据往往不完整。写法是:

end="${2:-$(date -d yesterday +%F)}"

date -d yesterday是 GNU date 提供的相对日期表达式,输出昨天日期。要注意这个方案在 UTC 容器里会把系统当前时区的昨天算成业务时区的今天,具体在时区那一节说。

3.2 遍历主体:curl 拉取、失败重试与非空跳过

起止日期确定后,转成时间戳开始逐日遍历。下面的循环体是实例核心:

start_ts=$(date -d "$start" +%s) end_ts=$(date -d "$end" +%s) for ((cur = start_ts; cur <= end_ts; cur += 86400)); do day=$(date -d "@$cur" +%F) file="${outdir}/${day}.json" if [[ -s "$file" ]]; then echo "skip ${day}, file exists" continue fi for attempt in 1 2 3; do if curl -fsS "https://api.example.com/data?date=${day}" -o "$file"; then break else echo "retry ${day}, attempt ${attempt}" sleep $((attempt * 2)) fi done if [[ ! -s "$file" ]]; then echo "failed: ${day}" >&2 exit 1 fi done

循环里每一轮先算出当天的日期字符串,再拼出目标文件名。[[ -s "$file" ]]是判断文件存在且非空,如果上次已经跑过就跳过,这是补数脚本里很实用的断点续传机制。内层 for 循环尝试三次,curl -fsS中-f让接口返回 4xx/5xx 时命令直接失败,-s关闭进度条,-S保留错误信息。重试等待时间用$((attempt * 2))递增,1 秒、2 秒、4 秒,避免连续打爆接口。

最后一次如果文件还是不存在或为空,脚本会输出错误并退出。这里的exit 1很重要,外层有set -e时,内层 if 条件为假不会触发自动退出,必须手动检查最终结果。补数任务宁可失败在明处,也不要生成一批空文件后才被人发现。

3.3 用 grep 做结果校验:文件和内容两层检查

日期遍历完不代表数据一定可用,文件齐全还得看内容是否正常。grep 在 shell 脚本里最常见的用法就在这里:用grep -q做静默匹配,用grep -c统计命中行数,用grep -L反向列出不匹配的文件。先检查文件数量:

shopt -s nullglob json_files=("${outdir}"/*.json) echo "file count: ${#json_files[@]}"

shopt -s nullglob是关键,没有它时如果目录里没有 JSON 文件,通配符会原样保留成*.json,统计数量会得到 1,这是一个非常隐蔽的错误。文件数对得上后,再对每个文件检查内容标记:

for f in "${json_files[@]}"; do if ! grep -q '"status":"ok"' "$f"; then echo "bad content: $f" >&2 exit 1 fi done echo "all files pass"

grep -q只判断是否匹配,不输出任何内容,退出码 0 表示匹配成功。这里用! grep -q表示“没匹配到就算失败”,可以直接把不合格文件挡在门外。比wc -c检查非空更靠谱,因为很多接口失败时也会返回一个非空 JSON,但里面是错误信息。

4. 日期遍历的五个高频坑:从跨月、补零到时区与并发

这五个坑是日期脚本里最容易反复踩的,每一条都按“现象、原因、解决”展开,方便排查时对照。

4.1 跨月边界日期错乱:别对日字段做纯字符串加法

现象:遍历 2024-12-28 到 2025-01-02,输出序列里出现 2024-12-32,接口返回 400;有些脚本则把 12 月 31 日直接算成 2025-01-32,后面一路错。

原因:有人想省事,从日期字符串里拆出日字段做加 1,再拼回原字符串。比如:

d="2024-12-31" echo "$(( ${d:8:2} + 1 ))"

这行输出 32,因为 Shell 根本不认识月份有 12 月、12 月只有 31 天。日期进位是日历规则,Shell 字符串算术完全不知道。

解决:不要手动拆分日期字段。要么用date -d "$cur + 1 day"让命令完成进位,要么用时间戳加 86400 秒。这里没有捷径,越是想省事,越容易写出只有 1 月能正常跑的脚本。跨年场景最好先用时间戳写好,再单独验证 12 月 30 日到 1 月 2 日的用例。

4.2 前导零丢失:seq 和 printf 的补零问题

现象:遍历月份或者手工拼日期时,输出变成2024-1-1,而不是2024-01-01。单个数字时没问题,排序时却会发现2024-1-10排在2024-1-2前面,文件顺序错乱。

原因:date +%m默认会补零,但如果你用了date +%-m,或者通过seq 1 12生成月份数字,得到的是不带前导零的1、2这类值。seq按整数递增,默认不会自动补零。

解决:统一用固定宽度格式化输出:

for m in $(seq 1 12); do month=$(printf "%02d" "$m") echo "2024-${month}" done

printf "%02d"表示整数占两位,不足两位用 0 补齐。数字转日期字符串时,我习惯处处用%Y-%m-%d,尽量不手工拼字符串。手动拼${year}-${month}-${day}看着灵活,实际上是把格式控制权交了出去。

4.3 GNU date 与 macOS BSD date 参数不通用

现象:同一段遍历脚本在 Linux 服务器上跑得好好的,拿到 macOS 本机一执行就报错,提示illegal option -- d。

原因:GNU date 和 BSD date 的参数风格不同。Linux 上通用的date -d "2024-01-01 + 1 day",在 macOS 上会被解析成设置调试选项,直接报错。

解决:先探测当前环境支持哪种语法,再分别处理:

next=$(date -d "$cur + 1 day" +%F 2>/dev/null) || \ next=$(date -j -v+1d -f "%Y-%m-%d" "$cur" +%F)

第一句在 GNU date 下成功,在 BSD date 下因为参数非法会失败,输出被 2>/dev/null 丢弃,然后走第二句。-j表示不修改系统日期,-v+1d表示加一天,-f "%Y-%m-%d"声明输入格式。这种兼容写法会让脚本同时覆盖 Linux 和 macOS,但在 macOS 上时间戳加 86400 的方式仍然可用,因为date -r "$cur_ts" +%F是 BSD date 的秒数转日期写法。

如果脚本越来越复杂,我不建议在 Shell 里硬扛兼容性,跨平台就改用 Python 的 datetime 或 Perl 的 Time::Piece 生成日期序列,Shell 只负责调用,这样最省心。

4.4 时区导致“昨天”错位:零点前后最容易翻车

现象:定时任务每天晚上 23:50 跑,日志目录偶尔少一天,检查时发现脚本算出的“昨天”和业务日期对不上。

原因:date +%F用的是系统本地时区,不是业务时区。很多 Docker 容器默认时区是 UTC,和中国标准时间差 8 小时,北京时间凌晨 0 点后容器里还是前一天下午 4 点,取“昨天”自然错位。

解决:给 date 命令临时指定时区:

TZ=Asia/Shanghai date +%F

TZ是环境变量,加在命令前面只会影响这一次 date 调用,不会污染整个脚本。注意容器里如果没装 tzdata 时区库,指定Asia/Shanghai未必生效。更稳的办法是设置TZ=UTC后全部按 UTC 日期遍历,并在接口地址或文件名里显式标明时区,避免两个时区概念混在一个脚本里。日期遍历脚本最怕“看似正确”,我建议在脚本开头 echo 一行业务日期,人工对比一次再跑批量。

4.5 串行遍历几百天太慢:用 xargs 做按日并发

现象:要补过去一年的数据,365 天串行跑,每天接口响应加落盘要 10 秒,整体跑一个多小时,中间断一次又要重来。

原因:for 循环天然是串行的,每轮都等上一轮结束才开始。日期范围遍历每轮互相独立,本质是 embarrassingly parallel 的任务。

解决:先把整个日期范围生成到标准输出,再用 xargs 控制并发数:

seq_days() { local start="$1" end="$2" s e t s=$(date -d "$start" +%s) e=$(date -d "$end" +%s) for ((t = s; t <= e; t += 86400)); do date -d "@$t" +%F done } process_day() { day="$1" echo "handle $day" # 这里放实际的 curl 或计算逻辑 } export -f seq_days process_day seq_days 2024-01-01 2024-12-31 | xargs -P 8 -I {} bash -c 'process_day "$@"' _ {}

xargs -P 8表示同时运行 8 个进程,-I {}把每一行的日期替换到 bash -c 的参数位置,_是 bash -c 的第零个参数占位,$@才能正确拿到日期。每轮跑 10 秒的任务,8 并发能压到原来的八分之一左右。

实际使用中注意两点:一是并发任务写文件时文件名必须包含日期,避免不同进程写同一个路径;二是接口有频率限制时控制 -P 的数值,别把上游打挂。这个方案能解决大跨度性能问题,但网络类任务要量力而行。

5. 把日期遍历封装成可复用函数:加校验、留自测

5.1 一个带参数校验的 range_dates 函数

前面几节的逻辑可以收拢成一个函数,放到公共脚本库或 shell 配置文件里:

range_dates() { local start="$1" end="$2" cur_ts end_ts if [[ ! "$start" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then echo "invalid start date: $start" >&2 return 1 fi if [[ ! "$end" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then echo "invalid end date: $end" >&2 return 1 fi date -d "$start" >/dev/null 2>&1 || { echo "unparsable start: $start" >&2; return 1; } date -d "$end" >/dev/null 2>&1 || { echo "unparsable end: $end" >&2; return 1; } cur_ts=$(date -d "$start" +%s) end_ts=$(date -d "$end" +%s) while ((cur_ts <= end_ts)); do date -d "@$cur_ts" +%F cur_ts=$((cur_ts + 86400)) done }

正则校验保证格式是四位数年月日加横杠,date -d "$start" >/dev/null 2>&1再确认这个日期真的存在,比如 2024-02-30 这种值会在这里被挡下。用return 1而不是exit 1,函数出错时调用方可以自主决定是跳过还是退出,不让工具函数直接结束整个脚本。

5.2 三条自测断言,防止改坏边界

函数写好后,我建议永久保留一段自测代码,每次改动后跑一遍:

check_range() { local actual actual=$(range_dates "$1" "$2") local expected expected=$(printf '%s\n' "${@:3}") if [[ "$actual" != "$expected" ]]; then echo "FAIL: $1 -> $2" >&2 return 1 fi } check_range 2024-12-30 2025-01-02 \ 2024-12-30 2024-12-31 2025-01-01 2025-01-02 check_range 2024-01-01 2024-01-03 \ 2024-01-01 2024-01-02 2024-01-03 check_range 2024-02-28 2024-03-01 \ 2024-02-28 2024-02-29 2024-03-01 echo "all tests passed"

第一条自测跨年,第二条验证普通连续日,第三条验证闰年 2 月 29 日。如果哪天有人把 86400 改成别的值,或者把<=写成<,执行到对应断言就会直接暴露。日期遍历脚本最怕的是偶尔错一天,这种错在日志里很隐蔽,用固定断言堵住边界是最好的后悔药。我现在的习惯是所有日期相关脚本开头先跑一次自测,宁可早报错也不要等到批量补数跑到一半才发现日期序列断了,希望帮到你。

本文还有配套的精品资源,点击获取

返回列表