ARTICLE DETAIL

资讯详情

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

Shell脚本遍历日期范围:三种写法与避坑要点

Shell脚本遍历日期范围:三种写法与避坑要点 简介在Linux/Unix系统管理中按日期范围遍历是Shell脚本自动化处理的常见需求。一份PDF文档完整呈现了一个实现实例面向系统管理员、运维工程师及脚本入门者解决两个指定日期之间逐日生成日期序列、驱动后续任务的问题。资源包仅含1个PDF文件大小约27KB内容紧凑包含可参考的脚本源码与逐段逻辑说明。实例重点演示了利用date命令将起止日期转为Unix时间戳通过嵌套循环逐日递减输出日期并在过程中调用Python脚本传递当前日期作为参数读者可据此理解日期比较与循环控制的核心写法也可调整循环上限和日期格式应用到日志归档、报告生成、定时备份等场景。目前已有2026人学习下载适合需要快速掌握Shell日期处理技巧的初学者和中级用户。1. 为什么“遍历日期范围”最难的不是遍历是日期解析日志按天切、报表按天出、数据按天补这类批处理脚本里最常遇到的需求就是给定一个起始日期和一个结束日期把中间每一天都拿出来跑一遍。Shell脚本遍历一个日期范围听起来只是循环加变量但真正动手时会发现循环是壳日期解析才是坑。同样是“加一天”Linux和macOS的写法不一样同一个时间戳在容器里换算出来的日期可能比宿主机少一天。这篇文章以这个实例为主线从最小可用写法讲到参数配置和常见踩坑点适合刚接触shell脚本入门、准备用系统自带命令做批处理的运维或后端同学也适合已经写过类似脚本、想确认边界情况的人。2. 用shell脚本for循环写出最小可用版本偏移量与步长固定写法shell脚本基础知识里for循环通常用来遍历文件列表或参数列表很少专门讲日期范围。但实际批处理里日期范围往往才是最外层的循环内层才是具体任务。先给一个可以直接抄的for版本环境要求是Linux下的GNU date步长固定为1天起止日期写死在变量里。这个版本不追求花哨先把“每一天都处理到”这件事做对。2.1 先算总天数再循环for版本的最小骨架#!/bin/bash # 遍历 2024-01-01 到 2024-12-31步长 1 天 start2024-01-01 end2024-12-31 # 先把起止日期换算成时间戳算出总天数 start_ts$(date -d $start %s) end_ts$(date -d $end %s) total_days$(( (end_ts - start_ts) / 86400 )) # 从 0 开始加偏移保证起始日被包含itotal_days 保证结束日也被包含 for (( i0; itotal_days; i )); do day$(date -d $start ${i} days %F) echo $day done这个骨架的关键在于先算总天数再进循环循环里只做一次日期偏移计算边界条件用itotal_days而不是itotal_days否则最后一天会被漏掉。两个细节值得注意date -d $start ${i} days是GNU date的偏移语法%F和%Y-%m-%d等价都输出固定宽度的日期字符串。如果你能确定脚本只在Linux服务器上跑直接保存执行就可以。这个版本还有一个隐含好处即使total_days是负数比如结束日期早于起始日期循环不会执行脚本不会报错只是静默结束。不过这种输入错误最好显式拦住第4章会写校验做法。2.2 用seq生成偏移序列打印日期列表的另一种写法#!/bin/bash start2024-01-01 days365 # 生成 0 到 days-1 的偏移序列 for i in $(seq 0 $((days - 1))); do day$(date -d $start ${i} days %F) echo $day doneseq 0 364会输出0到364这365个数字每个数字占一行。把它放进$( )后换行会被shell切分成空格for循环拿到的就是一组参数。这里的days需要提前算好实际使用中可以先通过第2.1节的时间戳差值算出总天数再交给这个版本。它比C风格for循环更直观适合快速验证“这个日期范围到底有哪些天”。但要注意$(seq 0 9999)这种写法会把全部输出一次性读进内存再按空白分词。范围只有几百天时无所谓如果跨度上万天内存和分词开销都会明显增大脚本启动时会卡一下。遇到那种需求直接跳到第3章的时间戳整数循环更合适。2.3 这个写法的常见误用与适用边界有一种看起来省事的写法是让seq直接生成带偏移量的日期字符串例如# 错seq -f 输出的每一行里带空格for 会按空格拆分 for day in $(seq -f $start %g days 0 10); do echo $day doneseq -f $start %g days输出的是2024-01-01 0 days这类带空格的字符串命令替换后会被shell拆成2024-01-01、0、days三块循环体拿到的根本不是完整日期调试时非常容易迷惑。我一般不会让seq直接拼带空格的参数宁可多写一行day$(date ...)把“偏移量计算”和“日期格式化”分开出问题时也好定位。for版本的适用边界也很明确适合跨度在几百天到两三年以内、步长固定为1天、起止日期基本不变的情况。如果步长要变成7天偏移表达式要改成${i} weeks如果步长要变成1个月就不能简单改成天数月份有28到31天固定天数会越跑越偏。更麻烦的是for方案每轮循环都要启动一次date进程365天就是365次外部命令调用性能虽然谈不上灾难但确实不优雅。动态起止日期和更灵活的步长控制需要交给while循环。3. while循环与时间戳方案处理动态起止日期的两种通用写法for方案适合“范围写死在脚本里”的场景。真实业务里起止日期往往来自命令行参数、上一天备份列表、配置文件里的状态值。这时候我更推荐while循环或者时间戳整数循环。它俩不需要预先算总天数起止日期传进来就能跑也更容易支持“步长不是1天”的需求。3.1 用字符串比较控制边界的while循环不用预先算天数差#!/bin/bash start2024-01-01 end2024-12-31 cur$start # 固定长度的 YYYY-MM-DD 可以直接按字典序比较 while [[ $cur $end || $cur $end ]]; do echo $cur cur$(date -d $cur 1 day %F) done这段脚本的核心是[[ $cur $end || $cur $end ]]。为什么敢用比较日期因为YYYY-MM-DD是从年到月到日的固定宽度格式字典序完全等于时间序。比如2024-02-29会排在2024-03-01前面2024-09-30也会排在2024-10-01前面因为月份位先比较09小于10。这里必须用[[ ]]而不是[ ]因为双中括号里的是字符串比较单中括号里的会被当成shell重定向得写成\或\才安全。需要特别警惕的写法是while [ $cur ! $end ]这类“不等于”判断。当步长不是1天时cur可能直接跳过end比如起始日是2024-01-01、结束日是2024-01-02、步长是7天第一轮结束后cur变成2024-01-08永远等不到2024-01-02循环会一路跑到2025年。正确做法就是上面这种“小于或等于”的字符串判断。这个边界判断是整个日期遍历脚本里最容易被改坏的一行后面避坑章节还会再强调。3.2 时间戳整数循环把日期比较变成数字比较#!/bin/bash start2024-01-01 end2024-12-31 start_ts$(date -d $start %s) end_ts$(date -d $end %s) # ts 从起始时间戳开始每次加 86400 秒直到超过结束时间戳 for (( tsstart_ts; tsend_ts; ts86400 )); do day$(date -d $ts %F) echo $day done这段循环里没有字符串日期比较只有整数比较是三种写法里性能最好的。$ts告诉date从Unix时间戳解析%F只截取日期部分。用86400秒当作一天的步长在UTC和不实行夏令时的时区没有问题比如中国时区。但有一个容易翻车的细节date -d $ts输出的是当前环境时区下的日期。注意如果脚本跑在UTC时区的Docker容器里而宿主机是东八区时间戳换算出来的日期会整体慢8小时常见表现是“最后一天的日志没出来”。解决办法很简单在脚本开头加一行export TZAsia/Shanghai确保所有date调用都在同一个时区下解析。这个细节在本地脚本里很难暴露一上容器就现原形第5章会展开讲。3.3 封装成date_range函数把边界判断收进一个函数#!/bin/bash date_range() { local start$1 local end$2 local cur$start while [[ $cur $end || $cur $end ]]; do echo $cur cur$(date -d $cur 1 day %F) done } # 方式一直接遍历输出 for day in $(date_range 2024-01-01 2024-01-05); do echo 处理 $day done # 方式二用进程替换读取循环内变量不会丢 count0 while read -r day; do echo 处理 $day count$((count 1)) done (date_range 2024-01-01 2024-01-05) echo 共处理 $count 天把日期边界判断封装成date_range函数好处是调用方不用关心到底是字符串比较还是时间戳比较只要传入两个YYYY-MM-DD格式的日期就能拿到完整日期流。方式一用$( )读取时要确保启始日期到结束日期之间没有空格这个函数输出的是纯日期所以安全。方式二用进程替换 (...)而不是管道... | while read原因很隐蔽管道会让while循环跑在子shell里循环里累加的count变量在循环结束后会消失进程替换能让while留在当前shell里。这个细节在调试时最容易让人怀疑人生。3.4 三种方案放在一起怎么选方案循环条件适用场景注意点for 偏移天数先算天数再itotal_days范围固定、步长1天每次循环都要fork datewhile 字符串比较cur end或cur end起止日期动态传入日期格式必须统一为YYYY-MM-DD时间戳 86400秒整数比较tsend跨度大、要求性能注意时区一致性选择标准可以归结为三条日期范围是否事先知道、步长是否固定为1天、脚本是否要跨环境运行。事先知道范围选for起止日期是算出来的选while字符串比较跨度超过一年且循环体内没有更重任务时选时间戳方案。实际项目里我默认选while字符串比较版本因为绝大多数批处理任务一天只跑一轮date进程的开销完全可以忽略而字符串判断的可读性最好。4. 把日期范围做成脚本参数默认值、校验与按周按月步长脚本一旦要给别人用甚至给一个月后的自己用就不该把起止日期直接写死在变量里。常见做法是把日期范围做成前两个位置参数第三个参数控制步长模式。配合默认值和合法性校验能拦掉大部分因为参数传错导致的返工。4.1 起止日期参数的默认值与合法性校验#!/bin/bash # 用法: ./run_dates.sh [起始日期] [结束日期] start_date${1:-$(date %F)} end_date${2:-$(date %F)} echo 处理范围: $start_date 到 $end_date${1:-$(date %F)}的意思是如果第一个参数没传或为空就取当天日期。这里用date %F而不是date是为了保证默认值也是YYYY-MM-DD格式。日期变量从头到尾保持同一种格式是这条链路里最重要的习惯后面避坑章节会解释为什么带时间的默认输出会拆散循环参数。参数校验建议放在循环之前for d in $start_date $end_date; do if ! date -d $d %F /dev/null 21; then echo 无法解析的日期: $d 2 exit 1 fi done if [[ $start_date $end_date ]]; then echo 起始日期晚于结束日期请检查参数 2 exit 1 fidate -d $d %F /dev/null 21只保留命令的成败状态不输出内容。这一步能拦住2024-13-45这种错误输入也能拦住拼写造成的错位。起止日期大小比较用[[ ]]里的它同样是字典序比较不需要转义。校验写完后面的循环才敢放心跑。4.2 步长怎么设按天、按周、按月三种模式按天和按周可以共用一套逻辑因为周本质上是天的倍数step${3:-1} cur$start_date while [[ $cur $end_date || $cur $end_date ]]; do echo $cur cur$(date -d $cur ${step} days %F) donestep1按天step7按周。这里${step} days由GNU date解析所以step3就是每3天跑一次。注意不要自己换算成$((step * 86400))秒再传给date秒数累加遇到月末和跨年没有语义问题但一旦脚本要兼容夏令时时区就会出错。按月就不能用固定天数了因为2月可能是28天也可能29天4月6月9月11月是30天其余月份是31天。mode${3:-day} # day|week|month case $mode in day) step_expr${4:-1} days ;; week) step_expr$(( ${4:-1} * 7 )) days ;; month) step_expr${4:-1} month ;; *) echo mode 只支持 day|week|month 2 exit 1 ;; esac cur$start_date while [[ $cur $end_date || $cur $end_date ]]; do echo $cur cur$(date -d $cur $step_expr %F) done这样调用方可以写./run_dates.sh 2024-01-01 2024-12-31 month 1表示从1月到12月每月跑一次。1 month交给date自己处理跨年和大小月比脚本里手写日历逻辑可靠得多。但要提醒一句1 month按自然月累加如果起始日是1月31日加1个月并不会自动收敛到每月最后一天而是会滚动到3月初第5章的避坑记录里专门有这条。4.3 循环体里的日志与失败控制失败即停还是跳过继续processed0 failed0 cur$start_date while [[ $cur $end_date || $cur $end_date ]]; do echo [$(date %F %T)] 开始处理 $cur if run_task $cur; then processed$((processed 1)) echo [$(date %F %T)] 成功 $cur else failed$((failed 1)) echo [$(date %F %T)] 失败 $cur 2 exit 1 fi cur$(date -d $cur 1 day %F) done echo 完成: 成功 $processed 个日期我习惯在循环体里加两个东西日志行带$(date %F %T)失败时exit 1而不是安安静静跳过。原因很现实补数任务如果某一天失败而脚本继续跑最终输出表看起来是满的但里面缺了一天这种错误很难从结果里发现。失败即停虽然粗暴但能让问题当天暴露。如果业务接受“失败跳过”把exit 1改成continue并保留failed计数跑完后再人工处理失败日期。5. 常见问题排查日期遍历最容易翻车的5个现场日期遍历的代码本身写起来都差不多真正让人花时间的全是运行环境差异和边界情况。下面5条按“现象 → 原因 → 解决”的顺序记录基本覆盖了我做批处理脚本时遇到过的几类硬伤。5.1 现象date命令报invalid date脚本第一步就退出同样一段date -d 2024-01-01 1 day在Linux服务器上正常输出在macOS上却报date: illegal time format或者invalid date。原因是GNU date的-d参数支持灵活的日期表达式而macOS自带的BSD date用的是完全不同的参数体系根本不认-d。如果团队里有人用macOS开发、脚本部署到Linux服务器这个差异会反复制造麻烦。解决方式是在脚本里做个探测然后分别提供日期函数if date -d 2024-01-01 %F /dev/null 21; then next_day() { date -d $1 1 day %F; } else next_day() { date -j -v1d -f %F $1 %F; } fi cur2024-01-01 echo $(next_day $cur)BSD date的-j表示不设系统时间-v1d表示加1天-f %F $1表示按固定格式解析输入。把平台差异封装成一个函数后循环体里只认next_day不用到处判断环境。生产环境更省事的方案是直接在服务器或容器里跑但脚本本身保留这个探测逻辑能让本地调试顺畅很多。5.2 现象循环停不下来日志日期跑到明年脚本早上9点启动第二天看日志时发现已经处理到明年1月明显是死循环。问题几乎都出在边界条件上while [ $cur ! $end ]这类“不等于”判断。步长为1天时偶尔能正常结束一旦步长变成2天或7天cur可能直接跨过end。比如结束日期是2024-01-31cur从2024-01-30加7天变成2024-02-06永远不等于2024-01-31于是循环一路跑下去。解决边界判断不要用“不等于”要用“小于或等于”。格式统一为YYYY-MM-DD时直接用字符串比较# 小于或等于的字符串写法 while [[ $cur $end || $cur $end ]]; do如果担心字符串比较在不同语言环境下有差异就退回时间戳整数比较while (( $(date -d $cur %s) $(date -d $end %s) )); do后者每次循环会启动两次date进程性能差一些但绝对可靠。我的习惯是格式统一时选字符串比较凡是步长可能大于1天的脚本一律在边界条件里用“小于等于”兜底。5.3 现象容器里生成的日期序列少一天同一个脚本宿主机跑得好好的放到Docker容器里最后一天的日志一直没出来或者第一天的日期对应的是前一天。问题出在时区。宿主机是Asia/Shanghai很多基础镜像默认是UTCdate -d $ts把时间戳换算成日期时按容器时区算东八区的凌晨0点到8点之间会被UTC换算成前一天日期序列自然偏移。解决脚本顶部显式设置时区并且放在所有date调用之前export TZAsia/Shanghaiexport TZ对GNU date和BSD date都生效它会改变时间戳的换算基准。这个坑也常见于cron环境某些基础镜像时区是UTC而任务期望用本地日期划分日志一漏掉这句话整个批处理的日期就会整体错位。5.4 现象按月遍历时冒出3月2日这种日期需求是“每个月底跑一次”起始日给的是2024-01-31代码里直接写date -d $cur 1 month %F。结果第二个月的日期变成了2024-03-02而不是2024-02-29。这是GNU date对“加一个月”的实现逻辑先把月份加1再保留原来的日如果目标月份没有这一天就顺延到下个月。2024年1月31日加1个月2月只有29天多出的2天顺延到3月于是得到3月2日。解决如果业务要的是“每月最后一天”不要从月末日期开始累加而是先定位到月初再用“下月1日减1天”的方式求月末month_end() { local ym$1 # 参数必须是 YYYY-MM 格式 local first_day${ym}-01 local next_month_first$(date -d $first_day 1 month %F) date -d $next_month_first -1 day %F } echo $(month_end 2024-01) # 输出 2024-01-31 echo $(month_end 2024-02) # 输出 2024-02-29这个函数先把输入规整到当月1日加1个月变成下月1日再减1天回到当月最后一天。1 month用在月初永远不会触发月末溢出得到的日期一定落在目标月份内。注意调用时月份必须是合法的YYYY-MM否则第一步就错了。5.5 现象日期变量传进函数后变成两个参数日志里出现“无法识别日期: 2024-01-01”这种奇怪报错仔细看其实后面还跟了一个时间字符串。原因是脚本里某个日期变量是用$(date)赋的值输出类似2024-01-01 12:30:00带空格。后面调用函数时没有加双引号比如process_day $dayshell会按空格把参数拆成两个函数只拿到前半段或者直接报错。解决脚本内部所有日期变量统一用date %F或%Y-%m-%d存储不要用date的默认完整输出。调用函数时养成加双引号的好习惯process_day $day如果必须读取一个带时间的文件用while read -r整行读取再抽取日期部分while read -r line; do day$(date -d $line %F) process_day $day done input.txt这里的-r防止反斜杠转义date -d $line负责把任意可解析的时间格式统一成YYYY-MM-DD。这个问题的本质是shell按空白分词和5.2里的死循环一样都属于“语法正确但语义错误”的典型范例。6. 验证日期范围的三个技巧先空转、再自检、后对账新脚本第一次跑谁都不敢直接让它动业务。我习惯在脚本里放一个DRY_RUN空转开关默认只打印日期不执行真实任务DRY_RUN${DRY_RUN:-1} # 设为1时只打印日期不执行真实任务 if [[ $DRY_RUN 1 ]]; then echo $cur else run_task $cur fi跑空转的方式也简单DRY_RUN1 ./run_dates.sh 2024-01-01 2024-12-31 date_list.txt然后打开文件看首尾两天、看行数、看中间有没有3月2日这种异常日期。确认无误后再把DRY_RUN设成0跑真实任务。这个开关成本极低但能给补数任务上一道保险。日期解析的“宽容性”会掩盖输入错误。比如2024-02-30会被GNU date解析成2024-03-01如果脚本不校验起点就悄悄变成了3月1日。所以在进入循环前我会做一次回读自检[[ $(date -d $start_date %F) $start_date ]] || { echo 起点自检失败: $start_date; exit 1; } [[ $(date -d $end_date %F) $end_date ]] || { echo 终点自检失败: $end_date; exit 1; }日期数量也可以对账。空转结果存到文件后计算期望行数expected$(( ( $(date -d $end_date %s) - $(date -d $start_date %s) ) / 86400 1 )) actual$(wc -l date_list.txt) if [[ $expected -eq $actual ]]; then echo 日期数量核对通过: $actual 天 else echo 日期数量不一致: 期望 $expected, 实际 $actual fi这三个操作加起来不过十几行但对补数、报表、日志回跑这类“跑完才知道结果对不对”的任务它能提前拦下最贵的返工。我自己的教训是凡是遍历日期范围的脚本DRY_RUN开关永远保留哪怕上线跑了一年也没摘掉因为哪天要回溯三个月的数据你总得先知道这个脚本到底会碰哪些天。先空转一遍再自检首尾最后对账数量这个流程走完再放心交给定时任务。希望帮到你。本文还有配套的精品资源点击获取
返回列表