首先用Python写一个命令行参数显示工具,命名为showargv.py,内容如下:

#!/usr/bin/python

import sys

i = 0
for arg in sys.argv:
    print('argv[{}]:{}'.format(i, arg))
    i += 1

然后建立两个实验对象,一个命名为original.txt,内容如下:

/tmp/normal
/tmp/blank  line

/tmp/apos
/tmp/quot
/tmp/backslash

另一个命名为special.txt,内容如下:

/tmp/normal
/tmp/blank  line

/tmp/ap'os
/tmp/qu"ot
/tmp/back\slash

区别是special.txt含有引号和反斜线,而original.txt不含。

我们先拿original.txt做实验:

cat original.txt | xargs /tmp/showargv.py

argv[0]:/tmp/showargv.py
argv[1]:/tmp/normal
argv[2]:/tmp/blank
argv[3]:line
argv[4]:/tmp/apos
argv[5]:/tmp/quot
argv[6]:/tmp/backslash

可以看到空行被无视,空格和换行符作为分隔符(多个空格看作一个),showargv.py只调用了一次。

cat original.txt | xargs -n 1 /tmp/showargv.py

argv[0]:/tmp/showargv.py
argv[1]:/tmp/normal
argv[0]:/tmp/showargv.py
argv[1]:/tmp/blank
argv[0]:/tmp/showargv.py
argv[1]:line
argv[0]:/tmp/showargv.py
argv[1]:/tmp/apos
argv[0]:/tmp/showargv.py
argv[1]:/tmp/quot
argv[0]:/tmp/showargv.py
argv[1]:/tmp/backslash

可以看到-n的作用是指定每次执行时的最大参数个数,相当于把上面那一坨拆成一个一个执行。

cat original.txt | xargs -d '\n' /tmp/showargv.py

argv[0]:/tmp/showargv.py
argv[1]:/tmp/normal
argv[2]:/tmp/blank  line
argv[3]:
argv[4]:
argv[5]:/tmp/apos
argv[6]:/tmp/quot
argv[7]:/tmp/backslash

可以看到-d选项指定分隔符的效果。此时整个输入文件以-d所指定的分隔符分成若干参数。因为没有指定-n,showargv.py只调用一次。

cat original.txt | xargs -L 1 /tmp/showargv.py

argv[0]:/tmp/showargv.py
argv[1]:/tmp/normal
argv[0]:/tmp/showargv.py
argv[1]:/tmp/blank
argv[2]:line
argv[0]:/tmp/showargv.py
argv[1]:/tmp/apos
argv[0]:/tmp/showargv.py
argv[1]:/tmp/quot
argv[0]:/tmp/showargv.py
argv[1]:/tmp/backslash

可以看到-L的效果是指定每次最多执行几行(这里是1;空行被无视)。行尾的空格意味着连接到下一行(这里没有实验,请自行验证)。

cat original.txt | xargs -I {} /tmp/showargv.py {}

argv[0]:/tmp/showargv.py
argv[1]:/tmp/normal
argv[0]:/tmp/showargv.py
argv[1]:/tmp/blank  line
argv[0]:/tmp/showargv.py
argv[1]:/tmp/apos
argv[0]:/tmp/showargv.py
argv[1]:/tmp/quot
argv[0]:/tmp/showargv.py
argv[1]:/tmp/backslash

注意-I与-L 1的区别。-I蕴含了-L 1,但-I还有一个作用是取消空格的终止作用,所以_blank line_被看成了一个参数。

然后我们用special.txt来试试看:

cat special.txt | xargs /tmp/showargv.py

xargs: unmatched single quote; by default quotes are special to xargs unless you use the -0 option
argv[0]:/tmp/showargv.py
argv[1]:/tmp/normal
argv[2]:/tmp/blank
argv[3]:line

出错了!有个没配对的单引号。什么选项都不加的时候,引号和反斜线是有特殊含义的,就像bash里一样。

cat special.txt | xargs -d '\n' /tmp/showargv.py

argv[0]:/tmp/showargv.py
argv[1]:/tmp/normal
argv[2]:/tmp/blank  line
argv[3]:
argv[4]:
argv[5]:/tmp/ap'os
argv[6]:/tmp/qu"ot
argv[7]:/tmp/back\slash

加上-d之后就好了。这是因为- d还有个作用是禁止引号和反斜线的特殊含义,也就是说现在引号和反斜线都是普通字符了(literally)。同理,-0也有这个作用。

可是-d还有一个副作用:空行不再被忽略了。这个暂时还没有办法,只能事先sed过滤一下了。

cat special.txt | xargs -I {} /tmp/showargv.py {}

argv[0]:/tmp/showargv.py
argv[1]:/tmp/normal
xargs: unmatched single quote; by default quotes are special to xargs unless you use the -0 option
argv[0]:/tmp/showargv.py
argv[1]:/tmp/blank  line

而-I在遇到引号的时候可耻地失败了,可见-I并没有禁止字符特殊含义的作用。

总结:

  • 先用sed ‘/^$/d’过滤空行。
  • 如果要使用默认分隔符且所有输入作为一组参数传入,什么也不用加。
  • 如果要按行作为一组参数传入,用-d ‘\n’。
  • 如果要一行一行执行,且每行中的输入作为一组参数传入(默认分隔符),用-L 1(注意行尾不能有空白符,否则会和下行合并)。
  • 如果要一行一行执行,且每行作为一个参数传入,用-d ‘\n’ -L 1(在-d存在的情况下-L表示-d分开的逻辑行,且行尾空白符不再具有合并下行的作用)。也可以用-d ‘\n’ -n 1,看上去更容易理解。
  • 如果在输入中引号和反斜线表示转义,又想使用-d的断行功能,可以先用管道 xargs -L 1 脱去转义。

于是感觉xargs的设计还是有点乱:

  • 缺少一个决定输入中的引号和反斜线是否表示转义的选项。这个选项嵌在-d和-0里副作用太大了。
  • 在用-d指定分隔符后-L的含义就不清晰了,不再表示输入中的物理行了。
  • -L那个行尾空白符合并下行的功能感觉没啥用,特别是批量输出数据的时候很正常地每行最后一个数据也会加空格分开。如果真要表示合并下行也应该用反斜线吧,符合惯例。

拿上面那个special.txt来说。假设每个非空行表示一个文件,引号和反斜线表示字面意义,而我想对每个文件执行execute 12345。一般来说我们需要用到-I选项,因为不能保证文件里的输入总是最后一个参数(例如这里)。那么命令行应该是:

cat special.txt | sed '/^$/d' | xargs -d '\n' -I {} execute {} 12345

所以……我真是想重写xargs啊!但是重写了就未必兼容POSIX了,所以这活还是应该由GNU来做吧?我先用-0将就一下好了,反正绝大多数时间都是和find在一起用的。