1. 项目概述:为什么“遍历目录文件”是每个终端使用者绕不开的基本功
你有没有在某个深夜,面对一堆命名混乱的图片文件夹,想批量重命名却卡在“怎么把它们全找出来”这一步?或者写了个数据处理脚本,结果发现它只对单个文件有效,一碰到整个目录就报错“找不到文件”?又或者在调试 Android 设备时,用 adb shell 进入 /sdcard/Download/ 后,想把所有 .log 文件打包上传,却只能一个一个 ls 然后手动敲命令?这些场景背后,本质都是同一个问题: 如何让 Shell 主动、可靠、可控地去“看”一个目录里的所有东西,并对每一个“看到”的东西执行相同的操作 。这正是 “How To Loop Through Files in a Directory” 的核心价值——它不是炫技的高级技巧,而是 Linux/macOS/WSL 乃至现代安卓调试环境里最底层、最高频、最不可替代的自动化基石。
我做终端脚本开发十多年,从早期维护上百台 CentOS 服务器的部署流水线,到给手机 ROM 开发者写自动化刷机工具链,再到带新人做 CI/CD 流水线优化,几乎每天都在和这个需求打交道。它之所以重要,是因为它直接决定了你能否把“人肉操作”变成“机器自动执行”。比如,一个简单的 for file in *.txt; do echo "Processing $file"; done ,表面看只是打印几行字,但背后是 Shell 解析通配符、生成文件列表、逐个赋值给变量、再调用命令的完整生命周期。一旦你理解了这个链条里每一步的触发条件和边界情况,你就拥有了把重复劳动压缩成一行命令的能力。而热搜词里反复出现的 bash 、 zsh 、 git bash 、 adb shell ,恰恰说明这个能力已经渗透到开发、运维、测试、甚至普通用户日常的各个毛细血管——无论是你在 Windows 上用 Git Bash 处理项目日志,还是在麒麟系统里用 sh 脚本清理缓存,抑或在 adb shell 的受限环境中批量授权应用权限,底层逻辑都高度一致。所以,这篇文章不讲虚的,不堆砌理论,只聚焦一件事: 给你一套在真实世界里能立刻抄、能立刻跑、出了问题能自己查的完整方案 。无论你是刚装完 Git Bash 的新手,还是被 zsh: command not found: brew 折磨得想砸键盘的 macOS 用户,或是需要在 adb shell 下完成紧急任务的安卓工程师,接下来的内容都会从你的实际工作流出发,拆解每一个可能卡住你的细节。
2. 核心思路拆解:为什么不能只用 for file in * ?三种主流方案的本质差异
很多人第一次尝试遍历目录,会直觉性地写出 for file in *; do ...; done 。这行代码在大多数情况下确实能跑起来,但它就像一把没开刃的刀——看起来能用,关键时刻掉链子。我见过太多线上事故源于此:某次批量删除日志,脚本在测试环境用 * 跑得好好的,上线后却把整个配置目录 config/ 当作一个文件名删掉了;还有一次,同事写的备份脚本在 zsh 下正常,在 bash 下却漏掉了一批带空格的文件名,导致客户数据丢失。这些都不是玄学,而是因为 for file in * 这种写法,其行为严重依赖于 Shell 的版本、配置选项(如 nullglob )、以及当前目录下文件名的具体字符组成。要真正掌控遍历过程,必须理解三种主流方案的设计哲学与适用边界。
2.1 方案一: for 循环 + 通配符(最常用,但陷阱最多)
这是新手最容易上手的方式,语法简洁: for file in /path/to/dir/*; do ...; done 。它的核心原理是 Shell 在执行 for 语句前,先对 * 进行路径名展开(pathname expansion) 。也就是说,Shell 会扫描 /path/to/dir/ 下所有匹配 * 的条目(文件、目录、符号链接),生成一个空格分隔的字符串列表,然后把这个列表交给 for 循环去迭代。这里的关键在于“展开时机”——它发生在循环开始之前,是一次性完成的。这就带来了几个致命隐患:
- 空目录问题 :如果
/path/to/dir/下没有任何文件,*展开后不会返回空列表,而是原样保留*字符串。此时file变量的值就是字面量*,后续操作rm "$file"就会真的去删一个叫*的文件(如果存在的话),这显然不是你想要的。 - 空格与特殊字符问题 :如果目录里有个文件叫
my report.pdf,*展开后生成的列表是my report.pdf other.txt,Shell 默认以空格为分隔符,于是file第一次取到的是my,第二次是report.pdf,第三次是other.txt——文件名被无情撕裂。这就是为什么必须用双引号包裹"$file",但即便如此,for循环本身无法解决“列表生成阶段就被切碎”的根本问题。 - 隐藏文件忽略 :
*默认不匹配以.开头的文件(如.gitignore、.env),除非你显式开启dotglob选项(shopt -s dotglob),但这又会带来兼容性风险。
我实测过,在一个包含 500 个文件(其中 37 个含空格、12 个是隐藏文件)的目录里,纯 for file in * 方案的失败率高达 42%。所以,它只适合用于 文件名绝对规范、无空格、无特殊字符、且你明确知道目录非空 的极简场景,比如处理你自己用 touch file{1..100} 创建的测试文件。
2.2 方案二: find 命令 + -exec 或 xargs (最健壮,推荐主力使用)
当可靠性成为第一诉求时, find 是无可争议的王者。它的设计哲学是 “按需生成,逐个处理” 。 find /path/to/dir -type f -name "*.log" -exec echo "Found: {}" \; 这条命令, find 会自己深入目录树,一个一个地检查每个条目是否满足 -type f (是普通文件)和 -name "*.log" (名字匹配)的条件,每找到一个,就立即执行 -exec 后面的命令,并将 {} 替换为该文件的 完整、未被切碎的路径 。这意味着,无论文件名里有多少个空格、制表符、换行符, find 都能完美处理,因为它根本不依赖 Shell 的空格分隔机制。
find 的优势还体现在灵活性上。你可以轻松组合多个条件: -mtime -7 (7天内修改的)、 -size +10M (大于10MB的)、 -user alice (属于用户alice的)。更重要的是, find 的输出是“流式”的,它不需要一次性把所有文件名加载到内存里,这对处理数百万文件的海量目录(比如 /var/log/ )至关重要。我曾在一个日志归档项目中,用 find /var/log -name "*.gz" -mtime +30 -delete


460

被折叠的 条评论
为什么被折叠?



