也许你觉得流和管道让Linux专家看起来像管道工人一样,那么让我们来看看究竟,并且学习一下重定向和多路输出。还会学到把一个流作为命令的参数。
在本文里,我们将继续使用在上一篇文章中创建的文件做实验,即便你没学过上篇文章或者没有当时没有保存那些文件也不要紧,我们重新建立它们。首先在家目录下创建一个名为lpi103-4的目录,然后在这个目录下创建所需的文件。这些可以通过如下命令来完成。
[root@localhost ~]# mkdir -p lpi103-4 && cd lpi103-4 && {
> echo -e "1 apple\n2 pear\n3 banana" > text1
> echo -e "9\tplum\n3\tbanana\n10\tapple" > text2
> echo "This is a sentence. " !#:* !#:1->text3
echo "This is a sentence. " "This is a sentence. " "This is a sentence. ">text3
> split -l 2 text1
> split -b 17 text2 y; }
执行完成后的目录结构如下:
[root@localhost lpi103-4]# ll
total 28
-rw-r--r-- 1 root root 24 Jun 26 14:21 text1
-rw-r--r-- 1 root root 25 Jun 26 14:21 text2
-rw-r--r-- 1 root root 63 Jun 26 14:21 text3
-rw-r--r-- 1 root root 15 Jun 26 14:21 xaa
-rw-r--r-- 1 root root 9 Jun 26 14:21 xab
-rw-r--r-- 1 root root 17 Jun 26 14:21 yaa
-rw-r--r-- 1 root root 8 Jun 26 14:21 yab
像bash这样的Linux shell程序,无论接受输入还是发送输出都是按照字符序列(字符流)来进行的。每一个字符都与前后相邻的其他字符彼此独立。这些字符不会组成结构化的记录,也不会是固定大小的块。无论真正的字符流来自或者流向一个文件、键盘、显示的窗口还是其他的设备,都可以统一通过文件IO技术来访问。Linux shell程序使用三个标准IO流,每一个都用一个众所周知的文件描述符来指定:
上面的n代表文件描述符,如果没有给出,那么其默认值就是1,也就是标准输出。下面是一些例子:
[root@localhost lpi103-4]# ls x* z*
ls: cannot access z*: No such file or directory
xaa xab
[root@localhost lpi103-4]# ls x* z* >stdout.txt 2>stderr.txt
[root@localhost lpi103-4]# ls w* y*
ls: cannot access w*: No such file or directory
yaa yab
[root@localhost lpi103-4]# ls w* y* >>stdout.txt 2>>stderr.txt
[root@localhost lpi103-4]# cat stdout.txt
xaa
xab
yaa
yab
[root@localhost lpi103-4]# cat stderr.txt
ls: cannot access z*: No such file or directory
ls: cannot access w*: No such file or directory
我们说过使用n>来进行重定向会覆盖掉原来的内容。然而你可以通过set -o noclobber来改变这种行为,如果这么做了,可以使用n>|来覆盖。
[root@localhost lpi103-4]# set -o noclobber
[root@localhost lpi103-4]# ls x* z* >stdout.txt 2>stderr.txt
bash: stdout.txt: cannot overwrite existing file
[root@localhost lpi103-4]# ls x* z* >|stdout.txt 2>|stderr.txt
[root@localhost lpi103-4]# cat stdout.txt
xaa
xab
[root@localhost lpi103-4]# cat stderr.txt
ls: cannot access z*: No such file or directory
[root@localhost lpi103-4]# set +o noclobber #恢复原来的设置
有时候你可能想要把stdout和stderr重定向到同一个文件,这种情况包括自动处理或者后台工作,目的是可以在以后再查看输出。这可以通过&>或&>>来完成。另一种可行的方法是先重定向n到m,然后把m重定向到文件,使用的命令是 m>&n或者m&>>n。重定向的顺序非常重要,如:
command 2>&1 >output.txt
与
commnd >output.txt 2>&1
是不一样的。
第一个例子中,stderr被重定向到了stdout,然后stdout被重定向到了文件output.txt,但是第二次重定向知识影响了stdout,并没有影响stderr。
第二个例子中,stderr被重定向到了stdout,而此时的stdout已经重定向到了output.txt,所以stderr也就重定向到了output.txt。
[root@localhost lpi103-4]# ls x* z* &>output.txt
[root@localhost lpi103-4]# cat output.txt
ls: cannot access z*: No such file or directory
xaa
xab
[root@localhost lpi103-4]# ls x* z* >output.txt 2>&1
[root@localhost lpi103-4]# cat output.txt
ls: cannot access z*: No such file or directory
xaa
xab
[root@localhost lpi103-4]# ls x* z* 2>&1 >output.txt # stderr 没有定向到output.txt
ls: cannot access z*: No such file or directory
[root@localhost lpi103-4]# cat output.txt
xaa
xab
有些时候,你可能想把stdout或stderr忽略掉。这可以通过重定向到特殊的/dev/null文件来完成。如下:
[root@localhost lpi103-4]# ls x* z* 2>/dev/null
xaa xab
[root@localhost lpi103-4]# cat /dev/null
与我们可以重定向stdout和stderr一样,我们可以通过<操作符来重定向stdin。前面文章中我们使用cat file | command 的形式来把file作为command的输入来源,这不是必须的,我们可以通过重定向stdin来实现同样的效果。如下:
[root@localhost lpi103-4]# tr ' ' '\t'
2 pear
3 banana
包括bash在内的shell程序们还有一个here-document的概念,这是另外一种形式的重定向。它的格式是<<紧接一个单词(如"END"),这个END的作用是作为输入的终结标志,如下:
[root@localhost lpi103-4]# sort -k2 <
> 2 pear
> 3 banana
> END
1 apple
3 banana
2 pear
你可能想知道是否可以不输入END,而是通过Ctrl+d来结束输入。答案是肯定的,但是这在脚本文件里行不通,因为脚本文件里不能输入Ctrl+d。因为shell脚本为大量使用了tab来缩进代码,出现了针对这种情况的一种here-document变体---- <<-,此时每行开头的tab将被忽略。
下面例子中,我们创建了一个脚本文件,文件中使用了<<-。如下:
[root@localhost lpi103-4]# ht=$(echo -en "\t")
[root@localhost lpi103-4]# cat<
> cat <<-EOF
> apple
> EOF
> ${ht}cat <<-EOF
> ${ht}pear
> ${ht}EOF
> END
[root@localhost lpi103-4]# cat ex-here.sh
cat <<-EOF
apple
EOF
cat <<-EOF
pear
EOF
[root@localhost lpi103-4]# bas
base64 basename bash bashbug-32
[root@localhost lpi103-4]# bash ex-here.sh
apple
pear
前面我们在“文本流和过滤器”中说过,可以使用管道来连接多个文本处理命令。但是管道并不局限于文本流,尽管在文本流中经常使用。
使用管道操作符|可以把第一个命令的stdout定向到第二个命令的stdin。更多地命令和管道可以组成更长的管线。管线中的每一个命令都可以有参数,很多命令使用一个单独的-代表输入文件是stdin。伪代码如下:
command1 | command2 parameter1 | command3 parameter1 - parameter2 | command4
需要注意的是,管道只是把stdout定向到了stdin。你不能使用2| 把stderr也加入管道中。如果stderr已经被重定向到了stdout,那么所有的输出流就被管道处理了。例子如下:
[root@localhost lpi103-4]# ls y* x* z* u* q*
ls: cannot access z*: No such file or directory
ls: cannot access u*: No such file or directory
ls: cannot access q*: No such file or directory
xaa xab yaa yab
[root@localhost lpi103-4]# ls y* x* z* u* q* 2>&1 | sort
ls: cannot access q*: No such file or directory
ls: cannot access u*: No such file or directory
ls: cannot access z*: No such file or directory
xaa
xab
yaa
yab
Linux或者Unix系统上的管道的一个优点是,管道处理过程中并没有临时文件生成。第一个命令的stout不会写入一个文件,然后第二个命令读取这个文件。前面的文章中提到过使用tar 来一步完成打包和压缩,如果你工作的Unix系统中的tar恰好不支持-z或者-j等压缩选项,这也没有问题,使用管道可以轻松搞定,如下:
bunzip -c somefile.tar.bz2 | tar -xvf -
上面的例子中,管线开始于一个产生输出的命令。当然也可以开始于一个已经存在的文件,使用<把第一个命令的输入重定向到这个文件即可。(译者注:从文件开始也符合过滤器的哲学,就是过滤器只处理数据而不产生数据)。
在前面的管道的讨论中,你学会了如何利用一个命令的输出作为另一个命令的输入。假设你想要一个命令的输出或者是一个文件的内容作为一个命令的参数而不是输入呢?管道无法做到这个,解决方法是:
如果输入中的空白被单引号或双引号包围,或者被反斜线转义,那么xargs就不会分隔输入。如下:
[root@localhost lpi103-4]# echo '"4 plum"' | cat text1 -
1 apple
2 pear
3 banana
"4 plum"
[root@localhost lpi103-4]# echo '"4 plum"' | cat text1 - | xargs -n 1
1
apple
2
pear
3
banana
4 plum
目前为止,所有的参数都被加到了命令的后面。如果你需要在其他的参数中使用它们,那么可以使用-I选项。如下:
[root@localhost lpi103-4]# xargs -I XYZ echo "START XYZ REPEAT XYZ END"
START 2 pear REPEAT 2 pear END
START 3 banana REPEAT 3 banana END
[root@localhost lpi103-4]# xargs -IX echo "
<2 pear><2 pear>
<3 banana><3 banana>
[root@localhost lpi103-4]#
[root@localhost lpi103-4]# cat text1 text2 | xargs -L2
1 apple 2 pear
3 banana 9 plum
3 banana 10 apple
其中 -L选项告诉xargs把每一行当作一个参数。
尽管例子中我们使用了一个简单的文本文件来演示,实际中,你很少会这么使用,通常使用的输入是来自ls,grep等命令的输出。如下:
[root@localhost lpi103-4]# ls | xargs grep "1"
text1:1 apple
text2:10 apple
xaa:1 apple
yaa:1
如果上面例子中一个或者多个文件名包含空格会如何呢?答案是会产生错误。
对于ls命令,你可以使用--quoting-style选项来强制为文件名增加引号或转义。一个更好的解决方案是使用xargs的-0选项,这样xargs就使用\0来分隔输入的参数。尽管ls不支持选项来产生\0结尾的文件名输出,很多其他的命令都支持。
下面的例子中展示了这些技巧。
[root@localhost lpi103-4]# cp text1 "text 1"
[root@localhost lpi103-4]# ls *1 | xargs grep "1" # error
text1:1 apple
grep: text: No such file or directory
grep: 1: No such file or directory
[root@localhost lpi103-4]# ls --quoting-style escape *1
text1 text\ 1
[root@localhost lpi103-4]# ls --quoting-style shell *1
text1 'text 1'
[root@localhost lpi103-4]# ls --quoting-style shell *1 | xargs grep "1"
text1:1 apple
text 1:1 apple
[root@localhost lpi103-4]# ls *1 | tr '\n' '\0' | xargs -0 grep "1"
text1:1 apple
text 1:1 apple
xargs不能构建任意长度的命令。直到Linux 内核的 2.26.3版本中,命令的最大数量也是有限的。像rm somepath/*,如果是一个有很多长文件名的文件的目录,那么这可能会失败,并告知参数太长。在一些老的Linux版本或者Unix系统中,这个限制可能仍然会存在。
可以使用xargs的--show-limits选项来显示默认的限制,使用-s选项来设置这个限制的大小。
在“文件与目录管理”中,我们学会了如何根据文件名、修改时间、文件大小、或者其他的文件属性使用find来查找文件。一旦你得到了这样一组文件,你通常会对他们做些什么:删除它们、复制、重命名、或者其他操作。现在让我们来看看find的-exec选项,它具有和xargs类似的功能。
[root@localhost lpi103-4]# find text[12] -exec cat text3 {} \;
This is a sentence. This is a sentence. This is a sentence.
1 apple
2 pear
3 banana
This is a sentence. This is a sentence. This is a sentence.
9 plum
3 banana
10 apple
把它与xargs相比,你会发现很多不同点: