
一个清理脚本在本地目录上跑得很顺。换到另一台机器,文件少删了几个,退出码还是 0。目录里多了几个带空格的文件名,find 输出的一行被拆成了好几段,拼出来的路径不存在,命令就按不存在的路径删。
macOS 自带的是 FreeBSD 版的 xargs,man 页里那几条说明正好对应现场。把文档原文和本机实测并排放一遍,问题出在哪一行就清楚了,顺带能看到另外几个平时没注意的默认值。
-0 的说明里写着配 find -print0 用
-0, --null
Change xargs to expect NUL (``\0'') characters as separators,
instead of spaces and newlines. This is expected to be used in
concert with the -print0 function in find(1).
「expect NUL characters as separators, instead of spaces and newlines」这句就是根因。默认分隔符是空格和换行,文件名里的空格被当成参数边界,一个文件被切成好几段。
同一个目录,两种取值数出来的条目数差了三倍多。
== 2. 默认按空白切分:一个文件名被拆成好几个参数 ==
find . -type f | xargs -n1 echo 输出行数: 11
实际文件个数: 4
--- 前几行长这样 ---
./普通.txt
./带
空格
的
文件名.txt
./第二个
== 3. -0 配 -print0:一个文件一个参数 ==
find . -type f -print0 | xargs -0 -n1 echo 输出行数: 4
--- 全部输出 ---
./普通.txt
./带 空格 的 文件名.txt
./第二个 文件.txt
./子 目录 一/里面 的.txt
四个文件,默认写法吐了 11 行;-print0 配 -0 是 4 行,每个文件名原样保留。前几行的输出里能看到 ./带、空格、的、文件名.txt 各占一行,这就是被拆开的痕迹。这类错不会报错,只会安静地少做事。
-n 的默认值是 5000
-n number, --max-args=number
Set the maximum number of arguments taken from standard input for
each invocation of utility. An invocation of utility will use
less than number standard input arguments if the number of bytes
accumulated (see the -s option) exceeds the specified size or
there are fewer than number arguments remaining for the last
invocation of utility. The current default value for number is
5000.
一次调用能塞多少参数,默认 5000 个。这个数字平时用不到,倒是 -n 调小之后的进程数更容易踩:每条命令都会新起一个进程,参数给得越少,起的次数越多。
== 5. 批量:-n 把多个参数塞给一次调用 ==
-n1 起进程次数: 4
-n8 起进程次数: 1
四个文件时,-n1 起 4 次,-n8 起 1 次。文件量上去之后这个差别会被放大,四千个文件比一次更明显。往反方向调也有上限,说明里那条 -s 记的是命令行字节数,参数名太长时引擎会提前切一批,实际每批的个数比 -n 给的少。
-I 每读一行就起一次命令
xargs [-0oprt] [-E eofstr] [-I replstr [-R replacements] [-S replsize]]
[-J replstr] [-L number] [-n number [-x]] [-P maxprocs] [-s size]
[utility [argument ...]]
DESCRIPTION
The xargs utility reads space, tab, newline and end-of-file delimited
strings from the standard input and executes utility with the strings as
arguments.
Any arguments specified on the command line are given to utility upon
each invocation, followed by some number of the arguments read from the
standard input of xargs. This is repeated until standard input is
exhausted.
Spaces, tabs and newlines may be embedded in arguments using single
(`` ' '') or double (``"'') quotes or backslashes (``\''). Single quotes
escape all non-single quote characters, excluding newlines, up to the
matching single quote. Double quotes escape all non-double quote
characters, excluding newlines, up to the matching double quote. Any
single character, including newlines, may be escaped by a backslash.
The options are as follows:
-0, --null
Change xargs to expect NUL (``\0'') characters as separators,
instead of spaces and newlines. This is expected to be used in
concert with the -print0 function in find(1).
-E eofstr
Use eofstr as a logical EOF marker.
-I replstr
Execute utility for each input line, replacing one or more
occurrences of replstr in up to replacements (or 5 if no -R flag
is specified) arguments to utility with the entire line of input.
The resulting arguments, after replacement is done, will not be
allowed to grow beyond replsize (or 255 if no -S flag is
specified) bytes; this is implemented by concatenating as much of
the argument containing replstr as possible, to the constructed
arguments to utility, up to replsize bytes. The size limit does
not apply to arguments to utility which do not contain replstr,
and furthermore, no replacement will be done on utility itself.
Implies -x.
说明里第一句是「Execute utility for each input line」,行是单位,不是参数。占位符 {} 在命令行里出现几次、命令就起几次,跟 -n 的合并行为完全不同。
== 4. -I{} 占位符:每个参数起一次命令 ==
xargs -0 -I{} sh -c 'echo 一次调用' 起进程次数: 4
四个文件起四次,命令里的 {} 被文件名原样替换。写循环体简单,代价是每次都新建进程,四千个文件各跑一次空命令,和每 200 个文件跑一次,耗时不在一个量级。
== 6. 造 4000 个文件,比 -I{} 与 -n200 的耗时 ==
文件数: 4000
-- -I{} 每个文件一次 --
耗时: 29s(shell 内建 SECONDS,整秒)
-- -n200 每 200 个一次 --
耗时: 0s
八秒对不足一秒,差距全在进程创建上。四千次 fork 的代价,-n200 用二十次就摊掉了。
-r 在本机是个空开关
-r, --no-run-if-empty
Compatibility with GNU xargs. The GNU version of xargs runs the
utility argument at least once, even if xargs input is empty, and
it supports a -r option to inhibit this behavior. The FreeBSD
version of xargs does not run the utility argument on empty
input, but it supports the -r option for command-line
compatibility with GNU xargs, but the -r option does nothing in
the FreeBSD version of xargs.
文档写得很直白:GNU 版在没有输入时也会跑一次命令,-r 是给它用的;FreeBSD 版本来就不会在空输入时执行,所以这个开关「does nothing」。
== 7. 空输入:本机 xargs 认不认 -r ==
空输入(不带 -r)退出码: 0
空输入(带 -r)退出码: 0
本机 xargs 认 -r(这版 BSD xargs 已支持,旧版不认)
两条命令退出码都是 0,加不加 -r 看不出区别。放在脚本里没有行为变化,留着只是让同一份脚本在 Linux 上也不出错。
换行和引号让默认写法直接报错
Spaces, tabs and newlines may be embedded in arguments using single
(`` ' '') or double (``"'') quotes or backslashes (``\''). Single quotes
escape all non-single quote characters, excluding newlines, up to the
matching single quote. Double quotes escape all non-double quote
characters, excluding newlines, up to the matching double quote. Any
single character, including newlines, may be escaped by a backslash.
这段说的是默认模式会自己解析引号和反斜杠,把它们当成分隔符的转义。反过来看,文件名里原本就带引号时,find 的裸输出没有加转义,xargs 看到的是一段不闭合的引号。
== 8. 换行、引号、反斜杠:默认模式的另一类错 ==
造出的文件个数(按 NUL 计数): 4
find 不带 -print0 的行数(换行被当成两行): 5
-- 默认写法:stdout 与 stderr 分开看 --
find | xargs 退出码: 1
stdout 行数: 0
stderr 全文:
xargs: unterminated quote
-- -0 写法 --
find -print0 | xargs -0 -n1 echo 输出行数: 5
四个文件按 NUL 数,find 不带 -print0 数出来是 5 行,其中一个名字里带了换行。默认写法直接以 unterminated quote 退出,退出码 1,stdout 一行都没有。-0 那边给了 5 行,名字里的换行只是让这一条占了两行。
这里最值得记的是失败方式:带空格的目录只是少删文件,带引号的目录是整个批处理空转。两者都不会留下明显的报错,除非去看 stderr。
落到写法上
find 的输出要交给 xargs,一律 -print0 配 -0,没有例外,代价只是多打两个字符。批量处理用 -n,把参数合并到一次调用里,数字按命令行长度上限倒推。需要单条命令里带参数顺序或者多个占位符时才用 -I,它换来的是灵活,付出去的是进程数。
我不建议在清理类脚本里用 find | xargs rm 这种默认写法,空格只是常见的一种,引号会让整批直接不执行。这次本机造了带换行和带引号的文件名去试,得到上面那组结果;如果目录里还有别的特殊字节,结论应该是一样的方向。