内容


学习 Linux,101

流、管道和重定向

熟悉 Linux 管道

Comments

系列内容:

此内容是该系列 # 部分中的第 # 部分: 学习 Linux,101

敬请期待该系列的后续内容。

此内容是该系列的一部分:学习 Linux,101

敬请期待该系列的后续内容。

概述

本文帮助您巩固重定向标准 I/O 流的基础 Linux 技术。您将学习如何:

  • 重定向标准 I/O 流:标准输出和标准错误
  • 通过管道将一个命令的输出导入到另一个命令的输入
  • 将输出发送到 stdout 和文件中
  • 将命令输出用作另一个命令的参数

本文帮助您准备 Linux Professional Institute's Junior Level Administration (LPIC-1) 考试 101 的主题 103 下的考核目标 103.4。该考核目标的权值为 4。

设置示例

在本文中,我们将使用在文章 “学习 Linux,101:文本流和过滤器” 中创建的一些文件练习命令。即使您没有阅读那篇文章或者没有保存所创建的文件,也一样能顺利学习本文。我们首先在您的主目录下创建一个名为 lpi103-4 的子目录并在其中创建必要的文件。为此,在作为当前目录的主目录下打开一个文本窗口,将清单 1 中的内容复制到文本窗口并运行命令。完成之后就创建了您将要使用的 lpi103-4 子目录和文件。

清单 1. 创建示例文件
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
split -l 2 text1
split -b 17 text2 y; }

您的窗口应该类似于清单 2,并且当前的目录为新创建的 lpi103-4 目录。

清单 2. 创建示例文件 - 输出
[ian@echidna ~]$ 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->text3echo "This is a sentence. " 
"This is a sentence. " "This is a sentence. ">text3
> split -l 2 text1
> split -b 17 text2 y; }
[ian@echidna lpi103-4]$

重定向标准 I/O

Linux shell(比如 Bash)接收或发送序列和字符串 形式的输入或输出。每个字符都独立于与之相邻的字符。字符没有被组织成结构化记录或固定大小的块。不管实际的字符串流进入或来自文件、键盘、显示窗口或其他 I/O 设备,都使用文件 I/O 技术来访问流。Linux shell 使用 3 种标准的 I/O 流,每种流都与一个文件描述符相关联:

  1. stdout标准输出流,它显示来自命令的输出。它的文件描述符为 1。
  2. stderr标准错误流,它显示来自命令的错误输出。它的文件描述符为 2。
  3. stdin标准输入流,它为命令提供输入。它的文件描述符为 0。

输入流通常通过终端击键为程序提供输入。输出流通常向终端输出文本字符。最初的终端是 ASCII 打字机或显示终端,但现在更多是指图形桌面上的文本窗口。

如果您已经学习了文章 “学习 Linux,101:文本流和过滤器”,那么就熟悉本文的部分内容。

重定向输出

可以通过两种方法将输出重定向到文件:

n>
将输出从文件描述符 n 重定向到文件。您必须具有该文件的写权限。如果该文件不存在,将创建它。如果该文件已经存在,通常将覆盖所有现有内容,并且没有任何警告。
n>>
还可以将输出从文件描述符 n 重定向到一个文件中。这里也一样要求您具有该文件的写权限。如果该文件不存在,将创建它。如果该文件已经存在,输出将附加到现有的内容后面。

在 n> 或 n>> 中的 n 引用文件描述符。如果省略它,将执行标准输出。清单 3 在我们先前在 lpi103-4 目录中创建的文件中使用重定向将标准输出和标准错误从 ls 命令分离出来。我们还显示将输出附加到现有文件中。

清单 3. 输出重定向
[ian@echidna lpi103-4]$ ls x* z*
ls: cannot access z*: No such file or directory
xaa  xab
[ian@echidna lpi103-4]$ ls x* z* >stdout.txt 2>stderr.txt
[ian@echidna lpi103-4]$ ls w* y*
ls: cannot access w*: No such file or directory
yaa  yab
[ian@echidna lpi103-4]$ ls w* y* >>stdout.txt 2>>stderr.txt
[ian@echidna lpi103-4]$ cat stdout.txt
xaa
xab
yaa
yab
[ian@echidna 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 内置控件的 noclobber 选项对此进行控制。如果该选项已经设置,您可以使用 n>| 覆盖它,如清单 4 所示。

清单 4. 带有 noclobber 选项的输出重定向
[ian@echidna lpi103-4]$ set -o noclobber
[ian@echidna lpi103-4]$ ls x* z* >stdout.txt 2>stderr.txt
-bash: stdout.txt: cannot overwrite existing file
[ian@echidna lpi103-4]$ ls x* z* >|stdout.txt 2>|stderr.txt
[ian@echidna lpi103-4]$ cat stdout.txt
xaa
xab
[ian@echidna lpi103-4]$ cat stderr.txt
ls: cannot access z*: No such file or directory
[ian@echidna lpi103-4]$ set +o noclobber #restore original noclobber setting

在某些情况下,您可能想要将标准输出和标准错误都重定向到一个文件中。这通常为自动进程或后台作业而执行的,以便以后可以查看输出。使用 &> 或 &>> 同时将标准输出和标准错误重定向到同一个文件中。另一种方法是,首先重定向文件描述符 n,然后使用 m>&n 或 m>>&n 将文件描述符 m 重定向到同一个文件。例如,
command 2>&1 >output.txt
不同于
command >output.txt 2>&1
在第一种情况中,stderr 被重定向到 stdout 的当前位置,然后在将 stdout 重定向到 output.txt,但第二次重定向仅影响 stdout,不影响 stderr。在第二种情况中,stderr 被重定向到 stdout 的当前位置,即 output.txt。我们在清单 5 中显示了这些重定向。注意,在最后一个命令中先重定向标准错误在重定向标准输出,所以标准错误输出仍然打印在终端窗口中。

清单 5. 将两个流重定向到一个文件中
[ian@echidna lpi103-4]$ ls x* z* &>output.txt
[ian@echidna lpi103-4]$ cat output.txt
ls: cannot access z*: No such file or directory
xaa
xab
[ian@echidna lpi103-4]$ ls x* z* >output.txt 2>&1
[ian@echidna lpi103-4]$ cat output.txt
ls: cannot access z*: No such file or directory
xaa
xab
[ian@echidna lpi103-4]$ ls x* z* 2>&1 >output.txt # stderr does not go to output.txt
ls: cannot access z*: No such file or directory
[ian@echidna lpi103-4]$ cat output.txt
xaa
xab

不过,有时候您可能想要完全忽略标准输出或标准错误。为此,将选择的流重定向到空文件 /dev/null。清单 6 显示了如何从 ls 命令忽略错误输出,同时也使用 cat 命令显示 /dev/null 是空的。

清单 6. 使用 /dev/null 忽略输出
[ian@echidna lpi103-4]$ ls x* z* 2>/dev/null
xaa  xab
[ian@echidna lpi103-4]$ cat /dev/null

重定向输入

就像可以重定向 stdout 和 stderr 流一样,我们也可以使用 < 操作符从文件重定向 stdin。如果您已经学习了文章 “学习 Linux,101:文本流和过滤器”,那么您可能还会记得我们在 sort 和 uniq 小节中使用 tr 命令将 text1 文件中的空格替换成制表符。在那个例子中我们使用来自 cat 命令的输出为 tr 命令创建标准输入。现在,我们没有必要调用 cat,而是使用输入重定向将空格转换成制表符,如清单 7 所示。

清单 7. 输入重定向
[ian@echidna lpi103-4]$ tr ' ' '\t'<text1
1       apple
2       pear
3       banana

Shell(包括 bash)还有存在 here-document 的概念,它是另一种输入重定向形式。它将 << 和一个单词(比如 END)结合构成一个标记,用来表示输入端。我们在清单 8 中对此进行演示。

清单 8. 使用 here-document 的输入重定向
[ian@echidna lpi103-4]$ sort -k2 <<END
> 1 apple
> 2 pear
> 3 banana
> END
1 apple
3 banana
2 pear

您可能很想知道可不可以仅输入 sort -k2 和数据,然后按 Ctrl-d 表明输入端。最简单的答案是,您可以这样做,但您必须先了解 here-documents。详细的答案是,here-documents 通常用于 shell 脚本(脚本没有其他方式能够表明应该将脚本的哪一行看作输入)。因为 shell 脚本通过广泛使用制表符来提供缩进,所以 here-documents 还有另一个特点。如果您使用 <<- 而不是 <<,那么将消除前面的制表符。

在清单 9 中,我们使用命令行替换创建了一个强制制表符,然后创建了一个包含两个 cat 命令的 shell 脚本,这两个命令都从 here-document 读取数据。注意,我们使用 END 作为从终端读取的 here-document 的标记。如果我们在该脚本中也使用 END 作为标记,将导致提前结束输入。因此我们使用 EOF 作为标记。在创建好脚本之后,我们使用点号 . 命令导入它,即在当前的 shell 上下文中运行它。

清单 9. 使用 here-document 的输入重定向
[ian@echidna lpi103-4]$ ht=$(echo -en "\t")
[ian@echidna lpi103-4]$ cat<<END>ex-here.sh
> cat <<-EOF
> apple
> EOF
> ${ht}cat <<-EOF
> ${ht}pear
> ${ht}EOF
> END
[ian@echidna lpi103-4]$ cat ex-here.sh
cat <<-EOF
apple
EOF
        cat <<-EOF
        pear
        EOF
[ian@echidna lpi103-4]$ . ex-here.sh
apple
pear

在本系列的后续文章中,我们将更详细地介绍命令替换和脚本。查看我们的 学习 Linux,101:LPIC-1 路线图 获得本系列所有文章的简介和链接。

创建管道

在文章 吧学习 Linux,101:文本流和过滤器” 中,我们这样描述文本过滤:接收文本输入流并对文本执行一些转换,然后在发送到输出流的过程。这种过滤通常是通过构造命令管道线 来完成的,其中来自一个命令的输出被导入重定向 为下一个命令的输入。管道的这种使用方式并不局限于文本流,尽管这是它的最常见用法。

通过管道将 stdout 导入到 stdin

在两个命令之间使用管道 | 操作符将的一个命令的 stdout 指向第二个命令的 stdin。您可以通过添加更多的命令和管道操作符来构造更长的管道线。任何命令都可能包含选项或参数。许多命令使用连字符 (-) 取代文件名作为一个参数,用于表示输入来自 stdin 而不是文件。查看手册页确保正确使用命令。构造由多个命令(每个命令都有特定的功能)组成的长管道线是在 Linux 和 UNIX® 中用于完成任务的常见方法。在清单 10 的假设管道线中,command2command3 都带有参数,但 command3 仅使用 - 参数表示来自 stdin 的输入。

清单 10. 通过管道从几个命令导出输出
command1 | command2 paramater1 | command3 parameter1 - parameter2 | command4

需要说明的是,管道线将 stdout 导向 stdin。您不能使用 2| 单独导出 stderr,至少使用我们目前所了解的工具还不能这样做。如果 stderr 已被重定向到 stdout,那么两个流都会被通过管道导出。在清单 11 中,我们展示了一个不太现实的 ls 命令,它有 4 个不是按字母顺序出现的通配符参数,然后使用一个管道对包含正常和错误输出内容的进行分类。

清单 11. 使用管道导出两个输出流
[ian@echidna 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
[ian@echidna 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 系统中的管道的优点之一是,与其他流行的操作系统不同,它们的管道不涉及到中间文件。第一个命令的 stdout 没有到一个文件中,然后再由第二个命令读取。在文章 “学习 Linux,101:文件和目录管理” 中,您学习了如何使用 tar 命令在一个步骤中归档和压缩文件。即使您使用的 UNIX 系统的 tar 命令不支持使用 -z(gzip)或 -j(bzip2)进行压缩也不成问题。您可以使用这样的管道

bunzip2 -c somefile.tar.bz2 | tar -xvf -

完成该任务。

使用文件而不是 stdout 开始管道线

在以上的管道线中,我们在开始时使用一些生成输出的命令,然后通过管道线的每个阶段导出输出。如果我们要以现有的文件开始,应该怎么办呢?许多命令都接受 stdin 或文件作为输入,因此这不成问题。如果您有要求来自 stdin 的输出的过滤器,那么可以考虑使用 cat 命令将文件复制到 stdout。不过,您可以对第一个命令使用输入重定向,然后在剩余的管道下中导出该命令的输出,这是更加常见的解决方案。仅需使用 < 操作符将第一个命令的 stdin 重定向到需要处理的文件。

使用输出作为参数

在前面对管道线的讨论中,您学习了如何接受一个命令的输出,并将它用作另一个命令的输入。反过来,假设您想将一个命令或文件的内容作为另一个命令的参数而不是输入。管道线不能用于实现该目的。三种常见的解决办法是:

  1. xargs 命令
  2. 带有 -exec 选项的 find 命令
  3. 命令替换

您将首先了解第一个解决办法。我们曾经在清单 9 中创建了一个强制制表符,您可以从中看到命令替换的例子。可以在命令行上使用命令替换,但在脚本中使用它则更常见;您将在本系列的后续文章中更多地了解它和脚本。查看我们的 学习 Linux,101:LPIC-1 路线图 获得本系列所有文章的简介和链接。

使用 xargs 命令

xargs 命令读取标准的输入,然后使用参数作为输入构建和执行命令。如果没有给出命令,那么将使用 echo 命令。清单 12 是使用我们的 text1 文件的基础例子,它包含 3 个行,每行只有两个单词。

清单 12. 使用 xargs
[ian@echidna lpi103-4]$ cat text1
1 apple
2 pear
3 banana
[ian@echidna lpi103-4]$ xargs<text1
1 apple 2 pear 3 banana

为什么 xargs 只有一行输出?默认情况下,xargs 在空格处中断输出,并且每个生成的标记都成为一个参数。不过,当 xargs 构建命令时,它将一次传递尽可能多的参数。您可以使用 -n 覆盖该行为,或使用 --max-args 参数。在清单 13 中,我们使用了这两种方法,并为使用 xargs 添加一个显式的 echo 调用。

清单 13. 使用 xargsecho
[ian@echidna lpi103-4]$ xargs<text1 echo "args >"
args > 1 apple 2 pear 3 banana
[ian@echidna lpi103-4]$ xargs --max-args 3 <text1 echo "args >"
args > 1 apple 2
args > pear 3 banana
[ian@echidna lpi103-4]$ xargs -n 1 <text1 echo "args >"
args > 1
args > apple
args > 2
args > pear
args > 3
args > banana

如果输入包含由单引号或双引号保护的空格,或使用了斜杠进行转义,那么 xargs 将不在遇到这些空格时中断。清单 14 显示了这些空格点。

清单 14. 使用带引号的 xargs
[ian@echidna lpi103-4]$ echo '"4 plum"' | cat text1 -
1 apple
2 pear
3 banana
"4 plum"
[ian@echidna lpi103-4]$ echo '"4 plum"' | cat text1 - | xargs -n 1
1
apple
2
pear
3
banana
4 plum

到目前为止,已经在命令的末尾添加了所有参数。如果您需要在这些参数后面再使用其他参数,可以使用 -I 选项指定一个替换字符串。如果 xargs 将要执行的命令包含有替换字符串,那么将使用参数替换它。进行了替换之后,仅将参数传递给每个命令。不过,将从一整行输出创建参数,而不仅是一个标记。您还可以使用 xargs-L 选项让命令将行当作参数看待,而不是默认的以单个空格分隔的标记。使用 -I 选项表示 -L 1。清单 15 显示了使用 -I-L 选项的例子。

清单 15. 使用带有输入行的 xargs
[ian@echidna lpi103-4]$ xargs -I XYZ echo "START XYZ REPEAT XYZ END" <text1
START 1 apple REPEAT 1 apple END
START 2 pear REPEAT 2 pear END
START 3 banana REPEAT 3 banana END
[ian@echidna lpi103-4]$ xargs -IX echo "<X><X>" <text2
<9      plum><9 plum>
<3      banana><3       banana>
<10     apple><10       apple>
[ian@echidna lpi103-4]$ cat text1 text2 | xargs -L2
1 apple 2 pear
3 banana 9 plum
3 banana 10 apple

尽管我们的例子为了便于演示使用了简单的文本文件,您很少看到包含这样的输入的 xargs。您通常需要处理某些命令生成的大量文件,这些命令包括 lsfindgrep。清单 16 显示了一种通过 xargs 将目录清单传递到命令(比如 grep)的方法。

清单 16. 使用带有多个文件的 xargs
[ian@echidna lpi103-4]$ ls |xargs grep "1"
text1:1 apple
text2:10        apple
xaa:1 apple
yaa:1

如果上一个例子中的一个或多个文件名包含空格,那么会发生什么呢?如果您像清单 16 那样使用该命令,那么将得到一个错误。在实际情况中,文件列表可能来自一些源,比如定制脚本或命令,而不是 ls,或者您希望通过其他管道线阶段传递它,以进一步进行过滤。所以您应该使用 grep "1" * 取代以上构造。

对于 ls 命令,您可以使用 --quoting-style 选项强制给导致问题的文件名加上引号或进行转义。另外一种更好的解决办法是使用 xargs-0 选项,从而使用 null 字符串 (\0) 分隔输入参数。尽管 ls 没有提供使用 null 字符串分隔的文件名作为输出的选项,但许多命令都提供这样的选项。

在清单 17 中,我们首先将 text1 复制到 “text 1”,然后显示一些在 xargs 命令中使用包含空格的文件名列表的方法。这些示例仅为了演示概念,因为 xargs 可能更加复杂。尤其是在最后一个例子中, 如果一些文件名已经包含新行字符串,那么将新行字符串转换成 null 字符串将导致错误。在本文的下一个部分中,我们将查看另外一个更加健壮的解决方案,即使用 find 命令生成合适的以 null 字符串分隔的输出。

清单 17. 文件名中包含空格的 xargs
[ian@echidna lpi103-4]$ cp text1 "text 1"
[ian@echidna 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
[ian@echidna lpi103-4]$ ls --quoting-style escape *1
text1  text\ 1
[ian@echidna lpi103-4]$ ls --quoting-style shell *1
text1  'text 1'
[ian@echidna lpi103-4]$ ls --quoting-style shell *1 |xargs grep "1"
text1:1 apple
text 1:1 apple
[ian@echidna lpi103-4]$ # Illustrate -0 option of xargs
[ian@echidna 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 选项显示 xargs 的默认限制,然后使用 -s 选项将输出命令的长度限制在允许的最大字符串数量之内。查看手册页了解其他未能再次讨论的选项。

使用带有 -exec 选项或 xargsfind 命令

在文章 “学习 Linux,101:文件和目录管理” 中,您学习例如如何使用 find 命令根据名称、修改时间、大小或其他特征查找文件。找到匹配的文件集之后,您通常希望对它们执行某些操作:删除、移动和重命名它们等。现在我们看一下 find 命令的 -exec 选项,其功能类似于使用 find 并通过管道将输出指向 xargs

清单 18. 使用 find-exec
[ian@echidna 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 命令相比,它有几个不同之处。

  1. 必须使用 {} 标记文件名在命令中的位置。它不是自动添加在末尾的。
  2. 您必须使用转义后的分号终止该命令,比如 \;、';' 或 ";" 都行。
  3. 该命令对每个输入文件执行一次。

尝试运行 find text[12] |xargs cat text3 亲自看看区别在哪里。

现在,我将话题转回到文件名中的空格。在清单 19 中我们尝试使用带有 -execfind,而不是带有 xargsls

清单 19. 对包含空格的文件名使用 find-exec
[ian@echidna lpi103-4]$ find . -name "*1" -exec grep "1" {} \;
1 apple
1 apple

到目前为止,一切进展顺利。但是不是缺少了什么?哪个文件包含 grep 找到行?缺少了文件名,因为 find 为每个文件调用 grep 一次,而 grep 非常智能,能够知道您是不是仅提供文件名,您不需要它告诉您是哪个文件。

我们也可以改为使用 xargs,但我们已经看到了文件名中包含空格时出现的问题。我们还提到 find 可以生成一个以 null 分隔符分隔的文件名列表,这是 -print0 选项所起的作用。新的 find 可能使用加号(+)取代分号(;)作为分隔符,这允许 find 在一次调用命令时传递尽可能多的名称,类似于 xargs。在这种情况中,仅能使用 {} 一次,并且它必须是该命令的最后一个参数。清单 20 显示了这两种方法。

清单 20. 对包含空格的文件名使用 findxargs
[ian@echidna lpi103-4]$ find . -name "*1" -print0 |xargs -0 grep "1"
./text 1:1 apple
./text1:1 apple
[ian@echidna lpi103-4]$ find . -name "*1" -exec grep "1" {} +
./text 1:1 apple
./text1:1 apple

一般而言,两种方法都是有效的,选择哪种方法由您决定。记住,使用管道导出包含未受保护的空格的内容将导致问题,因此如果您要使用管道将输出导出到 xargs,请使用将 -print0 选项和 find 结合使用,并使用 -0 选项告诉 xargs 接收使用 null 分隔符分隔的输入。其他命令,包括 tar,也支持使用 -0 选项并用 null 分隔符分隔的输入,因此应该对支持该选项的命令使用它,除非您能确保您的输入列表不会造成问题。

最后,我们介绍对文件列表进行操作。在执行删除或重命名文件等重要操作之前,最好彻底地测试列表和仔细测试命令。进行良好的备份也是非常有价值的。

分离输出

这个小节简单地讨论另一个命令。有时候,您可能希望在屏幕上看到输出,同时保留一个副本。尽管您可以将命令输出重定向到一个窗口中的文件,然后使用 tail -fn1 在另一个屏幕中跟踪输出来实现该目的,但使用 tee 命令要简单得多。

您可以将 tee 和管道一起使用。对标准输出而言,参数是一个或多个文件。-a 选项附加而非覆盖文件。在前面关于管道的讨论中可以看到,必须先将 stderr 重定向到 stdout ,然后再重定向到 tee,如果您需要同时保存两者的话。清单 21 显示 用于将输出保存到文件 f1 和 f2 中的 tee

清单 21. 使用 tee 分离 stdout
[ian@echidna lpi103-4]$ ls text[1-3]|tee f1 f2
text1
text2
text3
[ian@echidna lpi103-4]$ cat f1
text1
text2
text3
[ian@echidna lpi103-4]$ cat f2
text1
text2
text3

相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Linux
ArticleID=447268
ArticleTitle=学习 Linux,101: 流、管道和重定向
publish-date=11162009