侧边栏壁纸
博主头像
一笑痕

仙人之下我无敌,
仙人之上一换一。

  • 累计撰写 75 篇文章
  • 累计收到 7 条评论

11 个古怪文件名,五种批量写法只有两种全对

2026-9-20 / 0 评论 / 7 阅读

11 个古怪文件名,五种批量写法只有两种全对

11 个文件,5 种写法,2 种全对。其余三种里最难堪的是最常见的那条管道:find | xargs 碰到一个带单引号的文件名,直接报 unterminated quote,整批一个都没处理。批量脚本和备份任务这类活儿几乎都会用到这几种写法,而它们的差别只在名字古怪时才暴露,平时跑得好好的。数字来自本机脚本 tools/verify-weird-filenames.sh,临时目录建、跑完删。

数据先摆出来

临时目录: /tmp/weird-names.V6OmNS

=== 测试集 ===
  [dollar$sign.txt]
  [here.txt]
  [newline]
  [plain.txt]
  [star*glob.txt]
  [tab  here.txt]
  [with space.txt]
  [with"dquote.txt]
  [with'squote.txt]
  [with\backslash.txt]
  [中文名.txt]
  共 11 个文件(含 1 个名字里带换行的)

=== 五种写法的结果 ===
A  find | xargs cat                            命中  0 / 11 个名字,另有 0 行多余输出,报错 1 行
                                                 没还原出来: dollar$sign.txt|here.txt|newline|plain.txt|star*glob.txt|tab    here.txt|with space.txt|with"dquote.txt|with'squote.txt|with\backslash.txt|中文名.txt|
                                                 报错前两行: xargs: unterminated quote

B  find -print0 | xargs -0 cat                 命中 11 / 11 个名字,另有 0 行多余输出,报错 0 行

C  find -exec cat {} +                         命中 11 / 11 个名字,另有 0 行多余输出,报错 0 行

D  find | while read f; cat $f                 命中  8 / 11 个名字,另有 0 行多余输出,报错 3 行
                                                 没还原出来: here.txt|newline|with\backslash.txt|
                                                 报错前两行: cat: files/newline: No such file or directory cat: here.txt: No such file or directory

E  find | while IFS= read -r f                 命中  9 / 11 个名字,另有 0 行多余输出,报错 2 行
                                                 没还原出来: here.txt|newline|
                                                 报错前两行: cat: files/newline: No such file or directory cat: here.txt: No such file or directory

测试集里 11 个名字对应 10 个文件,因为其中一个文件名里嵌了换行,名字本身跨了两行。每个文件的内容就是它自己的名字,这样「处理到了几个」可以直接数出来,参考答案是 shell 自己按同一批名字打一遍,不经过任何被测写法。命中数只统计名字被完整还原的个数,多出来的空白行和报错行分开数。

逐条读数字

方式 A 的 0 命中最刺眼。报错只有一行:xargs: unterminated quote,含义是它把文件名里的引号当成引号的起止标记去配对了,配不上就放弃整批输入。前面 9 个完全正常的名字一起跟着没处理,这类失败在输出里只留一行提示,脚本若把 stderr 吞掉就完全看不出来。命中 0 而多余输出也是 0,正是「什么都没做」的样子,和「跑完了」在退出码上还可能都是 0。

为什么会这样,得看 xargs 的工作方式:它拿到的是一串字节,要自己重新切分成参数,切分规则借用了 shell 的词法,所以空白分隔、成对引号这些约定它都认。文件名本身是数据,落到它的切分器里却被当成语法来解析,空格与引号都会改变切分结果。反向的例子也有:star*glob.txtdollar$sign.txt 在这套测试里没造成麻烦,因为通配符展开与变量替换是 shell 解析命令行时做的,名字作为参数往下传的时候不会再展开一次。

方式 B 与 C 都是 11 / 11。B 把分隔符从换行换成 NUL 字节,文件名里允许出现的字符都不含 NUL,所以边界天然可靠,连嵌了换行的名字也能完整送到。C 干脆不经过管道,-exec 后面跟 + 时 find 自己把参数攒成批再调,同样绕开了重新解析一遍名字这一步。

方式 D 丢的是带反斜杠的那个名字,因为 read 不带 -r 时会把反斜杠当转义字符吃掉,with\backslash.txt 送进 cat 时变成了 withbackslash.txt,文件当然不存在。它同时也没有处理带换行的名字,报错两行,路径被拆成了 files/newlinehere.txt 两个不存在的路径。

方式 E 补上了 -r,反斜杠这条恢复正常,命中从 8 升到 9。剩下的缺口是换行:while read 按行读取,文件名里只要有换行就必然被拆开,这是读循环的固有边界,加参数补不上。想让循环也拿到完整名字,得改成 find -print0 | while IFS= read -r -d '' f 这样按 NUL 读的写法。

什么时候用哪个

日常批量处理,find ... -print0 | xargs -0find ... -exec cmd {} + 二选一即可,两者在上述测试集上都没有失败项。要在循环里做复杂逻辑(计数,条件分支,调用自定义函数)时,用按 NUL 读的 while 循环,别用按行读的版本。手写 | xargs 而没加 -0,是我最不建议保留的一条,它失败起来是静默的整批放弃,而不是漏一个文件。

要在自己的项目里验一遍,跑脚本时别把 stderr 重定向到 /dev/null,再记得在测试数据里放一个名字带空格的文件,这两种改动就能把上面三种写法的问题暴露出来。

    🤞 分享