2.5 玩转xargs
我们可以用管道将一个命令的stdout
(标准输出)重定向到另一个命令的stdin
(标准输入)。例如:
- cat foo.txt | grep "test"
但是,有些命令只能以命令行参数的形式接受数据,而无法通过stdin
接受数据流。在这种情况下,我们没法用管道来提供那些只有通过命令行参数才能提供的数据。
那就只能另辟蹊径了。xargs
是一个很有用的命令,它擅长将标准输入数据转换成命令行参数。xargs
能够处理stdin
并将其转换为特定命令的命令行参数。xargs
也可以将单行或多行文本输入转换成其他格式,例如单行变多行或是多行变单行。
Bash黑客都喜欢单行命令。单行命令是一个命令序列,各命令之间不使用分号,而是使用管道操作符进行连接。精心编写的单行命令可以更高效、更简捷地完成任务。就文本处理而言,需要具备扎实的理论和实践才能够写出适合的单行命令解决方法。xargs
就是构建单行命令的重要组件之一。
2.5.1 预备知识
xargs
命令应该紧跟在管道操作符之后。它以标准输入作为主要的源数据流,并使用stdin
并通过提供命令行参数来执行其他命令。例如:
command | xargs
2.5.2 实战演练
xargs
命令把从stdin
接收到的数据重新格式化,再将其作为参数提供给其他命令。
xargs
可以作为一种替换方式,作用类似于find
命令中的 -exec
参数。下面介绍一些借助xargs
命令能够实现的技巧。
- 将多行输入转换成单行输出
只需要将换行符移除,再用" "
(空格)进行代替,就可以实现多行输入的转换。'\n'
被解释成一个换行符,换行符其实就是多行文本之间的定界符。利用xargs
,我们可以用空格替换掉换行符,这样一来,就能够将多行文本转换成单行文本:
- $ cat example.txt # 样例文件
- 1 2 3 4 5 6
- 7 8 9 10
- 11 12
- $ cat example.txt | xargs
- 1 2 3 4 5 6 7 8 9 10 11 12
- 将单行输入转换成多行输出
指定每行最大的参数数量n,我们可以将任何来自stdin
的文本划分成多行,每行n个参数。每一个参数都是由" "
(空格)隔开的字符串。空格是默认的定界符。按照下面的方法可以将单行划分成多行:
- $ cat example.txt | xargs -n 3
- 1 2 3
- 4 5 6
- 7 8 9
- 10 11 12
2.5.3 工作原理
xargs
命令拥有数量众多且用法简单的选项,这使得它很适用于某些问题场合。让我们来看看如何能够巧妙地用这些选项来解决问题。
我们可以用自己的定界符来分隔参数。用 -d
选项为输入指定一个定制的定界符:
- $ echo "splitXsplitXsplitXsplit" | xargs -d X
- split split split split
在上面的代码中,stdin
是一个包含了多个X
字符的字符串。我们可以用 -d
将X
作为输入定界符。在这里,我们明确指定X
作为输入定界符,而在默认情况下,xargs
采用内部字段分隔符(IFS)作为输入定界符。
同时结合-n
,我们可以将输入划分成多行,而每行包含两个参数:
- $ echo "splitXsplitXsplitXsplit" | xargs -d X -n 2
- split split
- split split
2.5.4 补充内容
我们已经从上面的例子中学到了如何将stdin
格式化成不同的输出形式以作为参数。现在让我们来学习如何将这些参数传递给命令。
- 读取
stdin
,将格式化参数传递给命令
编写一个小型的定制版echo
来更好地理解用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
就第一个问题,我们可以将这个命令执行多次,每次使用一个参数:
- $ cat args.txt | xargs -n 1 ./cecho.sh
- arg1 #
- arg2 #
- arg3 #
每次执行需要X
个参数的命令时,使用:
INPUT | xargs -n X
例如:
- $ cat args.txt | xargs -n 2 ./cecho.sh
- arg1 arg2 #
- arg3 #
就第二个问题,我们可以执行这个命令,并一次性提供所有的参数:
- $ cat args.txt | xargs ./ccat.sh
- arg1 arg2 arg3 #
在上面的例子中,我们直接为特定的命令(例如cecho.sh)提供命令行参数。这些参数都只源于args.txt文件。但实际上除了它们外,我们还需要一些固定不变的命令参数。思考下面这种命令格式:
./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
,可以提供上面这种形式的命令执行序列。我们可以用-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的时候,命令就似乎是在一个循环中执行一样。如果有三个参数,那么命令就会连同{}
一起被执行三次,而{}
在每一次执行中都会被替换为相应的参数。
- 结合
find
使用xargs
xargs
和find
算是一对死党。两者结合使用可以让任务变得更轻松。不过,人们通常却是以一种错误的组合方式使用它们。例如:
- $ find . -type f -name "*.txt" -print | xargs rm -f
这样做很危险。有时可能会删除不必要删除的文件。我们没法预测分隔find
命令输出结果的定界符究竟是'\n'
还是' '
。很多文件名中都可能会包含空格符,而xargs
很可能会误认为它们是定界符(例如,hell text.txt会被xargs
误认为hell和text.txt)。
只要我们把find
的输出作为xargs
的输入,就必须将 -print0
与find
结合使用,以字符null来分隔输出。
用find
匹配并列出所有的 .txt文件,然后用xargs
将这些文件删除:
- $ find . -type f -name "*.txt" -print0 | xargs -0 rm -f
这样就可以删除所有的.txt文件。xargs -0
将\0
作为输入定界符。
- 统计源代码目录中所有C程序文件的行数
统计所有C程序文件的行数是大多数程序员都会遇到的活儿。完成这项任务的代码如下:
- $ find source_code_dir_path -type f -name "*.c" -print0 | xargs -0 wc -l
- 结合
stdin
,巧妙运用while
语句和子shell
xargs
只能以有限的几种方式来提供参数,而且它也不能为多组命令提供参数。要执行一些包含来自标准输入的多个参数的命令,可采用一种非常灵活的方法。我将这个方法称为子shell妙招(subshell hack)。一个包含while
循环的子shell可以用来读取参数,并通过一种巧妙的方式执行命令:
- $ cat files.txt | ( while read arg; do cat $arg; done )
- # 等同于 cat files.txt | xargs -I {} cat {}
在while
循环中,可以将cat $arg
替换成任意数量的命令,这样我们就可以对同一个参数执行多项命令。我们也可以不借助管道,将输出传递给其他命令。这个技巧能适用于各种问题环境。子shell内部的多个命令可作为一个整体来运行。
$ cmd0 | ( cmd1;cmd2;cmd3) | cmd4
如果cmd1
是cd /
,那么就会改变子shell工作目录,然而这种改变仅局限于子shell内部。cmd4
则完全不知道工作目录发生了变化。