ARTICLE DETAIL

资讯详情

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

Shell脚本遍历日期范围:避坑指南与可复用实例

Shell脚本遍历日期范围:避坑指南与可复用实例 简介一份面向Linux/Unix系统管理员与自动化脚本初学者的Shell脚本遍历日期范围实例主要解决日志分析、定时任务、按日期批量处理数据等场景中生成并遍历指定起止日期的问题。资源为PDF格式共1个文件压缩包大小27KB内容包含完整的脚本代码、运行命令示例以及实际输出结果可直接参考改写。脚本通过date -d选项结合Unix时间戳进行日期比较利用嵌套循环与边界检查实现从开始日期到结束日期的倒序输出同时演示了如何调用外部Python程序传递日期参数便于扩展为更复杂的日期处理流程。已有2025人学习下载适合希望快速掌握Shell日期处理技巧的运维人员参考。1. 接手“Shell脚本遍历一个日期范围”先看场景再谈代码写Shell脚本的同行迟早会遇到一个看起来没有技术含量、实际很容易翻车的需求给定一个起始日期和一个结束日期要在这个区间内一天一天地执行某段逻辑。做日志清理、补数据任务、对账、按天跑批全都要靠它。但真正动手写的时候你会发现日期遍历本身不复杂复杂的是日期格式、跨月跨年、环境差异这些边界情况全凑到一起。这篇文章要讲的就是“Shell脚本遍历一个日期范围实例”这件事。读完你会知道为什么网上一搜一大把的for循环写法在换成真实场景之后经常跑不出期望结果也会拿到几个可以直接改改参数就用的脚本覆盖按天输出、数字串格式、到当前日期为止这些常见诉求。无论是刚上手Shell脚本的入门者还是被线上定时任务折磨过的老手这篇都值得花十分钟过一遍。2. 为什么日期循环不能无脑用seqdate命令的三个约定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 datedate --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 dateLinux默认可用 start_date2024-01-01 days5 for ((i0; idays; i)); do current_date$(date -d $start_date $i days %Y-%m-%d) echo $current_date done这段代码的核心是date -d $start_date $i daysGNU 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 pattern2024-01-%02d for day in $(seq -w 1 31); do log_date2024-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_date2024-12-28 end_date2025-01-03 current$start_date while [ $current \ $end_date ]; do echo processing: $current # 在这里放你的业务逻辑比如调用接口或清理文件 current$(date -d $current 1 day %Y-%m-%d) donewhile循环的退出条件是字符串比较因为日期串是%Y-%m-%d格式字典序正好就是时间序。这里用\转义是因为在[ ]表达式里和会被Shell当作重定向运算符处理必须写成\和\。循环体末尾用date -d $current 1 day把当前日期推后一天。这行命令是整个循环的引擎它保证了无论当前是12月31日还是2月28日天数进位都交给date命令去算不需要自己在脚本里判断大月小月、平年闰年。跨年场景下这个写法几乎是零心智负担的。3.4 日期转数字串的场景报表文件命名日志表、报表文件、数据库分区经常用八位数字串做命名比如20240101。Shell脚本获取当前日期时间并转换为数字串是这类场景的常规操作。把上面的输出格式稍微改一下就能得到数字串#!/bin/bash # 功能遍历日期范围并输出YYYYMMDD格式的数字串 start_date2024-12-28 end_date2025-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_date2024-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 datecrontab日志里刷屏。原因开发环境是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 TZAsia/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 21; 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 21 记得在脚本开头额外加一句exec (tee -a run.log)或者确保重定向覆盖了stdout和stderr两者。只重定向stdout不重定向stderr的后果是等到排查问题时日志里只有正常输出错误信息全丢在了终端上。这样一个函数加一个日志习惯足够让日期遍历脚本的维护成本降到最低希望帮到你。本文还有配套的精品资源点击获取
返回列表