
简介面向Linux/Unix运维与开发人员的Shell实用脚本实例演示如何遍历两个指定日期之间的全部日期并按倒序输出。脚本完整展示了date命令的日期解析、时间戳转换、外层循环安全边界与内层递减逻辑适合日志分析、定时任务调度以及需要按日期批量处理数据的自动化场景。资源包仅含1个PDF文件压缩后大小27KB内容紧凑正文直接列出脚本源码、命令行调用方式和完整的倒序输出结果便于对照运行。已有2025人学习下载。通过该实例可掌握date -d与%F、%s格式化选项的搭配用法学会用时间戳比较控制循环终止条件并理解外层seq循环作为安全上限的作用同时还能学习如何设置两个日期参数、调整循环次数或日期格式来适配不同业务。脚本末尾将当前日期作为参数传给Python脚本的衔接方式为跨命令协同或后续数据处理提供了可复用的模板对初学者熟悉Shell日期处理与循环控制很有帮助。1. 日期范围遍历这个 Shell 脚本为什么值得拆开看做运维和数据处理的人迟早会遇到这类需求给一个开始日期和一个结束日期把中间每一天都拉出来跑一遍任务——日志增量补齐、按天生成 URL、批量导数据、定时任务回刷。最常见的做法就是写一个 shell 脚本 for 循环去遍历日期但很多人第一次写就卡在日期运算上要么格式不对要么跨月出错。这个实例脚本解决的正是这个问题它用date -d配合时间戳比较在 bash 里实现了一段倒序的日期范围遍历并且每输出一个日期就调用一次 python 脚本来做下游处理。适合正在写定时任务、批量数据拉取脚本或者想找一段现成日期遍历模板来改的工程师。脚本不长但里面的时间转换和循环推进方式值得花十分钟拆清楚。2. 读懂核心逻辑date 时间转换与 $i 驱动的倒序过程2.1 输入参数与三行时间转换脚本开头对两个输入参数做了一次格式化核心命令就三行tempdatedate -d -0 day $date1 %F enddatedate -d -0 day $date2 %F tempdateSecdate -d -0 day $date1 %s enddateSecdate -d -0 day $date2 %s这段代码做了两件事。第一把用户输入的日期统一格式化成YYYY-MM-DD%F就是%Y-%m-%d的简写第二把两个日期各自转成 Unix 时间戳也就是从 1970-01-01 00:00:00 UTC 到当前日期经过的秒数%s就是干这个的。为什么要转时间戳呢因为日期字符串没法直接做大小比较——2014-06-30和2014-06-01按字典序比较确实也能分出大小但一旦涉及跨年、跨月、不同位数的格式比如2014-6-1字符串比较就会翻车而时间戳永远是整数-lt、-gt随便用。需要说明的是-0 day其实是冗余写法意思是从当前日期往前推 0 天也就是保持原日期不变。原作者可能想表达「从 date1 开始往回减」所以统一加了-0 day这个前缀实际效果和直接写date -d $date1完全一样。这个写法没有副作用但后面改造的时候可以顺手去掉。2.2 for 循环里的日期推进机制$i 不是计数而是偏移量再看循环主体这是整个脚本最微妙的地方for i in seq 1 300; do if [[ $tempdateSec -lt $enddateSec ]]; then break fi echo $tempdate python pc_jibzhuanti_url.py $tempdate tempdatedate -d -$i day $date1 %F tempdateSecdate -d -$i day $date1 %s done外层的seq 1 300看起来像一个计数循环但变量$i实际承担的角色是「从 date1 往回减的天数偏移量」。第 1 轮循环$i 1于是tempdate更新为date1减 1 天第 2 轮$i 2更新为date1减 2 天以此类推。所以这不是让tempdate自己一步步往前减而是每一轮都重新用date1作为基准减去不同的天数。判断条件是tempdateSec -lt enddateSec就break。因为这是倒序遍历tempdate从 date1 开始逐渐变小当它已经小于 enddate 时说明已经越过结束日期可以收工了。注意比较符号是-lt小于不是-le小于等于所以结束日期本身会在最后一次循环里被输出一次——从结果看2014-06-30 到 2014-06-01 共 30 天全部输出是闭区间。这个写法的优点是直观只要知道date1和偏移量就能精确算出当前应该输出哪一天即使循环因为某种原因断掉重跑只要$i不变日期就不会飘。缺点也很明显tempdate的推进完全依赖$i严格递增如果中间某次date命令执行失败tempdate会停留在旧值但$i还在涨日期序列就会出现跳变。这一点在第 5 章会展开说。3. 跑通并衔接下游参数传递与 python 脚本的调用约定3.1 直接执行与输出核对脚本的调用方式很简单两个日期参数按「开始日期在前、结束日期在后」的顺序传入./pc_jibzhuanti_url_run.sh 2014-06-30 2014-06-01执行后终端会依次打印两行参数确认、一行分隔符然后从 2014-06-30 开始逐行输出日期直到 2014-06-01 结束。每一行日期输出的同时脚本还会执行一次python pc_jibzhuanti_url.py $tempdate也就是说这个 shell 脚本本身不处理业务逻辑它的定位是一个「日期调度器」——负责把日期序列生成好喂给下游的 python 程序。这里有一个值得注意的细节python命令后面直接跟的是脚本名pc_jibzhuanti_url.py没有路径前缀。这意味着执行 shell 脚本时当前工作目录必须包含这个 python 文件否则会报python: cant open file之类的错误。如果要把脚本放到 crontab 里定时跑最好在 shell 脚本开头加一行cd $(dirname $0)把工作目录切到脚本自身所在目录否则 cron 的默认工作目录是当前用户的家目录python 文件大概率找不到。3.2 python 侧怎么接收日期参数虽然素材里没有给出pc_jibzhuanti_url.py的实现但从调用方式python pc_jibzhuanti_url.py $tempdate可以确定这个 python 脚本通过sys.argv[1]接收日期参数。常见做法是像下面这样写import sys import datetime date_str sys.argv[1] print(fprocessing date: {date_str}) # 按业务需求处理比如拼接 URL url fhttp://example.com/data/{date_str.replace(-, )} print(url)这里sys.argv[1]对应 shell 传进来的第一个参数也就是$tempdate。要注意的是sys.argv[0]是脚本自身文件名参数从argv[1]开始取。如果 shell 里传的是带横杠的日期格式如2014-06-30python 侧拿到的就是字符串2014-06-30想转成datetime对象直接用datetime.datetime.strptime(date_str, %Y-%m-%d)即可。shell 传参给 python 有一个容易踩的坑如果日期字符串包含空格或者其他特殊字符必须加引号否则会被 shell 拆成多个参数。这个脚本里的日期格式是%F纯数字加横杠没有空格问题所以直接裸传没问题。但如果你把格式改成了%F %T日期加时间就必须写成python pc_jibzhuanti_url.py $tempdate这一点务必记住。4. 升级改造用 while 循环重写并补上参数校验4.1 为什么 forseq 不适合长跨度原脚本用for i in seq 1 300做循环框架最大的隐患是300这个数字是写死的。假设开始日期和结束日期之间隔了 400 天循环只跑 300 轮就结束了后面 100 天的日期永远不会被输出而且脚本不会报错——没有任何提示就是静默缺失。这种问题在数据回刷场景里尤其危险因为漏掉几天数据可能要到很久之后对账才发现。另外for i in seq 1 300这种方式会先把 300 个数字全部展开到内存里虽然 300 个数字不算什么但如果把上限改成 10000命令行的参数列表长度就会变得很夸张在某些旧系统上可能触发Argument list too long。正确做法是把循环次数去掉让退出条件完全由日期比较来决定这样无论跨度是 30 天还是 3000 天都能正确遍历完。4.2 一套更稳的倒序遍历写法我一般会改成 while 循环把date命令作为唯一的状态更新来源#!/usr/bin/env bash # 用法: ./date_range_run.sh start_date end_date # 功能: 倒序输出 [end_date, start_date] 范围内的每一天并调用 python 处理 set -euo pipefail start_date$1 end_date$2 # 参数校验格式必须为 YYYY-MM-DD for d in $start_date $end_date; do if ! date -d $d %F /dev/null 21; then echo Invalid date format: $d (expected YYYY-MM-DD) 2 exit 1 fi done # 校验 start 必须大于等于 end start_sec$(date -d $start_date %s) end_sec$(date -d $end_date %s) if [ $start_sec -lt $end_sec ]; then echo Error: start_date must be end_date 2 exit 1 fi current$start_date current_sec$start_sec while [ $current_sec -ge $end_sec ]; do echo $current python pc_jibzhuanti_url.py $current # 往前推一天如果已经到达 end_date 则退出 if [ $current $end_date ]; then break fi current$(date -d $current - 1 day %F) current_sec$(date -d $current %s) done这段代码和原脚本相比做了四个改进。第一加了set -euo pipefail遇到未定义变量直接报错退出管道中任何命令失败也会让脚本失败避免静默出错第二对两个输入参数做了格式校验格式不对立刻报错退出而不是让date命令输出一堆invalid date再继续跑——注意date -d $d %F /dev/null 21这行的作用如果能成功解析日期date会输出格式化结果到 stdout我们把它丢进/dev/null扔掉只看退出码第三校验了开始日期必须大于等于结束日期防止参数写反第四核心推进逻辑是date -d $current - 1 day %F基于当前日期自己往前减一天不再依赖循环变量$i循环轮数完全由current_sec -ge end_sec控制跨度再大也不会漏。退出逻辑需要单独说一句while [ $current_sec -ge $end_sec ]控制了只要当前日期还大于等于结束日期就继续循环在循环体内部又加了一个if [ $current $end_date ]; then break。为什么外面已经判断了-ge还要再判断一次相等呢因为当current正好等于end_date时我们会先打印它、调用 python 处理它然后再 break这样才能确保结束日期本身被包含在输出序列里。如果不加这个判断直接让循环体的最后一行去减一天那么current变成end_date的前一天时循环条件就为假了end_date本身会被漏掉。5. 避坑笔记日期遍历脚本的 5 个常见翻车现场5.1date: invalid date输入格式没校验现象脚本执行后没有任何日期输出终端直接报date: invalid date 2014-6-1或者更隐蔽的——日期被解析成了错误的值。原因date -d对日期字符串的解析很宽容但不接受所有格式。2014-6-1月份和日期不补零在大部分 Linux 发行版上能解析但2014/06/01在部分系统上会被当成非法格式。更坑的是如果你传入06-01-2014有些系统会按 MM-DD-YYYY 解析有些按 DD-MM-YYYY 解析结果差一天甚至差一个月。解决在脚本开头加一层格式校验强制要求YYYY-MM-DD。可以用正则做快速判断echo $d | grep -qE ^[0-9]{4}-[0-9]{2}-[0-9]{2}$再用date -d $d %F做二次确认——如果解析后的结果和原字符串不一致说明格式不标准也可以直接报错。另外要警惕date命令解析2024-02-30这类不存在的日期它不会报错而是会自动进位成2024-03-01静默地给你一个「正确」但违反直觉的结果。5.2 超过 300 天后日期静默缺失seq 写死的边界现象脚本能正常执行不报任何错误但输出的日期只有 300 天后面的日子全没了。如果数据回刷跨度超过一年就会明显发现输出序列「断尾」。原因原脚本的外层for i in seq 1 300只提供 300 次循环机会$i最大到 300意味着最多只能从开始日期往回减 300 天。这个数字应该是原作者按当时业务场景拍的换成其他场景就失效了。解决用 while 循环重写如第 4 章所示让循环次数完全由日期比较决定。如果暂时不想改结构懒人办法是把seq 1 300改成seq 1 3650覆盖 10 年但这只是把坑变大没有填平。任何时候写日期遍历都不应该存在一个与业务无关的「最大循环次数」。5.3 日期跳变或重复$i 更新方式依赖循环连续性现象输出序列中出现2014-06-30、2014-06-29、2014-06-29、2014-06-28这种连续两天重复或者直接跳过了某一天。原因原脚本中tempdate不是通过自己减一天来推进的而是通过-$i day $date1重新计算。一旦某轮循环中date命令因为系统负载、依赖的时区文件出错等原因执行失败tempdate保持旧值但下一轮$i已经 1算出来的新日期就会和上一轮相隔两天。如果失败发生在最后一轮则可能出现日期完全不推进的死循环在 for 循环体里表现为跳变。解决改用current$(date -d $current - 1 day %F)的方式让日期自己迭代自己每一轮的结果只依赖上一轮的结果不依赖循环计数。同时在关键步骤后加set -e让date失败时脚本直接退出而不是带着脏数据继续跑。5.4 macOS 与 Alpine 容器里date -d直接报错现象同一个脚本在 CentOS 上跑得好好的放到 macOS 上执行立刻报date: illegal time format或者放到 Alpine 的 Docker 容器里报date: invalid date -0 day 2014-06-30。原因date -d是 GNU coreutils 的语法macOS 自带的 BSD date 用的是-j -f参数解析方式完全不同。Alpine 默认用的 BusyBox date 对-d的支持也很有限不一定能识别-0 day这种英文表达式——BusyBox 的-d更倾向于接收YYYY-MM-DD这种裸日期对相对日期的支持取决于编译选项。解决先确认运行环境用date --version看是不是 GNU 版本。macOS 用户可以装 coreutils 后改用gdate或者干脆在脚本里做环境分支if command -v gdate /dev/null 21; then DATE_BINgdate else DATE_BINdate fi current$($DATE_BIN -d $current - 1 day %F)这句判断逻辑不算复杂优先用gdate找不到就退回date。在 Alpine 等 BusyBox 环境里如果不想折腾最保险的方式是跳过相对日期运算直接用时间戳做算术——一天是 86400 秒current_sec$((current_sec - 86400))再用date -d $current_sec %F转回日期格式绕开所有解析差异。注意时区会影响时间戳的转换结果建议脚本开头统一export TZAsia/Shanghai或按业务所在时区设置。5.5 时区没对齐导致输出日期差一天现象脚本在当前机器上执行输出2014-06-30但同一份脚本放到 Docker 容器里第一天就输出2014-06-29整整往前偏了一天或者每天 0 点左右执行定时任务时日期的变化时点总是不符合预期。原因date -d $current - 1 day %s转时间戳时参考的是系统当前时区而容器默认时区往往是 UTC和宿主机相差 8 小时。日期字符串2014-06-30在 UTC 时区解析得到的时间戳和上海时区解析的差了 8 小时做- 1 day的秒数运算后再格式化回来就可能在边界上偏差一天。解决在脚本开头显式声明时区export TZAsia/Shanghai或所在业务时区确保所有date解析和格式化都在同一时区下进行。如果是处理纯日期不带时间还有一个更稳的技巧不要在日期字符串之间做「减 86400 秒」的运算而是用date -d $current - 1 day %F这种基于日历日期的运算它会按日历规则处理跨月、跨年不受 UTC 偏移影响——但前提仍然是给date命令设置统一的TZ。6. 进阶用法多日期并行处理与输出完整性校验6.1 并行处理xargs -P 控制并发当日期跨度有一两百天每调一次 python 脚本要花几秒拉数据时串行执行的时间成本会让人失去耐心。常见的做法是让 shell 脚本只负责生成日期序列把并发交给 xargs 处理# 生成日期序列但脚本里的 python 调用先去掉 ./date_range_run.sh 2014-06-30 2014-06-01 dates.txt # 用 4 个并发进程处理每行日期作为一个参数 cat dates.txt | xargs -P 4 -I {} python pc_jibzhuanti_url.py {}-P 4表示同时跑 4 个 python 进程-I {}把 dates.txt 里的每一行替换成{}。这个方式的优点是不需要改 shell 脚本本身生成日期序列和消费序列解耦缺点是输出顺序会乱如果下游任务要求按日期顺序落盘就得让 python 脚本自己根据参数拼输出文件名而不是依赖标准输出的顺序。并发数不是越大越好要看下游接口的承受能力数据库写入的话 4~8 个并发通常比较安全接口调用压测一下再定。6.2 用 diff 校验日期序列完整性日期遍历脚本最怕的不是报错而是漏日子不报错。我有一个习惯批量处理之前先做一次「干跑校验」——生成期望的日期序列和实际输出做对比# 期望序列用 date 命令生成验证脚本的遍历逻辑 exp_file/tmp/expected_dates.txt cur2014-06-30 while [ $cur \ 2014-06-01 ]; do echo $cur $exp_file cur$(date -d $cur - 1 day %F) done # 实际序列跑重构后的脚本屏蔽 python 调用临时加一个参数跳过 ./date_range_run.sh 2014-06-30 2014-06-01 2/dev/null | grep -E ^[0-9]{4}- /tmp/actual_dates.txt diff $exp_file /tmp/actual_dates.txt echo PASS: 日期序列完整diff无输出且返回码为 0说明两个文件逐行一致序列完整有输出的话会明确列出哪一行开始不一致定位问题就很快。这个校验脚本我用过一次之后就成了固定动作——有一次改出了 bug日期在跨月边界上重复了一天输出看起来完全正常但 diff 立刻抓出来了。从那以后我每次改完日期遍历逻辑都会强制走一遍「期望序列 vs 实际输出」的 diff 校验再放进 crontab 跑正式任务。这个实例脚本虽然短但覆盖了日期格式化、时间戳转换、循环边界、参数传递和下游调用这些写自动化任务绕不开的点。把时间戳比较的原理搞明白把$i偏移改成日期自迭代再补上格式校验和完整性校验这套模板就能放心复用了。希望帮到你。本文还有配套的精品资源点击获取