简介:针对Shell脚本中常见的日期范围遍历需求,一份可直接运行的脚本实例被整理为PDF文档。脚本通过命令行传入开始日期与结束日期,利用date命令解析日期并转换为Unix时间戳,再借助嵌套循环逐日递减,输出范围内的全部日期,同时调用关联Python脚本完成后续处理;文档中还给出了完整代码、执行命令及实际输出结果,便于读者对照学习。资源文件仅有1个PDF文档,大小约27KB,内容精简,聚焦核心实现,适合Linux/Unix系统管理员、运维人员及Shell初学者快速理解日期遍历的原理与写法。该资源已有2026人学习下载,对于需要编写定时任务、日志按日归档或批量日期处理脚本的用户,可直接参考其中的循环结构与日期格式化方法,根据自身场景调整参数,有效提升自动化脚本的开发效率。
1. Shell脚本遍历一个日期范围:先搞懂你到底要按天、按月还是按步长走
做数据报表、日志清理、批量补跑任务的时候,十有八九会遇到这种需求:给你一个起止日期,比如从2024-01-01到2024-01-31,要把这31天逐个作为参数传给下游脚本,或者每天生成一个目录、下载一份文件。手工写31次命令显然不现实,于是大家第一反应就是写个Shell脚本循环。但真正上手你会发现,日期遍历这个看起来很简单的事,第一次跑通常会翻车——日期格式对不上、跨月算错、Mac和Linux的表现不一样,甚至循环到一半卡死。这篇文章就专门讲清楚Shell脚本遍历日期范围的原理、常用写法、参数取舍,以及我踩过的那些坑。适合正在写调度脚本的运维、数据分析师,以及任何需要批量按日期处理文件的开发者。核心思路是用date命令做日期加减,配合while或for循环,把字符串日期换成游标,逐个输出。
2. 循环用什么、日期怎么加:遍历日期范围的底层选型和三条可走的路
2.1 先想清楚“遍历”的粒度:天、月、工作日还是自定义步长
很多人上来就写for i in {1..31},这其实是把问题简化成了“数字遍历”,一旦碰到跨月、跨年、二月天数不同就报废。真正的日期遍历,每一步都依赖上一步算出的“新日期”,不是简单的整数加一。所以第一步是确定步长:按天循环最常见,比如日志按天生成,需要从过去30天逐个补跑;按月循环则常用于月度报表,需要处理每个月最后一天不同的问题;按小时或按自定义步长(比如每3天)也有场景,但对Shell脚本来说,核心机制都一样,只是日期加减的单位不同。我一般会先在纸上把输入输出写明白:起始日期、结束日期、步长、输出格式(YYYY-MM-DD还是YYYYMMDD),这四个参数定了,脚本骨架就出来了。
2.2 三条常用路线:for + seq、while + date -d、以及利用时间戳
路线一:for i in $(seq 1 天数)配合date -d "2024-01-01 +$i days"。这种写法是先算出区间内总天数,然后数字循环。优点是直观,缺点是需要额外计算间隔天数,而且跨月时如果你用“当月第几天”加偏移,很容易在月末出错。路线二:while循环,用一个变量存当前日期,每轮用date -d "$current + 1 day"算出下一个日期,直到当前日期大于结束日期。这是最稳妥且最通用的写法,因为它不关心月份跨度,date命令会帮你处理跨月进位。路线三:先把起止日期转成时间戳(date +%s),循环里对时间戳加86400秒,再转回日期。这种适合需要精确到小时或秒的场景,但要注意夏令时地区,一天不一定是86400秒,否则会累积漂移。对纯日期遍历,我更推荐路线二,后面所有代码都基于它。
2.3 为什么用date -d而不是手动算:边界处理和格式化的红利
date -d是 GNU date 提供的日期解析和运算入口,Linux 上默认就是 GNU 版本。它支持date -d "2024-01-31 + 1 month"直接得到2024-02-29,也支持date -d "2024-01-31 last day"取月末。这些逻辑自己用case或awk写非常容易漏掉“2月有没有29号”这种边界。另一个红利是输出格式随意定:+%Y%m%d得到紧凑格式,+%F得到标准日期格式,+%A还能拿到星期几,这在过滤周末时特别有用。所以我的建议是:所有日期运算一律交给date,脚本里只做比较和循环,不要手动拼接月份天数。
2.4 开始写前先确认你的 Linux 发行版和 date 版本
永远先跑一句date --version或date -d "2024-01-01 + 1 day"看看结果。macOS 自带的 BSD date 不支持-d,会直接报错,需要改用date -v+1d。如果你写的脚本要同时跑在 Mac 和 Linux 上,常见的做法是先检测uname,或者干脆在脚本开头定义一个NEXT_DATE()函数,根据系统类型切换命令。别小看这一步,很多人脚本写得没问题,换台机器就变成date: illegal option -- d,这是最容易踩的跨平台坑。生产环境如果是 CentOS 7 和 Ubuntu 20.04,GNU date 版本差异不大,但老版本对date -d "2024-01-31 + 1 month"的语义略有不同,建议至少验证一下。
3. 从零写一个可复用的日期范围遍历脚本:核心代码与参数设计
3.1 最小可用版:用 while 循环按天遍历,带两个命令行参数
下面这段脚本是我最常用的模板,支持传入起始日期和结束日期,按天步进输出每一天。先看代码,再解释各部分为什么要这么写。
#!/bin/bash # 功能: 按天遍历日期范围, 输出每一天 # 用法: ./date_range.sh 2024-01-01 2024-01-31 # 参数: 起始日期, 结束日期, 格式均为 YYYY-MM-DD start_date="$1" end_date="$2" # 防止使用者没传参时直接拿空值去跑 if [ -z "$start_date" ] || [ -z "$end_date" ]; then echo "用法: $0 <起始日期> <结束日期>" exit 1 fi current="$start_date" # 循环终止条件: 当前日期大于结束日期时退出 while [ "$(date -d "$current" +%s)" -le "$(date -d "$end_date" +%s)" ]; do echo "$current" # 这里放置你要对每个日期做的实际操作 # 核心: 用 date -d 计算下一天, 赋值给 current current=$(date -d "$current + 1 day" +%Y-%m-%d) done这段代码的关键在于比较逻辑:直接用字符串比较日期是不可靠的,因为"2024-02-01"小于"2024-01-31"这种字典序恰好能工作,可一旦格式换成YYYYMMDD且位数不固定就会出问题。所以我把两个日期都转成了 Unix 时间戳做数值比较。date -d "$current + 1 day"会正确输出下一天,跨月时自动进位,例如从2024-01-31会得到2024-02-01,完全不用自己判断月份天数。至于+%Y-%m-%d这层格式化,是为了保证 current 变量规范,避免date -d对某些省略写法的二次解析产生歧义。
3.2 增强版:增加步长参数、月末自动对齐和日期格式输出
实际业务里不可能只有按天一种需求。我给这个脚本增加三个能力:支持按 N 天步进、支持按月遍历并自动落到每月最后一天、支持自定义输出格式。先看按月遍历的完整实现,再解释参数。
#!/bin/bash # 功能: 按月遍历日期范围, 每个月的月末自动对齐 # 用法: ./month_range.sh 2024-01-31 2024-06-30 start_date="$1" end_date="$2" current="$start_date" while [ "$(date -d "$current" +%s)" -le "$(date -d "$end_date" +%s)" ]; do echo "$current" # 算出下个月的同一天, 然后取这个月的最后一天, 避免跳到下一个月的同一天丢失月末 current=$(date -d "$current + 1 month" +%Y-%m-%d) # 如果得到的日期不是月末, 修正为当月的最后一天 if [ "$(date -d "$current" +%d)" != "$(date -d "$current" +%d)" ]; then # 实际多数 GNU date 版本会自动处理, 这里保留备用逻辑 : fi current=$(date -d "$current" +%Y-%m-01) current=$(date -d "$current + 1 month - 1 day" +%Y-%m-%d) done上面注释里的:是占位用的,其实对于 GNU date,2024-01-31 + 1 month会直接输出2024-02-29,不会变成2024-03-02。但有些旧版本会有偏移逻辑,所以我在这里用了一个“先回到当月1号,再推到下个月1号,最后减一天”的兜底写法。这段代码演示了怎么处理“每月最后一天”这种业务语义:先取下个月1号,再减一天,永远正确,不会出现2月30号之类的鬼日子。如果你想按 N 天步进,只需要把+ 1 day改成+ $step days,其中 step 从第三个参数读入。
另一个常用需求是输出格式,比如文件名里要20240101而不是2024-01-01。做法是在 echo 和计算 current 时都改成+%Y%m%d,比较时仍然用时间戳,完全不受影响。我建议把格式定义成一个变量OUT_FMT="%Y-%m-%d",脚本里统一引用,这样以后改格式一处搞定。
3.3 为你的实际任务预留处理钩子,而不是把循环体写死
上面两个脚本都只是把日期echo出来,真正的任务是写在注释“这里放置你要对每个日期做的实际操作”那个位置的。常见做法有两种:第一种是直接内联命令,比如python3 monthly_report.py --date "$current";第二种是调用一个外部脚本,把日期作为参数传入,脚本里根据返回码决定是否重试。我推荐第二种,因为日期遍历脚本的职责是“生成日期序列”,具体业务处理应该和遍历解耦,方便单独测试。实际落地时,我会这样改造循环体:
# 执行业务命令, 保留输出日志, 根据返回值决定是否中断 if ! /data/bin/process_day.sh "$current"; then echo "处理失败: $current" exit 2 fi这样遍历一旦遇到失败,能立刻停止并暴露问题日期,避免后面所有日期白跑。如果希望跳过失败继续跑,就把exit 2改成continue,并配一个error.log记录。这个改动很小,但能省去很多排障时间。
3.4 时间戳、日期格式和循环次数的关系:三个常被忽略的参数
用date +%s转换消耗很小,一万次循环也就多零点几秒,所以别为了性能放弃可靠性。真正需要关注的是两个格式陷阱:一是%Y是四位年,%y是两位年,混用会导致比较错乱;二是日期字符串里的分隔符,20240101和2024-01-01在date解析时都能识别,但如果你用%s比较,原格式无关紧要,所以统一用标准格式最安全。循环次数的上限你自己心里要有数:按天遍历一年是365次,按小时是8760次,每次循环开一个子进程调用date,对 Shell 来说压力不大,但如果在循环里再启动重量级程序,就要评估总耗时,必要时改用 xargs 或 parallel 并发。并发后注意输出顺序,后续章节会讲。
4. 日期遍历避坑指南:这 5 个问题我全踩过,现象、原因和解决都在这里
4.1 跨月时循环少一天或多一天:问题出在字符串比较
现象:遍历2024-01-31到2024-02-02,输出只有 01-31 和 02-02,缺了 02-01。原因:如果循环条件写成[ "$current" != "$end_date" ],并且每次把日期加一天,那么当 current 变成2024-02-01时,它不等于结束日期,会继续加一天变成2024-02-02,此时等于结束日期,条件不满足就退出了,中间那一次循环被吞掉。解决:用时间戳比较而且要-le(小于等于),不是-ne也不是-lt。我在最小脚本里已经用了时间戳,但如果你是从网上抄的while [ "$cur" != "$end" ],就会遇到这个问题。建议统一写成一个函数:
date_less_equal() { [ "$(date -d "$1" +%s)" -le "$(date -d "$2" +%s)" ] }4.2 Mac 上运行报date: illegal option -- d
现象:脚本在 Linux 上一切正常,拷贝到 Mac 终端立刻报错。原因:macOS 的date是 BSD 版,不支持 GNU 的-d参数,连帮助文档都没有。解决:要么按系统判断切换命令,要么干脆在 Mac 上装 GNU coreutils,用gdate命令。但我不想引入额外安装,就在脚本开头加一个检测函数:
if [[ "$(uname)" == "Darwin" ]]; then DATE_CMD="date -v+1d" # mac 上的日期加减写法示例: date -v+1d -v-1m +%Y-%m-%d else DATE_CMD="date -d +1 day" fi注意 Mac 的-v参数语法和-d完全不一样,写函数时要把两种分支都实现,或者定义NEXT_DAY()和PREV_DAY()两个抽象函数,不要让业务代码直接调用 date。这样脚本迁移成本最低,也算是一种通用设计。
4.3 月份遍历得到 3 月 31 号跳到 5 月 1 号,语义不对
现象:按月遍历2024-01-31,期望下一个月是2024-02-29,结果有的是2024-03-02(某些版本)或是直接报错。原因:GNU date 对“月末加减一个月”的规则是“向后偏移,如果目标月份没有该日,则取该月最后一天”,但不同版本有这个行为的差异。解决:不要依赖date -d "2024-01-31 + 1 month"的隐含语义,自己用“当月1号加一个月减一天”来求月末。代码:
# 给定任意日期, 返回所在月份最后一天 last_day_of_month() { local y m y=$(date -d "$1" +%Y) m=$(date -d "$1" +%m) date -d "$y-$m-01 + 1 month - 1 day" +%Y-%m-%d }这样无论起始日是几号,月末结果永远是稳定的YYYY-MM-最后一天。如果你要的是“下个月同一天”,就另当别论,但大多数报表遍历要的都是“每个月跑月结”,所以用月末对齐最不容易产生空跑和重复跑。
4.4 时区和夏令时导致按小时遍历多一小时
现象:按小时遍历从2024-03-09到2024-03-11(北美夏令时切换日),发现只有23个小时或25个小时,时间戳加86400会出现重叠。原因:时间戳是绝对时间,而date -d "+ 1 hour"走的是本地时间流,两种方式混用时基准不一致。解决:按小时遍历时统一用date -d "$current + 1 hour",不手动加秒数;如果一定要加秒数,则注意结果要用date -d @$timestamp转换回本地时间,并且接受夏令时带来的“少一小时”。对国内开发来说,夏令时影响不大,但如果你在维护海外业务,这就是真实事故。建议在脚本里检测TZ变量,对非 UTC+8 的地区格外小心。
4.5 循环体内重定向导致日期变量丢失
现象:脚本写得好好的,突然某一天开始循环只执行一次,后面的日期全不输出。原因:有人在循环体里写了done > output.txt,或者用了管道while ... | sort,导致 while 在子 shell 中运行,循环里对变量current的修改传不到父 shell,实际上每一轮 current 都从初始值重新开始,循环退化成死循环或单次。解决:尽量避免给 while 加管道;如果非要排序,可以先把输出收集到临时文件再排序。或者用进程替换写法while ...; do ...; done < <(cat list),但这里我们本来就在循环内更新 current,所以最简单的做法是循环体不接管道。这个坑特别隐蔽,因为单步调试时能看到 current 在变,但整体输出就乱了。
5. 实战进阶:日期列表生成、并发处理和一个能救命的自检技巧
真正成熟的日期遍历脚本,不会只把日期打印到屏幕就结束。我习惯把“生成日期列表”和“执行任务”拆成两个阶段。先跑一次遍历,把结果写入文件,比如dates.txt,确认里面的日期行数、首尾、特殊月份都符合预期,然后再用while read或xargs逐行消费。这样既能自查,也方便断点续跑。举个例子,我要生成某个月所有工作日的日期列表,用第一段脚本加一个星期几判断:
# 生成 2024-01 到 2024-03 之间所有周一至周五的日期 for d in $(./date_range.sh 2024-01-01 2024-03-31); do w=$(date -d "$d" +%u) if [ "$w" -le 5 ]; then echo "$d" fi done > workdays.txt注意这里$(./date_range.sh ...)会把日期当作命令行参数,如果日期量巨大(比如几年)可能超出 ARG_MAX,建议改成先输出到文件,再while read逐行处理。并发是另一个真实需求:如果每天的任务互相独立,可以这样跑到 5 个并行进程:
cat dates.txt | xargs -P 5 -I {} ./daily_job.sh {}但加了xargs -P之后,输出顺序会乱,而且循环里不能再用current累加这种逻辑,因为每个任务都是独立进程。正确姿势是把日期作为参数传给子脚本,子脚本内部不需要知道“下一个日期是谁”。跑完后检查是否有失败记录:我一般在daily_job.sh里对成功或失败分别输出OK或FAIL加日期,最后用grep FAIL快速定位。还有一个自检技巧——用两个互相独立的方法验证结果:对天数可以用date -d "$end" +%s减date -d "$start" +%s再除以 86400,但注意要加一天才包含两端;对月数可以用echo $(( ( ($(date -d "$end" +%Y) * 12 + $(date -d "$end" +%m)) - ($(date -d "$start" +%Y) * 12 + $(date -d "$start" +%m)) + 1 ))。数值对得上,再交给下游。
最后说一个我的血泪习惯:任何日期遍历脚本,第一轮永远先在“小范围”上验证,起始日期选一个跨月的,比如2024-01-30到2024-02-03,结束日期选一个带月末的,比如2024-02-29。输出看一眼就知道有没有边界问题。另外,脚本里所有date命令都不要打-u,除非你想强制 UTC,否则本地时区会混入不可预知的时间漂移。希望这个技巧能帮你少走弯路,让遍历日期范围这个动作成为你脚本库里最省心的一个环节。
本文还有配套的精品资源,点击获取