Shell 玩转 xargs,Unix命令可以从标准输入(stdin
)或命令行参数中接收数据。之前的例子已经展示了如何利用管道将一个命令的标准输出传入到另一个命令的标准输入。
我们可以用别的方法来调用只能接受命令行参数的命令。最简单的方法就是使用反引号执行命令,然后将其输出作为命令行参数:
$ gcc `find '*.c'`
这种方法在很多情况下都管用,但是如果要处理的文件过多,你会看到一条可怕的错误信息:Argument list too long
。xargs
命令可以解决这个问题。
xargs
命令从stdin
处读取一系列参数,然后使用这些参数来执行指定命令。它能将单行或多行输入文本转换成其他格式,例如单行变多行或是多行变单行。
预备知识
xargs
命令应该紧跟在管道操作符之后。它使用标准输入作为主要的数据源,将从stdin
中读取的数据作为指定命令的参数并执行该命令。下面的命令将在一组C语言源码文件中搜索字符串main
:
ls *.c | xargs grep main
实战演练
zargs
命令重新格式化stdin
接收到的数据,再将其作为参数提供给指定命令。xargs
默认会执行echo
命令。和find
命令的-exec
选项相比,两者在很多方面都相似。
- 将多行输入转换成单行输出。
xargs
默认的echo
命令可以用来将多行输入转换成单行输出:
$ cat example.txt # 样例文件1 2 3 4 5 67 8 9 1011 12$ cat example.txt | xargs1 2 3 4 5 6 7 8 9 10 11 12
- 将单行输入转换成多行输出。xargs的
-n
选项可以限制每次调用命令时用到的参数个数。下面的命令将输入分割成多行,每行N个元素:
$ cat example.txt | xargs -n 31 2 34 5 67 8 910 11 12
工作原理
xargs
命令接受来自stdin
的输入,将数据解析成单个元素,然后调用指定命令并将这些元素作为该命令的参数。xargs
默认使用空白字符分割输入并执行/bin/echo
。
如果文件或目录名中包含空格(甚至是换行)的话,使用空白字符来分割输入就会出现问题。比如My Documents目录就会被解析成两个元素:My
和Documents
,而这两者均不存在。
天无绝人之路,这次也不例外。
我们可以定义一个用来分隔参数的分隔符。-d
选项可以为输入数据指定自定义的分隔符:
$ echo "splitXsplit2Xsplit3Xsplit4" | xargs -d X
Split1 split2 split3 split4
在上面的代码中,stdin
中是一个包含了多个X
字符的字符串。我们可以用–d
选项将X
定义为输入分隔符。
结合-n
选项,可以将输入分割成多行,每行包含两个单词:
$ echo "splitXsplitXsplitXsplit" | xargs -d X -n 2
split split
split split
xargs
命令可以同find
命令很好地结合在一起。find
的输出可以通过管道传给xargs
,由后者执行-exec
选项所无法处理的复杂操作。如果文件系统的有些文件名中包含空格,find
命令的-print0
选项可以使用0
(NULL)来分隔查找到的元素,然后再用xargs
对应的-0
选项进行解析。下面的例子在Samba挂载的文件系统中搜索.docx文件,这些文件名中通常会包含大写字母和空格。其中使用了grep
找出内容中不包含image的文件:
$ find /smbMount -iname '*.docx' -print0 | xargs -0 grep -L image
补充内容
上面的例子展示了如何使用xargs
组织数据。接下来将要学习如何在命令行中格式化数据。
- 读取
stdin
,为命令传入格式化参数
下面是一个短小的脚本cecho
,可以用来更好地理解xargs
是如何提供命令行参数的:
#!/bin/bash
#文件名: cecho.sh
echo $*'#'
当参数被传递给文件cecho.sh后,它会打印这些参数并以 #字符作为结尾。例如:
$ ./cecho.sh arg1 arg2
arg1 arg2 #
这里有一个常见的问题。
- 有一个包含着参数列表的文件(每行一个参数)要提供给某个命令(比如cecho.sh)。我需要以不同的形式来应用这些参数。在第一种形式中,每次调用提供一个参数。
./cecho.sh arg1
./cecho.sh arg2
./cecho.sh arg3
- 接下来,每次调用提供一到两个参数。
./cecho.sh arg1 arg2
./cecho.sh arg3
- 最后,在单次调用中提供所有参数。
./cecho.sh arg1 arg2 arg3
先别急着往下看,试着运行一下上面的命令,然后仔细观察输出结果。xargs命令可以格式化参数,满足各种需求。args.txt文件中包含一个参数列表:
$ cat args.txt
arg1
arg2
arg3
对于第一种形式,我们需要多次执行指定的命令,每次执行时传入一个参数。xargs
的-n
选项可以限制传入命令的参数个数:
$ cat args.txt | xargs -n 1 ./cecho.sh
arg1 #
arg2 #
arg3 #
如果要将参数限制为2个,可以这样:
$ cat args.txt | xargs -n 2 ./cecho.sh
arg1 arg2 #
arg3 #
最后,为了在执行命令时一次性提供所有的参数,选择不使用-n选项:
$ cat args.txt | xargs ./cecho.sh
arg1 arg2 arg3 #
在上面的例子中,由xargs
添加的参数都被放置在指定命令的尾部。但我们可能需要在命令末尾有一个固定的参数,并希望xargs
能够替换居于中间位置的参数,就像这样:
./cecho.sh -p arg1 -l
在命令执行过程中,arg1是唯一的可变内容,其余部分都保持不变。args.txt中的参数是像这样提供给命令的:
./cecho.sh -p arg1 -l
./cecho.sh -p arg2 -l
./cecho.sh -p arg3 -l
xargs
有一个选项-I
,可以用于指定替换字符串,这个字符串会在xargs
解析输入时被参数替换掉。如果将-I
与xargs
结合使用,对于每一个参数,指定命令只会执行一次。来看看解决方法:
$ cat args.txt | xargs -I {} ./cecho.sh -p {} -l
-p arg1 -l #
-p arg2 -l #
-p arg3 -l #
-I {}
指定了替换字符串。为该命令提供的各个参数会通过stdin
读取并依次替换掉字符串{}
。
使用
I
的时候,命令以循环的方式执行。如果有3个参数,那么命令就会连同{}
一起被执行3次。{}
会在每次执行中被替换为相应的参数。
- 结合
find
使用xargs
xargs
和find
可以配合完成任务。不过在结合使用的时候需要留心。考虑下面的例子:
$ find . -type f -name "*.txt" -print | xargs rm -f
这样做很危险,有可能会误删文件。我们无法预测find
命令输出的分隔符究竟是什么(究竟是'\n'
还是' '
)。如果有文件名中包含空格符(' '
),xargs
会将其误认为是分隔符。例如,bashrc text.txt会被视为bashrc和text.txt。因此上面的命令不会删除bashrc text.txt,而是会把bashrc删除。
使用find
命令的-print0
选项生成以空字符('\0'
)作为分隔符的输出,然后将其作为xargs
命令的输入。
下列命令会查找并删除所有的.txt文件:
$ find . -type f -name "*.txt" -print0 | xargs -0 rm -f
- 统计源代码目录中所有C程序文件的行数
大多数程序员在某一时刻都会统计自己的C程序文件的行数(Lines of Code,LOC)。完成这项任务的代码如下:
$ find source_code_dir_path -type f -name "*.c" -print0 | xargs -0 wc –l
如果你想获得更多有关源代码的统计信息,一个叫作SLOCCount的实用工具可以派上用场。现代GNU/Linux发行版一般都包含这个软件包,或者你也可以从http://www.dwheeler.com/sloccount/下载。
- 结合stdin,巧妙运用while语句和子shell
xargs
会将参数放置在指定命令的尾部,因此无法为多组命令提供参数。我们可以通过创建子shell来处理这种复杂情况。子shell利用while
循环读取参数并执行命令,就像这样:
$ cat files.txt | ( while read arg; do cat $arg; done )
# 等同于cat files.txt | xargs -I {} cat {}
在while
循环中,可以将cat $arg
替换成任意数量的命令,这样我们就可以对同一个参数执行多条命令。也可以不借助管道将输出传递给其他命令。这种利用()
创建子shell的技巧可以应用于各种问题场景。子shell操作符内部的多条命令在执行时就像一个整体,因此:
$ cmd0 | ( cmd1;cmd2;cmd3) | cmd4
如果cmd1是cd /
,那么就会改变子shell工作目录,然而这种改变仅局限于该子shell内部。cmd4
则不受工作目录变化的影响。
shell的-c
选项可以调用子shell来执行命令行脚本。它可以与xargs
结合解决多次替换的问题。下列命令找出了所有的C文件并显示出每个文件的名字,文件名前会加上一个换行符(-e
选项允许进行转义替换)。在文件名之后是该文件中含有main的所有行:
find . -name '*.c' | xargs -I ^ sh -c "echo -ne '\n ^: '; grep main ^"