菜鸟笔记
提升您的技术认知

你真的了解xargs命令吗

shell里的xargs命令大家都很熟悉,最常见的使用场景就是文件内查找文本

find . -type f -name "*.c" | xargs grep abc

这行命令首先通过find程序在当前目录(含子目录)下查找所有.c文件,然后运行grep程序在所有文件内容里搜索文本"abc"。

A:就是这么简单吗?

B:是啊,很简单,要不然无名师怎么会对来访的程序员说“Unix传统上认为一行shell脚本胜过万行c程序"

A:。。。。。。

可是这总让人觉得有点不对劲,命令本身没错,到底该怎么去理解这行命令的工作原理呢?a|b 这很好理解,是把前一个命令a的标准输出与后一个命令b的标准输入连接起来,那现在find的标准输出到底是给了xargs还是给了grep,如果是给了xargs,那xargs又是如何把这串文件名传递给grep的?每一个文件都调用了一次grep的话那有几万个文件,grep程序启动了几万次吗?

逐步分析,首先一个单独find命令输出是换行符分割的文件名,像下面这样



如果find后面加一个管道 | 则find的所有输出就作为后面命令的标准输入了,这么说来,xargs命令收到的是一堆的文件名,注意是作为标准输入而不是启动参数,那xargs后续又是怎么处理的呢?先看下文档

man xargs

在DESCRIPTION节里有如下一段说明


xargs reads items from the standard input, delimited by blanks (which can be protected with double or single quotes or a backslash) or newlines, and executes the command (default is /bin/echo)one or more times with any initial-arguments followed by items read from standard input.  Blank lines on the standard input are ignored.

文档说xargs就是设计用来读取标准输入的,并且把读取的标准输入的内容作为参数,启动后面的命令一次或多次。一次或多次?几万次算不算多次?文档没有给出明确的解释,看来只有我们自己试验了。

现在要构造一个简单的命令show替换掉grep,这个命令每次被调用都会输出信息,输出内容是传递给这个命令的参数的个数,这样我们就可以通过运行下面的程序来观察show被调用了多少次以及每次传递了多少参数给它。注意要把show文件放到环境变量的PATH里面,保证任何地方都能使用这个命令

运行命令:

find . -type f -name "*.c" | xargs show

show:

#!/bin/bash
echo $#

结果揭晓,每次调用的参数个数都不同,从2000多到4000多看似无规律,show没有被调用几万次,但也不止调用了一次。

根据试验结果,一个合理的猜测是xargs并不对参数个数进行限制,而是对参数的总长度进行了限制,当参数长度超过某个固定大小时,xargs会第二次启动后续命令(这里是show)如此反复直到所有的输入都被处理完,这也可以解释最后剩下的503个参数明显少于之前的个数,因为就剩下这么多了。
通过修改show命令来验证我们的猜测,show修改为下面的样子,这个命令可以统计出每次传入参数的个数和字符总长度

#!/bin/bash
echo $@ | wc

结果在下面这里,猜测被证明是正确的

每次传递过来的参数的总长度都在131012~131067之间,浮动很小,这是因为在截断文件名时,必须保证最后一个文件名的完整性,也就是说不能把一个文件名从中间截成两半,所以长度有细微差别。

所以结论已经出来了:xargs有一个固定大小的参数缓冲区。当参数长度小于缓冲区时,只调用一次后面的命令,将标准输入读取的内容作为参数传递给后续命令来运行它,当参数大于缓冲区长度时,就把满足一个缓冲区长的参数送给后面的命令启动(结尾会保证不截断),反复直到所有的输入都被读取完毕。

最后注意一点,如果已经为xargs调用的命令提供了一个参数(就像之前的abc),那xargs会将自己读取的内容放到书写的参数之后,作为第2-n个参数出现,也就是说按照之前的调用方式使用find/grep

find . -type f -name "*.c" | xargs grep abc

后半段实际是

grep abc file1.c file2.c file3.c .....

如果我想把xargs的参数放到abc的前面,让abc作为最后一个参数,该怎么做呢?man xargs里有解释,可以这么做

find ... | xargs -i grep {} abc

-i参数后使用符号{}来表示输出的参数,在命令后的合适位置写上这个{}即可。

花费时间探究xargs的工作原理,除了满足好奇心,对我们的工作有实际意义吗?

答案当然是肯定的,我们如果要提供程序给别人使用,要尽量让自己的程序更符合Unix风格,具备读取多个启动参数的能力,同时也具备从管道读取参数的能力,这种符合Unix风格的命令别人使用起来会非常方便,甚至组合出你在设计时都没想到的功能。就像最普通的grep程序那样

find . -type f -name "*.c" | xargs grep abc //从启动参数读取,过滤文件内容
find . -type f -name "*.c" | grep abc       //从管道读取,过滤文件名