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字符的字符串。我们可以用-dX作为输入定界符。在这里,我们明确指定X作为输入定界符,而在默认情况下,xargs采用内部字段分隔符(空格)作为输入定界符。

结合-n选项,我们可以将输入划分成多行,而每行包含两个参数:

$ echo "splitXsplitXsplitXsplit" | xargs -d X -n 2
split split
split split

2.5.4 补充内容

我们已经从上面的例子中学到了如何将stdin格式化成不同的输出形式以作为参数。现在让我们来学习如何将这些参数传递给命令。

  1. 读取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 ./cecho.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扩展时会被替换掉。如果将-Ixargs结合使用,对于每一个参数,命令都会被执行一次。

    试试下面的用法:

    $ cat args.txt | xargs -I {} ./cecho.sh -p {} -l
    -p arg1 -l #
    -p arg2 -l #
    -p arg3 -l #
    

    -I {}指定了替换字符串。对于每一个命令参数,字符串{}都会被从stdin读取到的参数替换掉。

    使用-I的时候,命令以循环的方式执行。如果有3个参数,那么命令就会连同{}一起被执行3次。在每一次执行中{}都会被替换为相应的参数。

  2. 结合find使用xargs

    xargsfind算是一对死党。两者结合使用可以让任务变得更轻松。不过人们通常却是以一种错误的组合方式使用它们。例如:

    $ find . -type f -name "*.txt"  -print | xargs rm -f
    

    这样做很危险。有时可能会删除不必要删除的文件。我们没法预测分隔find命令输出结果的定界符究竟是什么('\n'或者' ')。很多文件名中都可能会包含空格符(' '),因此xargs很可能会误认为它们是定界符(例如,hell text.txt会被xargs误解为hell和text.txt)。

    只要我们把find的输出作为xargs的输入,就必须将-print0find结合使用,以字符null('\0')来分隔输出。

    find匹配并列出所有的 .txt文件,然后用xargs将这些文件删除:

    $ find . -type f -name "*.txt" -print0 | xargs -0 rm -f
    

    这样就可以删除所有的.txt文件。xargs -0\0作为输入定界符。

  3. 统计源代码目录中所有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/处下载。

  4. 结合stdin,巧妙运用while语句和子shell

    xargs只能以有限的几种方式来提供参数,而且它也不能为多组命令提供参数。要执行包含来自标准输入的多个参数的命令,有一种非常灵活的方法。包含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
    

    如果cmd1cd /,那么就会改变子shell工作目录,然而这种改变仅局限于子shell内部。cmd4则完全不知道工作目录发生了变化。