☰
Shell脚本遍历日期范围:避坑指南与可复用实例
2026/9/30 3:43:59 网站建设 项目流程

简介:一份面向Linux/Unix系统管理员与自动化脚本初学者的Shell脚本遍历日期范围实例,主要解决日志分析、定时任务、按日期批量处理数据等场景中生成并遍历指定起止日期的问题。资源为PDF格式,共1个文件,压缩包大小27KB,内容包含完整的脚本代码、运行命令示例以及实际输出结果,可直接参考改写。脚本通过date -d选项结合Unix时间戳进行日期比较,利用嵌套循环与边界检查实现从开始日期到结束日期的倒序输出,同时演示了如何调用外部Python程序传递日期参数,便于扩展为更复杂的日期处理流程。已有2025人学习下载,适合希望快速掌握Shell日期处理技巧的运维人员参考。

1. 接手“Shell脚本遍历一个日期范围”:先看场景再谈代码

写Shell脚本的同行,迟早会遇到一个看起来没有技术含量、实际很容易翻车的需求:给定一个起始日期和一个结束日期,要在这个区间内一天一天地执行某段逻辑。做日志清理、补数据任务、对账、按天跑批,全都要靠它。但真正动手写的时候你会发现,日期遍历本身不复杂,复杂的是日期格式、跨月跨年、环境差异这些边界情况全凑到一起。

这篇文章要讲的就是“Shell脚本遍历一个日期范围实例”这件事。读完你会知道,为什么网上一搜一大把的for循环写法,在换成真实场景之后经常跑不出期望结果;也会拿到几个可以直接改改参数就用的脚本,覆盖按天输出、数字串格式、到当前日期为止这些常见诉求。无论是刚上手Shell脚本的入门者,还是被线上定时任务折磨过的老手,这篇都值得花十分钟过一遍。

2. 为什么日期循环不能无脑用seq:date命令的三个约定

2.1 看似最直观的写法,坑在哪里

你搜索Shell脚本遍历日期范围,八成会看到一个经典答案:先把日期转成时间戳或数字串,然后用for循环加date -d往前推。没错,这个大方向是对的,但它建立在三个约定之上,任何一个约定没搞清楚,脚本就可能在某个周末的凌晨安静地跑出脏数据。

先说第一个约定:date -d这个参数在GNU coreutils里是支持的,date -d "2024-01-01"能正常解析日期。但macOS自带的BSD date不认-d,它用的是-j -f。这意味着你在Linux服务器上调试好的脚本,拷贝到macOS本地上就会直接报illegal option。不少团队的脚本正是在开发机上能跑、一上生产就翻车,多半不是业务逻辑错了,而是这个底层命令的差异。

第二个约定:日期字符串能不能被date正确解析,取决于系统locale和日期格式。2024-1-1和2024-01-01在不同环境里解析结果可能不一样,24-01-01这种省略世纪的写法更是高危。第三个约定是时间戳:date +%s能得到当前时间的Unix时间戳,它本质上是秒数,不是天数。很多人把时间戳直接装进for循环里,循环一次加86400秒,这又引出了夏令时和闰秒的问题。

2.2 三种主流实现方案及选型

实际工程里,遍历日期范围有三套常见做法,各有各的适用场景。你不需要全记住,但至少要知道区别,不然换一台机器就抓瞎。

方案一是“时间戳累加”。用date -d "$start_date" +%s拿到起始时间戳,循环里每次加86400,再用date -d "@$timestamp" +%Y-%m-%d转回日期串。优点是逻辑简单、任何日期都能算,缺点是每秒都走一次系统调用date,循环一个月就要调用两千多次,在循环体内还要做业务处理的场景下,性能会明显拖后腿。

方案二是“天数总数固定”。先用$(( (end_ts - start_ts) / 86400 ))算好总天数,再套一个从0到总天数的for循环,内部用date -d "$start_date + $i days"生成每一天。这个做法在GNU环境下是最清晰的,循环次数稳定,代码可读性也最好,适合做报表、遍历日志这种周期性任务。

方案三是底层的“纯算术推日期”。这种做法不依赖date命令,直接用数组和闰年算法去模拟日历。好处是快、完全不依赖系统的date实现;坏处是要自己维护平闰年表,代码不容易写对。一般只有嵌入式环境或极简系统里才值得折腾。我给普通团队的选型建议是:Linux生产环境用方案二,跨平台脚本用方案一,方案三少碰。

2.3 判断结束日期是否包含在内,先统一口径

写日期遍历脚本之前,必须和需求方确认一件事:结束日期要不要包含在循环里。这个看起来不是问题的问题,几乎每个踩坑笔记里都会出现。比如补数据任务说“从1月1日补到1月3日”,多数人的直觉是三天的数据,但按左闭右开区间写出来的代码只会跑1月1日和1月2日。

我一般会在脚本里用一个变量inclusive_end来控制,默认设置为1,表示包含结束日期。代码里统一用while [ "$current" -le "$end_date" ],不要用-lt,更不要在date -d "+1 day"的前后各做一次字符串比较、然后互相矛盾。循环条件里的比较运算符,决定了你的任务是少跑一天还是多跑一天,这个细节值得在写第一版代码时就钉死。

2.4 环境检查:写脚本前跑两条命令

在写任何遍历逻辑之前,先确认这台机器的date能力。下面的命令分别检查GNU date和BSD date:

date --version 2>/dev/null | head -1 date -j -f "%Y-%m-%d" "2024-01-01" "+%s" 2>/dev/null

第一条在GNU系统的输出里会带有coreutils字样;第二条是BSD date设置日期的写法,能在macOS上返回时间戳。哪条命令能正常回显结果,就用哪套方案。如果两条都不行,说明这台机器可能连date命令都被裁剪过,这时候应该改用纯算术方案。

检查完机制,再来定日期格式。建议整个脚本不论输入什么格式,内部一律先标准化成%Y-%m-%d,也就是四位年份、两位月份、两位日期,各自用-连接。这样后面所有字符串比较、字典序排序都是可靠的,不会出现"2024-2-1"排在"2024-10-1"后面的尴尬。

3. 遍历一个日期范围实例:从最小命令到真实场景

3.1 最小实现:五天连续日期输出

先把最常用的方案端上来。下面这段脚本实现了从指定起始日期开始,连续输出指定天数:

#!/bin/bash # 功能:从起始日期开始连续输出N天日期 # 依赖:GNU date(Linux默认可用) start_date="2024-01-01" days=5 for ((i=0; i<days; i++)); do current_date=$(date -d "$start_date + $i days" "+%Y-%m-%d") echo "$current_date" done

这段代码的核心是date -d "$start_date + $i days",GNU date支持在日期字符串后面直接做日偏移运算。循环变量i从0开始,到days-1为止,所以输出的是从1月1日到1月5日共五天。

这里有一个新手常踩的点:+ $i days里的加号两侧必须带空格,写成+"$i days"虽然也能解析,但date字符串解析的规则在不同版本里兼容性不好,统一用空格分隔最稳。另外+%Y-%m-%d的+号是格式串开始的标志,注意和前面日偏移的+区分开,它们作用在不同位置。

3.2 在循环体里做事的真实写法

实际使用中,没有人真的只为了打印日期。真实场景通常是:用日期拼出文件路径,然后去读昨天的数据、清洗日志或者调接口。下面的例子演示了把日期字符串拼接到文件操作逻辑里的结构:

#!/bin/bash # 场景:遍历2024年1月的每一天,统计对应日期的日志大小 log_root="/data/logs/nginx" pattern="2024-01-%02d" for day in $(seq -w 1 31); do log_date="2024-01-${day}" log_file="${log_root}/access_${log_date}.log" if [ -f "$log_file" ]; then size=$(stat -c%s "$log_file") echo "${log_date} ${size}" else echo "${log_date} missing" >&2 fi done

这个例子用了seq -w来生成补零的01到31,拼出日期字符串。重点不在于循环写法多高明,而是提醒一个习惯:在遍历日期时,把“日期的生成”和“日期的消费”分开。循环里只负责产出log_date,后续的文件检查、告警输出都在同一段逻辑里按需扩展。

stat -c%s是Linux下取文件字节数的写法,macOS上对应的是stat -f%z,这也是跨平台脚本要注意的小岔路。如果文件存在但大小为0,上面会正常输出0字节,如果想知道哪些日子压根没有日志,missing分支已经写好了。

3.3 跨年跨月:让循环自己解决

按月份写死的循环只能应付固定场景,真实任务经常是“从2024-12-28跑到2025-01-03”,跨了一个年。这时候循环条件不能用月份判断,要直接用日期比较:

#!/bin/bash # 功能:遍历从start到end的每一个自然日,包含end # 用法:修改变量后直接执行 start_date="2024-12-28" end_date="2025-01-03" current="$start_date" while [ "$current" \<= "$end_date" ]; do echo "processing: $current" # 在这里放你的业务逻辑,比如调用接口或清理文件 current=$(date -d "$current + 1 day" "+%Y-%m-%d") done

while循环的退出条件是字符串比较,因为日期串是%Y-%m-%d格式,字典序正好就是时间序。这里用\<=转义是因为在[ ]表达式里,<和>会被Shell当作重定向运算符处理,必须写成\<和\>。

循环体末尾用date -d "$current + 1 day"把当前日期推后一天。这行命令是整个循环的引擎,它保证了无论当前是12月31日还是2月28日,天数进位都交给date命令去算,不需要自己在脚本里判断大月小月、平年闰年。跨年场景下,这个写法几乎是零心智负担的。

3.4 日期转数字串的场景:报表文件命名

日志表、报表文件、数据库分区,经常用八位数字串做命名,比如20240101。Shell脚本获取当前日期时间并转换为数字串,是这类场景的常规操作。把上面的输出格式稍微改一下,就能得到数字串:

#!/bin/bash # 功能:遍历日期范围,并输出YYYYMMDD格式的数字串 start_date="2024-12-28" end_date="2025-01-03" current="$start_date" while [ "$current" \<= "$end_date" ]; do date_num=$(date -d "$current" "+%Y%m%d") echo "${date_num}" current=$(date -d "$current + 1 day" "+%Y-%m-%d") done

+%Y%m%d就是得到数字串的关键,它去掉中间的横杠直接拼接。这里值得多说一句:不要试图在current变量里直接存数字串,比如从20241228用date -d加一天。date -d对纯数字串的解析规则和带横杠的格式不一样,某些版本会把20241228当成时间戳来处理,输出结果完全不是预期的。

安全的做法是永远用标准带横杠的%Y-%m-%d格式做循环变量,只在输出或传给业务函数时转换成%Y%m%d。这样你的循环逻辑只有一套,不会因为格式不同冒出两套分支。

3.5 遍历到“当前日期”为止:动态结束条件

增量同步任务的结束日期通常是“昨天”或者“今天”,不可能写死在脚本里。这时结束日期要用date命令动态生成:

#!/bin/bash # 功能:从指定日期开始遍历到今天为止 # 注意:结束时用昨天(%Y-%m-%d),避免今天的数据还没落地 start_date="2024-12-01" end_date=$(date -d "yesterday" "+%Y-%m-%d") # 如果想要包含今天,把上面改成:end_date=$(date "+%Y-%m-%d") current="$start_date" while [ "$current" \<= "$end_date" ]; do echo "sync: $current" # 此处放同步逻辑 current=$(date -d "$current + 1 day" "+%Y-%m-%d") done

日志同步任务经常用yesterday而不是today,是因为当天最后一个小时的日志可能还没写完,直接去读会把不完整的数据当成最终结果。这是一个业务语义问题,不是技术问题,但技术实现上要支持这种选择。

date -d "yesterday"是GNU date的简便写法,等同于date -d "1 day ago"。两者解析结果是等价的,代码里保留一种就好。如果你的脚本运行在凌晨三四点,直接使用today也没有问题,因为此时“今天”还没有产生新的业务数据,用today作为截止日期反而会导致每次运行的范围不断扩大。

4. 遍历日期脚本的常见问题排查:5个让脚本崩掉的细节

4.1 date命令版本不一致,导致线上全线报错

现象:脚本在开发机上跑得好好的,部署到生产服务器后直接输出date: invalid date,crontab日志里刷屏。

原因:开发环境是macOS或BSD系统,生产是Linux且date版本较老;也可能反过来,生产机的coreutils版本太新,某些写法已经废弃。date -d不是POSIX标准,BSD的date -j -f又是另一套逻辑,两种环境无法互相兼容。

解决:在脚本开头做一个探测分支,检测到BSD环境就走-j -f分支,否则走-d分支。更省事的做法是锁定运行环境,统一在Linux容器里跑这类定时任务,并声明脚本只支持GNU date。团队协作时,把这个声明写进脚本注释第一行,能省掉后面接手的人一整天的排查时间。

4.2 时区设置错位,日期多跑或少跑一天

现象:按天遍历的输出结果里,某一天被重复处理或者被跳过,但脚本本身没有报错。

原因:服务器的时区不是北京时间。比如时区设为UTC时,date -d "2024-01-01"解析出来的时间戳仍然对应2024年1月1日,但date命令在格式转换过程中如果涉及UTC偏移,输出可能变成2023-12-31。这类问题在美国VPS、海外云主机上尤其常见。

解决:在脚本开头执行export TZ='Asia/Shanghai',统一锁定时区再执行遍历逻辑。注意TZ环境变量会影响date命令的输出,但不影响date -d对输入日期的解析。写完脚本后,用date -d "2024-01-01" "+%Y-%m-%d %H:%M:%S %z"检查一次输出,确认时区偏移正常再跑批量任务。

4.3 循环条件大小比较被Shell吃掉

现象:while循环一次都不执行,脚本安静退出,没有任何报错日志。

原因:这是Shell语法细节里最常见的陷阱。while [ "$current" <= "$end_date" ]写成了没有转义的<=,Shell会把<当作输入重定向符号去解析,等到运行时不是报错,就是生成一个以=开头的文件,行为非常诡异。新手往往先是怀疑日期格式,最后才发现是符号转义的问题。

解决:统一写成while [ "$current" \<= "$end_date" ],或者改用while [[ "$current" <= "$end_date" ]]。[[ ]]内置表达式的行为与[ ]不同,支持<而不需要转义,这是bash的特性。脚本声明用#!/bin/bash的话,建议直接用[[ ]],写起来也更现代。

4.4 循环体内多次调用date,性能扛不住

现象:遍历365天的任务跑了十几分钟。如果循环体里还有数据库查询或文件解压,整个任务直接拖垮定时调度。

原因:每调一次date都会fork一个子进程,Linux上fork的开销在毫秒级别,但365次循环转换两次日期就是700多次子进程,累积起来非常可观。这还不算date -d解析字符串本身的耗时。

解决:先用一次date算出起始时间戳和结束时间戳,然后循环里变i为天数,只在真正需要展示日期时做一次转换。如果连一次转换都不想多做,可以改用date -d "@$ts"直接批量输出。另一个工程师常用的取巧方式是用seq -f "%02g"生成所有天数,把date的调用次数从天数 * 转换次数降为天数。

4.5 日期字符串尾部有空格或不可见字符

现象:日志里打印出来的日期看起来完全正确,但[ -f "$log_file" ]就是判断不到文件,或者接口请求报参数非法。

原因:从配置文件、Excel复制过来的日期字符串经常带着不可见的空白或\r回车符。Windows上编辑过的脚本尤其容易混入\r,导致变量值实际是2024-01-01\r,所有字符串比较都失败。

解决:在脚本入口统一清洗一次。

start_date=$(echo "$start_date" | tr -d '\r' | xargs) end_date=$(echo "$end_date" | tr -d '\r' | xargs)

tr -d '\r'删除回车符,xargs顺便把首尾空格去掉。这两步做完,再往循环里传。如果你在管道里拿到日期,先把它写进日志看一眼,用echo "$start_date" | od -c查看尾部字符,通常能立刻发现问题根源。

5. 封装成可复用函数:校验天数、格式化输出与日志排查习惯

经过前面几轮踩坑,我最终会把日期遍历封装成一个固定函数放到公共脚本库里边。这样一个函数可以同时做到参数校验、格式统一、输出可控,而整个脚本的任务逻辑变得很短。

#!/bin/bash # 功能:遍历日期范围,每个日期执行一次回调逻辑 # 参数:$1 起始日期 $2 结束日期 $3 是否包含结束日期(1/0) # 用法:walk_dates "2024-01-01" "2024-01-05" "1" walk_dates() { local start_date="$1" local end_date="$2" local inclusive="${3:-1}" local current="$start_date" local end_adjusted="$end_date" # 参数校验:非法输入直接退出,不给后续留隐患 if ! date -d "$start_date" >/dev/null 2>&1; then echo "ERROR: invalid start date: $start_date" >&2 return 2 fi if [ "$inclusive" -eq 0 ]; then end_adjusted=$(date -d "$end_date - 1 day" "+%Y-%m-%d") fi while [ "$current" \<= "$end_adjusted" ]; do # 调用外部传入的处理逻辑 process_day "$current" current=$(date -d "$current + 1 day" "+%Y-%m-%d") done } process_day() { local day="$1" echo "process: $day" # 实际任务逻辑写这里,例如按天拉取数据、归档日志 }

函数里有三个关键的工程习惯。第一,非法日期在入口直接拦截,用date -d "$start_date"做解析能力验证,不会把坏值带入循环造成半程报错,也更方便外部调用者感知参数错误。第二,结束日期的包含关系由参数控制,缺省值设为包含,保持最安全的口径。第三,业务逻辑通过process_day隔离在外部,函数本身只负责日期推进,这样换业务需求时不用改写遍历主体。

验证这个函数是否正确,最直接的办法是比较循环天数。假如要验证2024年1月1日到2024年1月31日的遍历,可以先跑脚本输出每一天,然后用wc -l统计行数。31天就代表包含结束日期,30天就代表排除掉了结束日。如果统计出来是32天或者29天,说明循环条件或日期推进写错了。

walk_dates "2024-01-01" "2024-01-31" "1" | wc -l # 期望输出:31

我建议把这样的函数放在团队的公共脚本库里,并在函数注释里标注“当前仅支持GNU date环境”。这会是新同学接手日期脚本时最省心的一个入口。还有一个小习惯:任务脚本里所有遍历日志统一用日期 动作 结果三列格式,比如2024-01-01 pull_ok或2024-01-02 pull_empty。日子久了,用grep在Shell脚本里过滤这些日志会非常顺手,grep "pull_empty" run.log | wc -l一眼就能看出哪些天出过问题。

另外,如果你的定时任务需要在后台执行,比如nohup ./walk_dates.sh > run.log 2>&1 &,记得在脚本开头额外加一句exec > >(tee -a run.log)或者确保重定向覆盖了stdout和stderr两者。只重定向stdout不重定向stderr的后果是,等到排查问题时,日志里只有正常输出,错误信息全丢在了终端上。这样一个函数加一个日志习惯,足够让日期遍历脚本的维护成本降到最低,希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询