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

发布时间:2026/9/30 1:39:46
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因为循环次数事先看不到start2024-01-28 end2024-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 passgrep -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再拼回原字符串。比如d2024-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} doneprintf %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 -v1d -f %Y-%m-%d $cur %F)第一句在 GNU date 下成功在 BSD date 下因为参数非法会失败输出被 2/dev/null 丢弃然后走第二句。-j表示不修改系统日期-v1d表示加一天-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 命令临时指定时区TZAsia/Shanghai date %FTZ是环境变量加在命令前面只会影响这一次 date 调用不会污染整个脚本。注意容器里如果没装 tzdata 时区库指定Asia/Shanghai未必生效。更稳的办法是设置TZUTC后全部按 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 21 || { echo unparsable start: $start 2; return 1; } date -d $end /dev/null 21 || { 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 21再确认这个日期真的存在比如 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 改成别的值或者把写成执行到对应断言就会直接暴露。日期遍历脚本最怕的是偶尔错一天这种错在日志里很隐蔽用固定断言堵住边界是最好的后悔药。我现在的习惯是所有日期相关脚本开头先跑一次自测宁可早报错也不要等到批量补数跑到一半才发现日期序列断了希望帮到你。本文还有配套的精品资源点击获取