
简介这份资源通过一个完整的 Shell 脚本实例演示如何在 Linux/Unix 环境中遍历指定的日期范围面向需要处理日期序列的运维人员、脚本开发者和自动化学习者。资源为单个 PDF 文档共 1 个文件体积约 27KB内容以脚本源码、执行流程说明和运行结果示例为主。已有 2026 人浏览学习。脚本以开始日期和结束日期作为命令行参数借助 date 命令的 -d 选项与 %F、%s 格式将日期转为时间戳比较通过循环逐日递减输出指定区间内每一天外层循环作为安全边界控制最大迭代次数示例以 2014-06-30 到 2014-06-01 为区间倒序输出 30 个日期并可将每个日期拼接至后续 Python 脚本进一步处理。对理解日期格式化、时间戳换算、循环控制与跨脚本调用都有参考价值读者可直接调整循环次数与日期格式移植到日志分析、定时任务或按日数据采集等场景。1. 遍历日期范围从手工改日期到循环逐日的Shell脚本写法Shell脚本遍历一个日期范围看起来是最不起眼的需求真到要做的时候才发现处处是坑。日志按天归档要遍历日期数据补数要遍历日期报表重新生成还是要遍历日期——这类任务没有复杂的算法难点全在日期推进、跨月跨年、格式兼容和并发控制上。这篇文章从一个能直接运行的实例讲起先拆三种常用的遍历写法再给出完整Shell脚本骨架最后把几个容易让你在深夜翻车的排查点列清楚。适合刚接触shell脚本的同学照着复现也适合写过几版日期脚本但总在边界处翻车的熟手对照自查。2. 三种日期遍历姿势GNU date循环、seq预生成与时间戳自增怎么选遍历日期范围第一件事是选姿势。我见过不少脚本写得又长又绕根源是遍历方式选错了。你可能会说用for循环加date命令不就能遍历吗真不是。GNU date、BSD date、带不带时区、需不需要预生成直接决定你写出的脚本能不能在别人机器上跑、能不能中断后续跑。这一章先把三种常见写法和各自适用面讲清楚第3章再落到完整实例。2.1 用GNU date逐日推进最直白也最依赖运行环境最常见的写法是维护一个“当前日期”变量每轮循环用date把它加一天。代码如下start2024-01-01 end2024-01-05 current$start while [[ $current $end || $current $end ]]; do printf %s\n $current current$(date -d $current 1 day %F) done这段代码的核心是date -d $current 1 day %Fdate自己处理大小月、闰月和跨年进位你不需要维护一张月份天数表。while的循环条件是字符串比较因为YYYY-MM-DD格式的字典序与日期序完全一致所以[[ $current $end || $current $end ]]可以安全地表达“小于等于结束日期”。如果我加上校验保证传入的开始日期不大于结束日期这里就不会出现死循环。参数说明%F等价于%Y-%m-%d输出固定长度2024-01-01不会变成2024-1-1date -d是GNU coreutils提供的自然语义解析。在CentOS、Ubuntu、Debian这些Linux发行版上没问题。注意这段代码在macOS自带的BSD date上会直接报illegal option -- d如果你的脚本要在Mac上跑先看2.3和5.1的兼容写法。2.2 用seq预生成日期序列先把列表备好再谈执行补数据场景和普通遍历不太一样我更倾向于先把日期列表完整生成出来人工检查一遍再挂业务任务。seq配合date偏移量就能做到for i in $(seq 0 4); do d$(date -d 2024-01-01 $i days %F) printf %s\n $d doneseq 0 4生成0到4的等差序列每轮偏移i天从2024-01-01一直输出到2024-01-05。这种写法的价值在于“生成列表”和“执行任务”分离。我先跑一遍生成列表重定向到文件检查文件里的日期数量对不对、头尾是否准确然后再拿这个列表去补数据。对补数、重算报表这类任务来说列表文件就是断点中途断了用grep找最后处理到哪一天比翻日志快得多。参数说明偏移量较大时date -d 2024-01-01 3650 days依然有效但循环里每轮都启动一次date子进程遍历十年跨度时耗时开始明显。seq方法适合生成一次性列表不适合在循环里反复执行重任务。这也是我第4章把“生成列表”和“消费列表”拆开的直接原因。2.3 用时间戳自增跨平台最稳但要小心时区如果运行环境不确定或者你想把日期推进的逻辑彻底掌握在自己手里时间戳自增是第三个可选方案start_ts$(date -u -d 2024-01-01 %s) end_ts$(date -u -d 2024-01-05 %s) for (( ts start_ts; ts end_ts; ts 86400 )); do date -u -d $ts %F done先把起止日期转成Unix时间戳每次加86400秒再格式化成日期。循环里不依赖date的自然日推进只要date能完成时间戳和日期的互转这段逻辑在任何系统上都能套用。这里的关键参数是-u它让所有转换都发生在UTC时区规避了本地时区在夏令时切换日把一天变成23小时或25小时的问题。如果你去掉-u一旦跨过夏令时切换点序列里会出现同一天重复或直接跳天这就是很多人说“日期遍历是玄学”的根源。这一版的问题在于起点和终点必须能被准确解析如果原始日期是20240101这类紧凑格式得先转成YYYY-MM-DD另外BSD date的时间戳转换语法完全不同需要走date -j -f具体写法在第5章给兼容函数。三种姿势各有利弊。2.1适合快速验证和短跨度任务2.2适合补数前先确认列表2.3适合跨平台或统一UTC跑批。落到完整实例时我更常用2.1的while版本因为它可读性好业务函数也好挂下一章直接从这个版本展开。3. 从最小实例到跨月边界完整Shell脚本怎么拆、参数怎么设选好遍历姿势后真正写成一个能交付的脚本还需要解决三件事参数怎么校验、跨月跨年怎么不翻车、业务函数怎么挂上去。这一章给一个可以直接保存运行的完整脚本再拆开讲边界处理和函数设计。3.1 一个可直接运行的for循环实例参数校验、逐日推进、任务函数分离先把完整骨架放出来文件保存为date_range_demo.sh运行方式在代码后面说明。这个脚本不需要root权限也不需要安装额外依赖只要运行环境是Linux且自带GNU date就能跑。#!/usr/bin/env bash set -euo pipefail # process_day真正的业务逻辑这里只做占位输出 process_day() { local day$1 printf [task] process %s\n $day } main() { if [[ $# -ne 2 ]]; then printf usage: %s start end\n $0 2 printf 示例: %s 2024-01-01 2024-01-31\n $0 2 exit 1 fi local start$1 end$2 if [[ -z $start || -z $end ]]; then printf 日期不能为空\n 2 exit 1 fi if [[ ! $start ~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ || ! $end ~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then printf 日期格式必须是 YYYY-MM-DD\n 2 exit 1 fi if [[ $start $end ]]; then printf 开始日期不能大于结束日期: %s %s\n $start $end 2 exit 1 fi local current$start while [[ $current $end || $current $end ]]; do process_day $current current$(date -d $current 1 day %F) done } main $运行方式bash date_range_demo.sh 2024-01-01 2024-01-03 # 输出 # [task] process 2024-01-01 # [task] process 2024-01-02 # [task] process 2024-01-03逻辑说明这里把“遍历”和“业务”分开遍历只负责把每一天交给process_day任务自身写在函数里后续改逻辑不需要动遍历骨架。set -euo pipefail是这套脚本的保险丝遇到未定义变量、管道错误、命令返回非零会立刻退出避免日期遍历跑到一半在错误数据上继续输出。参数说明$# -ne 2校验参数个数缺参数直接打印用法-z判断字符串为空-n判断非空这是shell脚本if判断的常见参数组合[[ ! $start ~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]用正则把日期格式收口拒绝2024-1-1和20240101这类松散格式。while循环条件用字符串比较而非数字比较这是整个方案能成立的前提。3.2 跨月和跨年为什么会翻车月份天数表不需要你维护最容易让新手栽跟头的就是跨月。常见错误写法是自己推算月份天数# 错误示范自己推算月份天数表 # if [[ $month 2 ]]; then days28; fi # 这样写遇到闰年必错一旦日期跨到2月立即面临闰年问题手动维护天数表等于把风险引进来。正确做法是让date的语义解析处理进位推进动作始终保持一行current$(date -d $current 1 day %F)验证一下三条边界date -d 2024-02-28 1 day %F # 2024-02-29闰年 date -d 2024-02-29 1 day %F # 2024-03-01 date -d 2024-12-31 1 day %F # 2025-01-01这三条输出分别覆盖闰年、闰年进位、跨年进位。你不需要在脚本里判断闰年也不需要知道2月有几天只需要依赖date的实现。唯一前提是运行环境是GNU datemacOS需要走第5章的兼容函数。3.3 任务函数怎么挂业务日志、结果记录与失败中断把占位函数换成真实业务时process_day往往不是一行printf。常见做法是把处理结果写日志并且决定失败时是否中断整个遍历process_day() { local day$1 if run_archive_job $day; then printf %s OK\n $day run.log else printf %s FAILED\n $day run.log return 1 fi } run_archive_job() { local day$1 if [[ ! -d raw/$day ]]; then printf 缺少目录 raw/%s\n $day 2 return 1 fi tar -czf archive/$day.tar.gz raw/$day/ }return 1配合set -e会中断整个遍历适合“某一天失败后续数据就没意义”的场景如果要做断点续跑就不要中断而是把失败日期收集到单独文件跑完后统一重试。这个决策应该在写脚本前确定而不是等跑挂了再想。业务函数里最常见的坑是目录名和日期对不上建议在run_archive_job里先检查raw/$day是否存在再做归档否则tar会把一个空的通配符路径也打进去生成一堆无用的空压缩包。4. 并发、黑名单与可配置化把遍历脚本改成批处理工具实例能跑了但真实场景不会只有三五天。补一个月数据可能跑一晚上补一年数据就需要并发跑批任务通常只跑工作日脚本换了团队用就不能把日期写死。这一章讲这三个升级点。4.1 列表与执行分离用xargs做并发消费如果把整个遍历串在一个while里逐日执行补一年的数据等于连续跑365个串行任务。常见做法是先把日期列表生成到文件再交给xargs并发消费generate_date_list() { local start$1 end$2 local current$start while [[ $current $end || $current $end ]]; do printf %s\n $current current$(date -d $current 1 day %F) done } NOW$(date %F-%T) generate_date_list $start $end dates_${NOW}.txt wc -l dates_${NOW}.txt xargs -P 4 -I {} ./process_one_day.sh {} dates_${NOW}.txt逻辑说明生成列表和消费列表被拆成了两步。生成列表是一趟纯遍历不依赖业务状态出问题重跑代价很低消费列表时xargs -P 4同时起4个进程每个日期调用一次process_one_day.sh这比在while里串行执行快得多。参数说明-P指定最大并发数建议从4开始观察服务器负载再调-I {}用文件里的整行内容替换{}。并发上去了日志会交错所以每个单独任务最好把结果写到自己的日志文件文件名里带日期避免两个进程同时写同一个文件造成脏数据。如果业务函数依赖共享临时文件并发模式下几乎必然会出问题这一点要在设计任务函数时提前规避。4.2 跳过周末和节假日黑名单文件比if判断更省心批处理经常只跑工作日。判断周末用date %w这个参数输出0到60是周日6是周六if [[ $(date -d $day %w) -eq 0 || $(date -d $day %w) -eq 6 ]]; then continue fi这段判断写在生成列表阶段比写在任务函数里更合适——列表里直接没有周末后续所有环节都省心。节假日比周末麻烦因为一年到头日期不固定。常见做法是维护一个黑名单文件# holidays_2024.txt每行一个日期格式严格 YYYY-MM-DD 2024-10-01 2024-10-02 2024-10-03 if grep -qxF $day holidays_2024.txt; then continue fi参数说明grep -q静默匹配不输出内容-x整行精确匹配-F把模式当固定字符串而不是正则。三个参数合起来避免了“2024-10-01”能匹配到“2024-10-0101”这类问题。黑名单文件可以脱离脚本单独维护运营同事也能改不需要打开脚本动代码。4.3 从配置文件读日期范围if判断与非空校验的组合用法当脚本要被不同团队使用时把日期范围写死在脚本里不现实。常见做法是抽一个配置文件# batch.conf START_DATE2024-01-01 END_DATE2024-01-31 WORKERS4脚本里source进来# shellcheck disableSC1091 source ./batch.conf if [[ -z ${START_DATE:-} || -z ${END_DATE:-} ]]; then printf batch.conf 里必须配置 START_DATE 和 END_DATE\n 2 exit 1 fi if [[ ! $START_DATE ~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then printf START_DATE 格式错误: %s\n $START_DATE 2 exit 1 fisource之后要立刻做参数校验这是shell脚本if判断的典型用法。${START_DATE:-}确保变量即使没定义也会展开为空配合-z判断能捕获缺失配置。配置文件里WORKERS可以直接交给xargs的-P参数这样哪天要补一段新数据改配置文件就可以不用打开脚本改逻辑。配置文件里的日期格式校验必须严格否则遍历生成第一个日期就把跑批搞错。我见过一次batch.conf里写成2024-1-31正则直接拒绝这是好行为如果没有正则校验字符串比较会把2024-1-31当成介于2024-01-01和2024-01-31之间实际遍历会提前结束而且日志看起来完全正常很迷惑。5. 避坑手册日期遍历最常见的五个翻车现场与排查写日期遍历脚本逻辑本身不难难在你永远不知道运行环境给你准备了什么“惊喜”。下面五条都是实际撞过墙的问题按现象、原因、解决三个步骤写方便直接对照。5.1 GNU date和BSD date参数打架现象在Linux上跑得好好的脚本放到macOS上直接报date: illegal option -- d整段遍历停摆。原因macOS自带的是BSD date不支持GNU date的-d解析参数这是环境差异而不是脚本逻辑问题。解决在脚本里探测系统类型给两套实现。我一般这样封装next_day() { local day$1 if [[ $(uname -s) Darwin ]]; then date -j -f %Y-%m-%d $day -v1d %Y-%m-%d else date -d $day 1 day %Y-%m-%d fi }后续所有日期推进都改成current$(next_day $current)全脚本只有这一处依赖date命令迁移环境时只改这里不用满文件找date -d。5.2 夏令时让86400秒不等于一天现象用时间戳加86400的写法在3月或11月某一天突然跳过了日期或者同一天出现两次。原因本地时区开启夏令时后某一天实际是23小时或25小时固定加86400秒会踩进不存在的凌晨时间点。解决能用自然日语义就不要亲手加秒数用上面的next_day推进如果必须用时间戳统一加TZUTC环境变量或者干脆把脚本设计成所有日期都以UTC输出只有最终展示时才换算成本地时间。这个问题的观察窗口通常在凌晨1点到3点所以凌晨跑批的脚本最容易中招。5.3 while循环接了管道循环里的变量“神秘消失”现象在while循环里累加计数循环结束后echo $count输出是空的。原因while read从管道读取时运行在子shell里子shell里修改的变量影响不到父进程。解决不要用管道喂while改用进程替换total0 while read -r day; do total$((total 1)) done (generate_date_list $start $end) printf total%s\n $total进程替换让while在当前shell中执行total能保留。如果脚本已经用了管道写法临时解决办法是把结果写文件再读回来但代码会更脏不如直接改写法。5.4 遍历十年日期每轮一个date子进程慢到怀疑机器现象遍历一年还勉强一跑五年十年的补数任务光生成日期列表就要几分钟加上业务函数更是慢得离谱。原因while循环里每次调用date都会fork一个子进程整段遍历的耗时主要耗在进程创建上业务逻辑还没开始。解决生成列表阶段用批量工具替代逐次date调用。在Linux且gawk环境下可以用GNU awk的mktime和strftime一次生成整段日期TZUTC awk -v start$start -v end$end BEGIN { t mktime(substr(start,1,4) substr(start,6,2) substr(start,9,2) 0 0 0) e mktime(substr(end,1,4) substr(end,6,2) substr(end,9,2) 0 0 0) while (t e) { print strftime(%Y-%m-%d, t) t 86400 } }注意两点这里同样存在86400秒不等于一天的问题前置TZUTC让awk在UTC下工作另外Debian、Ubuntu的默认awk是mawk没有mktime需要安装gawk才能用这个方案。如果不想引入awk退而求其次继续用next_day但长跨度补数任务我强烈建议列表阶段单独生成、单独存文件这样中断后能续跑不用从头生成这也算给自己留了后悔药。5.5 日期格式不统一字符串比较静默出错现象传入2024-1-1和2024-01-01结果遍历序列从2024-01-01跳到2024-01-04看起来完全没规律。原因YYYY-MM-DD的字典序之所以等于日期序前提是月、日必须补零到两位。2024-1-1这个格式让字符串比较变成“先比1和0”顺序立刻失效。解决入参用正则强制收口这是最有效的防线if [[ ! $start ~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then printf 日期格式必须是 YYYY-MM-DD\n 2 exit 1 fi如果数据源本身就不规范先规范化再比较normalized$(date -d $raw %F 2/dev/null) || { printf 无法解析日期: %s\n $raw 2 exit 1 }规范化放在正则校验之前还是之后取决于输入来源。如果输入来自外部文件我一般先规范化、再校验、再进遍历这样无论文件里写的是2024-01-01还是2024-1-1最终都能统一成同一种格式。6. 验证在前用期望天数卡住遍历脚本回归的最后一个习惯日期遍历脚本最大的特点是“不出错时很正常出错时很隐蔽”可能跑完几百天只有中间某一天错了日志还不一定有明显异常。我现在养成的习惯是任何日期脚本动业务之前先dry-run把要处理的日期列表落成一个文件然后算期望天数两者对不上就不放行。expected$(( ($(date -u -d $end %s) - $(date -u -d $start %s)) / 86400 1 )) actual$(wc -l date_list.txt) if [[ $actual -ne $expected ]]; then printf 日期数量校验失败: expected%s actual%s\n $expected $actual 2 exit 1 fi期望值用UTC计算避免本地时区夏令时把结果差一天actual用wc -l数文件行数前提是生成列表时一行一个日期。这个校验放在生成列表之后、消费列表之前成本极低收益是挡住所有日期连续性错误。另一个习惯是抽查头、中、尾三个日期分别用脚本里的next_day推一天确认连续。中间日期最好选2月末、3月切换时、12月31日这类边界而不是随机挑一天。这三处连续基本可以覆盖大小月和跨年。这些习惯是从一次补数补到一半发现第64天重复的教训里养成的。那次翻车让我明白日期遍历最不值得信赖的就是“感觉没问题”最值得信任的是由不变量构造出的校验期望天数就是那个不变量。希望帮到你。本文还有配套的精品资源点击获取