Unix Shell I/O重定向

Unix Shell I/O 重定向(包括管道)

目录

1.输出重定向 - 标准输出和标准错误
1.1 将输出重定向到文件中
1.1.1 标准输出("stdout")和标准错误("stderr)
1.2 通过 /dev/null 丢弃输出结果
1.3 应避免的输出重定向错误
2.输入重定向 - 标准输入
2.1 并非所有命令都能读取标准输入值
2.2 标准输入的 shell 重定向
3.重定向到程序(管道)
3.1.管道规则
3.2.将命令用作过滤器
3.3 管道实例
3.4 滥用程序重定向
4.独特的 STDIN 和 STDOUT
5.tr - 不接受路径名的命令
6.不要重定向全屏程序,如 VIM
7.仅将 stderr 重定向到管道中(高级!)

在下面的示例中,我使用元字符";"将多条命令放在一条 shell 命令行中: $ date ; who ; echo hi ; pwd 这些命令的行为就像你在不同行中分别输入的一样。

1.输出重定向 - 标准输出和标准错误

在输出重定向中,shell(而不是命令)会将通常出现在屏幕上的大部分命令输出转移(重定向)到其他地方,可以是其他命令的输入(使用管道元字符"|"),也可以是文件(使用文件重定向元字符">")。

  • 在找到命令之前,shell 首先会进行重定向;shell 不知道命令是否存在或是否会产生任何输出。

  • 你只能重定向你能看到的输出。 如果没有可见的输出,那么添加重定向也不会产生任何输出。

  • 重定向只能指向一个地方。 不能使用多个重定向将输出发送到多个地方。(参见 "tee "命令)。

  • 默认情况下,错误信息(称为 "标准错误 "或 "stderr")不会被重定向;只有 "正常输出"(称为 "标准输出 "或 "stdout")会被重定向(但也可以使用更多语法重定向 stderr)。

1.1 将输出重定向到文件中

shell 元字符 ">" 表示命令行上的下一个字节是一个输出文件(而不是程序),该文件应被创建或截断(设置为空),以便接收命令的标准输出:

date >outfile

文件名和 '>' 之间的空格是可选的:

date > outfile

文件总是在 shell 找到并运行命令之前创建或截断为空,除非你像这样把字符 > 加倍:

date >> outfile # 输出附加到 outfile;不截断

将输出重定向到文件的示例:

    $ echo hello # 输出到终端(屏幕)
    hello

    $ echo hello >file # 删除文件;将输出发送到文件
    $ cat file # 显示文件内容
    hello

    $ echo there >>file # 将输出添加到文件末尾
    $ cat file # 显示文件内容
    hello
    there

创建或截断文件并设置重定向的是 shell,而不是被重定向的命令。命令对重定向一无所知,在找到并执行命令之前,重定向语法已从命令行中删除:

$ echo one two three # echo 有三个参数
one two three
$ echo one two three >out # echo 仍有三个参数
$ cat out
one two three

Shell 会在寻找要运行的命令名称之前处理重定向。 事实上,即使找不到命令或根本没有命令,也可以进行重定向:

$ nosuchcommandxxx >out # 文件 "out "是空的
sh:nosuchcommandxxx:未找到命令
$ wc out
    0 0 0 out # shell 创建了一个空文件

$ >out # 文件 "out "为空
$ wc out
    0 0 0 out # shell 创建了一个空文件

shell 会创建或截断空文件 "out",然后尝试查找并运行不存在的命令,但失败了。任何现有文件的内容都会被删除:

$ echo hello >out ; cat out
hello
$ nosuchcommandxxx >out
sh:nosuchcommandxxx:未找到命令
$ wc out
    0 0 0 out # shell 截断了文件

重定向是在运行命令之前由 shell 完成的:

$ mkdir empty
$ cd empty
$ ls -l
总计 0 # 未找到文件

$ ls -l >out # shell 首先创建 "out"
cat out # 显示输出
共计 0
-rw-r--r-- 1 idallen idallen 0 Sep 21 06:02 out

$ date >out
$ ls -l
共计 4
-rw-r--r-- 1 idallen idallen 29 Sep 21 06:04 out

$ ls -l >out # shell 会先清空 "out"。
cat out # 显示输出
共计 0
-rw-r--r-- 1 idallen idallen 0 Sep 21 06:06 out

在运行 "ls "命令之前,shell 会创建或清空文件 "out"。

解释这一系列命令:

$ mkdir empty
$ cd empty
$ cp a b
cp: 无法统计 `a':没有此类文件或目录

$ cp a b >a
$ # 为什么这次 cp 没有错误信息?

解释这一系列命令:

$ date
Wed Feb 8 03:01:11 EST 2012

$ date >a
$ cat a
Wed Feb 8 03:01:21 EST 2012

$ cp a b
$ cat b
Wed Feb 8 03:01:21 EST 2012

$ cp a b >a
$ cat b
$ # 为什么文件 b 是空的?

Shell 并不关心在命令行的哪个位置进行文件重定向。文件重定向由 shell 完成,然后在调用命令前从命令行中删除重定向语法。 实际运行的命令不会看到重定向语法的任何部分;参数数量也不受影响。

下面所有的命令行都等同于 shell;在每种情况下,echo 命令都只看到三个参数,并且三个命令行参数 "hi"、"there "和 "mom "都被重定向为 "file":

echo hi there mom >file # echo 有三个参数
echo hi there >file mom # echo 有三个参数
echo hi >file there mom # echo 有三个参数
echo >file hi there mom # echo 有三个参数
>file echo hi there mom # echo 有三个参数

在命令运行之前,shell 会删除重定向语法;因此,重定向语法永远不会算作命令的参数。
例如:

    $ echo hello there
    #  - shell 调用带有两个参数的 "echo" ==> echo(hello,there)
    #  - echo "在标准输出中回传两个参数
    #  - 输出显示在默认位置(标准输出即屏幕)

    $ echo hello there >file
    #  - shell 创建 "文件",并将标准输出转入其中
    #  - shell 会删除命令行中的">文件 "语法
    #  - shell 调用带有两个参数的 "echo" ==> echo(hello,there) (注意,"echo "的参数与上一示例没有变化)
    #  - echo "在标准输出中回传两个参数
      - 标准输出记录在输出 "文件 "中,而不是屏幕上

    $ >file echo hello there
    #  - 这与上面的示例完全相同(shell 不关心在命令行的哪个位置设置重定向)。
    #  - 标准输出记录在输出 "文件 "中,而不是屏幕上
    #  - 你可以将重定向放在命令行的任何地方!

重定向首先由 shell 完成,然后再寻找执行命令:

  • shell 会创建一个新的空文件或截断(清空)一个现有文件
  • 在执行了重定向操作,并从命令行中移除了相关的语法后,shell 会查找并执行命令(如果有的话)。:

解释这一系列命令:

$ rm
rm: missing operand

$ touch file
$ rm >file
rm: 缺少操作数 # 为什么 rm 不能删除 "file"?

$ rm nosuchfile
rm:无法删除 "nosuchfile":没有此类文件或目录

$ rm nosuchfile >nosuchfile
$ # 为什么这里没有 rm 错误信息?

你只能重定向你能看到的输出!只能你看到的输出!

  • 重定向不会产生新的输出!只有你看到的!
  • 如果看不到命令的任何输出,添加重定向功能只会让 shell 创建一个空文件(无输出):

示例

$ cp /etc/passwd x # 标准输出中无输出
$ cp /etc/passwd x >out # 文件 "out "为空

cd /tmp # 标准输出上没有输出
cd /tmp >out # 文件 "out "为空

touch x ; rm x # 标准输出上没有 rm 的输出结果
touch x ; rm x >out # 文件 "out "是空的

重定向只能指向一个地方:

  • 最右侧的文件重定向获胜(其他文件创建空文件)
    示例: $ date >a >b >c # 输出到文件 c;a 和 b 为空

重定向到文件优于重定向到管道:

  • 请参阅下面关于使用"|"管道重定向到程序的章节
  • 如果重定向到文件和管道中,管道什么也得不到
    示例:$ date >a | cat # 输出到文件 "a";cat 什么也不显示

除非通过 >> 添加,否则重定向输出文件将被清空(截断)。

  • 在 shell 查找并运行命令之前,文件已被清空。
  • 不要将输出重定向文件作为同一命令的输入文件使用
    错误示例: $ sort a >a # 错误!文件 "a "被截断为空

1.1.1 标准输出("stdout")和标准错误("stderr)

大多数命令都有两个独立的输出 "流",编号分别为 1 和 2:

  1. stdout - unit 1 - 标准输出(正常输出)
  2. stderr - unit 2 - 标准错误输出(错误和警告信息)

屏幕上正常(非错误)的 "unit 1" 输出来自命令的 "标准输出"("stdout")。Stdout 是 C 和 C++ 程序中 "printf "和 "cout "语句的输出,也是 Java 中 "System.print "和"System.println"的输出。这是命令的预期、常规输出。

"unit 2" 在屏幕上输出的错误信息来自命令的 "标准错误输出"("stderr")。 Stderr 是 C 和 C++ 程序中 "fprintf(stderr, ...)"和 "cerr "语句的输出,也是 Java 中 "System.err.print" 和 "System.err.println "的输出。程序仅在此输出上打印错误信息。

在终端屏幕上,stdout 和 stderr 混合在一起。 它们在屏幕上看起来是一样的,所以你无法通过观察屏幕来分辨哪些是程序的 stdout,哪些是程序在 stderr 上。

要显示 stdout 和 stderr 同时出现在屏幕上的简单示例,
请使用 ls 命令,并给出一个存在的文件名和一个不存在的文件名(因此会显示错误信息):

$ ls -l /etc/passwd nosuchfile
ls:nosuchfile:没有此类文件或目录                          # 标准错误
-rw-r--r-- 1 root root 2209 Jan 19 20:39 /etc/passwd      # 标准输出

由于 stdout 命令使用内部 I/O 缓冲区,stderr(错误信息)输出往往先于 stdout 出现。

通常,stdout 和 stderr 会一起出现在终端上。shell 可以将这两个输出单独或一起重定向到文件或其他程序中。默认的输出重定向类型(无论是重定向到文件还是使用管道重定向到程序)只重定向标准输出,而让标准错误原封不动地进入终端。

下面是一些使用 shell 文件重定向元字符">"的示例:


$ ls /etc/passwd nosuchfile # 没有使用重定向
ls: nosuchfile:没有此类文件或目录 # 屏幕上显示来自 stderr 的信息
/etc/passwd # 从 stdout 显示在屏幕上

ls /etc/passwd nosuchfile >out # shell 只重定向 stdout
ls: nosuchfile:没有此类文件或目录 # 屏幕上只显示 stderr

$ cat out
/etc/passwd

你可以使用">"元字符前的单元号将 stdout 和 stderr 分别重定向到文件中:

  • stdout 始终为 unit 1,stderr 始终为unit 2(stdin 为 unit 0)
  • 在">"元字符前立即加上单元号(不留空白)。

    ">foo"(前面没有单元号)是 "1>foo "的shell速记符号
    ">foo "只重定向默认 unit 1(stdout),不重定向 stderr
    ">foo "和 "1>foo "完全相同

你还可以告诉 shell 将标准错误 unit 2 重定向到文件:

$ ls /etc/passwd nosuchfile 2>errors # shell 只重定向 stderr
/etc/passwd # 屏幕上只显示 stdout

$ cat errors
ls:nosuchfile:没有此类文件或目录

你可以将 stdout 重定向到一个文件,将 stderr 重定向到另一个文件:

$ ls /etc/passwd nosuchfile >out 2>errors # shell 重定向每个错误
$ # 屏幕上什么也没出现

$ cat out
/etc/passwd

$ cat errors
ls:nosuchfile:没有此类文件或目录

在 Bourne shells 中,你需要使用特殊语法 "2>&1 "将 stdout 和 stderr 安全地重定向到一个文件中。将语法 "2>&1 "理解为 "将unit 2 发送到unit 1 的相同位置":

$ ls /etc/passwd nosuchfile >both 2>&1 # 将两个文件重定向到同一个文件中
$ # 屏幕上什么也没出现

$ cat both
ls:nosuchfile:没有此类文件或目录
/etc/passwd

命令行中 >both 和 2>&1 的顺序很重要!

stdout重定向">both "必须位于stderr重定向 "2>&1 "的前面(左边),因为你必须先设置stdout(单元1)的去向*,然后再将stderr(单元2)发送到 "与单元1相同的地方"。 不要颠倒这些内容!

必须使用特殊语法">both 2>&1 "才能将 stdout 和 stderr 放入同一个文件。 不要使用下面这种不一样的语法:

$ ls /etc/passwd nosuchfile >wrong 2>wrong # 错!不要这样做!

$ cat wrong
/etc/passwd
ccess nosuchfile:没有此类文件或目录

上述错误示例会导致 stderr 和 stdout 互相覆盖,结果是输出文件被混淆;请勿这样做。

现代的 Bourne shells 现在有一种特殊的较短语法,可以将 stdout 和 stderr 重定向到同一个输出文件中:

$ ls /etc/passwd nosuchfile &>both # 将两个文件重定向到同一个文件中
$ # 屏幕上什么也没出现

$ cat both
ls:nosuchfile:没有此类文件或目录
/etc/passwd

现在你可以使用"&>both "或">both 2>&1",但只有后者能在所有版本的 Bourne shell 中使用(可以追溯到 20 世纪 60 年代!)。 在编写 shell 脚本时,请使用">both 2>&1 "版本,以获得最大的可移植性。

输出重定向摘要

重定向由 shell 完成。 事情按以下顺序发生

1.首先:所有重定向(和文件截断)都由 shell 完成。shell 会删除命令行中的所有重定向语法。即使没有命令执行,重定向和截断也会发生,命令不会知道它的输出被重定向了。

2.第二步:执行命令(如果有的话)并可能产生输出。

3.第三步:命令输出(如果有的话),并进入指定的重定向输出文件。 如果命令没有输出,输出文件将为空。添加重定向永远不会产生输出。

1.2.使用 /dev/null 丢弃输出结果

每个 Unix 系统都有一个特殊文件,你可以将不想保留或看到的输出重定向到该文件中:

/dev/null

下面的命令会产生一些我们不希望看到的错误输出:

$ cat * >/tmp/out
cat: course_outlines:是一个目录 # 错误会打印在 STDERR 上
cat: jclnotes:是一个目录 # 错误会打印在 STDERR 上
cat: labs:是一个目录 # 错误会打印在 STDERR 上
cat: notes:是一个目录 # 错误会打印在 STDERR 上

我们可以将错误(stderr,unit 2)扔到 /dev/null:

cat * >/tmp/out 2>/dev/null

文件 /dev/null 永远不会填满;它只是吃掉输出。 当用作输入路径名时,它看起来总是空的:

$ wc /dev/null
0 0 0 /dev/null

系统管理员:不要养成丢弃所有命令错误输出的习惯!你也会丢弃合法的错误信息,没有人会知道这些命令失败了。

1.3 应避免的输出重定向错误

首先,这里总结了如何正确使用重定向:

date >out
  1. shell 首先截断文件 "out"--文件现在是空的
  2. shell 将命令 "date "的标准输出重定向到文件 "out "中
  3. shell 删除命令行中的">out "语法
  4. shell 查找并运行 "date "命令
  5. 日期命令的标准输出转入标准输出(1 行)- 标准输出已被 shell 重定向到文件 "out "中

    结果:文件 "out "包含 "date "的一行输出结果

Unix 大型重定向错误

不要将重定向文件同时作为程序或管道的输出和输入! 下面的示例程序使用了排序命令--任何读取文件并产生输出的程序都有风险:

sort a >a # 错误!重定向输出文件被用作排序输入文件!
  1. shell 首先截断文件 "a" - 文件现在是空的
    • a "的原始内容会丢失、截断、消失!- shell 甚至还没来得及运行 "sort "命令!
  2. shell 将排序的标准输出重定向到空文件 "a "中
  3. shell 查找并运行带有一个文件名参数 "a "的 "排序 "命令 ==> 即 sort(a)
  4. 排序命令打开空参数文件 "a "供读取
  5. 标准输出已被 shell 重定向到文件 "a "中

    • 对空文件排序不产生任何输出;文件 "a "仍然为空

    结果:文件 "a "总是空的,无论之前有什么内容。

    正确方法(使用两个命令): $ sort a >tmp && mv tmp a
    正确方法(使用特殊的排序输出选项): $ sort -o a a

下面是另一个错误示例,使用相同的输出文件作为输入:

date >out
wc out >out # 错误!重定向输出文件被用作排序输入文件!
  1. shell 首先截断文件 "out"--文件现在是空的
    • out "中的原始内容会丢失--被截断--消失!- shell 甚至还没来得及运行 "wc "命令!
  2. shell 将 wc 的标准输出重定向到空文件 "a "中
  3. shell 找到并运行 "wc "命令,其中有一个文件名参数 "out" ==> 即 wc(out)
  4. wc 命令打开空参数文件 "out "供读取
    标准输出已被 shell 重定向到文件 "out "中

    • 计算一个空文件,在标准输出上产生 1 行 "0 0 0 out

    结果:一行 wc 输出 "0 0 0 out "被放入文件 "out"。 文件 "out "现在只有一行,即一个空文件的字数。在步骤 1 中,shell 截去了 "out "中的原始内容,从未使用过。

    正确方法(使用两条命令): $ wc out >tmp && mv tmp out

其他不起作用的错误重定向示例:

$ head file >file # 总是创建一个空文件
$ tail file >file # 总是创建一个空文件
$ uniq file >file # 总是创建一个空文件
$ cat file >file # 总是创建一个空文件
$ grep 'foo' file >file # 总是创建一个空文件
$ sum file >file # 始终校验空文件的和值
......等等

切勿在输入和输出中使用相同的文件名,shell 会在命令读取之前截断文件。

Unix 大型重定向错误 #2

切勿使用通配符/全局文件模式,因为这种模式会获取输出重定向文件的名称,并使其成为非预期的输入文件。

Bourne shell(如 BASH)会在创建重定向文件之前进行 GLOB 通配符扩展。 C shell 会先创建重定向文件,这可能会带来更多意想不到的问题。

这里使用 nl(行数)程序作为示例程序--任何读取文件并产生输出的程序都有风险:

$ cp /etc/passwd bar # 创建一个大于磁盘块的文件
$ touch foo
$ nl * >foo # 错误!GLOB * 输入文件与重定向输出文件匹配!
^C # 在磁盘满之前立即中断该命令!
$ ls -l
-rw-rw-r-- 1 idallen idallen 194172 Feb 15 05:19 bar
-rw-r--r-- 1 idallen idallen 289808384 Feb 16 05:20 foo

下面是使输出文件 "foo "永久增长的过程:

  1. Shell 扩展 "*"以匹配所有路径名,即 "bar "和 "foo"。
  2. Shell 截断 >foo 并准备接收命令的 stdout。
  3. nl 打开第一个文件 "bar",并将输出发送到 stdout(进入 foo)。
  4. nl 打开下一个文件 "foo",并从文件顶部开始读取,将输出写入文件底部。 这个过程永远不会结束,文件 "foo "会不断增加,直到用完所有磁盘空间。

结果:无限循环,随着 "foo "越来越大,磁盘驱动器也越来越满。

修复 #1:使用与 GLOB 不匹配的隐藏文件名:

$ nl * >.z
- 使用与 shell 通配符 "*"不匹配的隐藏文件名
- nl 命令没有将".z "作为参数,因此不会出现循环

修复 #2(两种方法):  使用其他目录下的文件:

$ nl * >.../z
$ nl * >/tmp/z
- 将输出重定向到不在当前目录下的文件中,这样就不会被 nl 命令读取,也就不会出现循环

2.输入重定向 - 标准输入

如果命令行中给出了文件路径名,许多 Unix 命令都会从文件中读取输入。 如果**没有给出文件名,这些命令通常从标准输入("stdin")读取,标准输入通常与键盘相连。 (你可以发送 EOF 让命令停止读取)。

cat 命令从文件读取数据,然后在没有提供文件时读取 stdin 的示例:

cat /etc/passwd # cat 读取文件 /etc/passwd 中的内容
[......此处打印多行......]
$

$ cat # 没有文件;cat 读取标准输入(你的键盘)
你在这里输入线条
^D # 键入 ^D (CTRL-D) 即可发出键盘 EOF 信号
在此输入行数 # 这是 cat 的输出结果
$

从路径名或标准输入读取内容的其他命令示例

less、more、cat、head、tail、sort、wc、grep、nl、uniq 等。

上述命令可以读取标准输入。 如果命令行上没有路径名可读取,它们只会*读取键盘上的内容、
而且**不涉及输入重定向:

$ wc foo # wc 打开并读取文件 "foo";wc 完全忽略 stdin
$ wc # wc 打开并读取标准输入 = 你的键盘

$ cat foo # cat 打开并读取文件 "foo";cat 完全忽略 stdin
$ cat # cat 打开并读取标准输入 = 你的键盘

$ tail foo # tail 打开并读取 "foo";tail 完全忽略 stdin
$ tail # tail 打开并读取标准输入 = 你的键盘

[......等所有可以从 stdin 读取的命令......]

要让命令停止读取键盘,可向其发送 EOF(文件结束)指示,通常是键入 ^D (Control-D)。 如果中断命令(例如键入 ^C),则可能会杀死命令,而且命令可能根本不会产生任何输出。

2.1 并非所有命令都能读取标准输入值

并非所有命令都从标准输入端读取数据,因为并非所有命令都从命令行提供的文件中读取数据。 不从文件或标准输入端读取任何数据的常见 Unix 命令示例:

ls、date、who、pwd、echo、cd、hostname、ps 等。# 从不读取 stdin

上述所有命令都有一个共同点,那就是它们从不*打开任何文件供命令行读取。 如果一条命令从不从任何文件中读取数据,那么它也不会从键盘上读取数据,更不会从标准输入中读取数据。

显然,Unix 复制命令 "cp "是从文件中读取内容的,但它从不从标准输入中读取文件数据,因为它必须同时包含源路径名和目标路径名参数。cp 命令必须始终有一个输入文件名。 它从不读取 stdin。

2.2 标准输入的 shell 重定向

shell 元字符"<"表示命令行上的下一个字是一个输入文件(而不是程序),应在标准输入上提供给命令。

使用 shell 元字符"<",你可以告诉 shell 使用输入重定向来改变标准输入的来源,使其不再来自键盘,而是来自输入文件。

只有在可以读取键盘的命令上才能使用标准输入重定向。 如果命令在没有重定向的情况下不读取键盘(标准输入),则添加重定向不起任何作用,会被忽略。 只有在不使用重定向的情况下,命令*会读取键盘时,重定向才会起作用。

如果(也只有在!)命令从标准输入读取数据,则重定向的标准输入将导致程序从 shell 附加到标准输入的任何内容中读取数据。 下面是使用 shell 将文件附加到所有读取标准输入的命令的示例:

$ cat food # 从文件 "food "中读取数据
$ cat # 从 stdin(键盘)读取数据
$ cat <food # 从 stdin 读取数据(现在是从文件 "food "读取数据)

$ head food # 从文件 "food "读取数据
$ head # 从 stdin(键盘)读取数据
$ head <food # 从 stdin 读取数据(现在是从文件 "food "读取数据)

$ sort food # 从文件 "food "中读取数据
$ sort # 从 stdin(键盘)读取数据
$ sort <food # 从 stdin 读取数据(现在是从文件 "food "读取数据)
[......等所有可以从 stdin 读取的命令......]

shell 并不知道哪些命令将实际从标准输入端读取输入;你可以将标准输入端的文件附加到任何命令。忽略标准输入的命令将忽略附加文件。

如果命令没有从标准输入端读取数据,将输入重定向到该命令的操作将被忽略,也不会有任何作用。 shell 无法强制命令从标准输入端读取数据。

例如,date 命令和 sleep 命令从不从标准输入读取数据,也不能通过添加重定向来强制读取数据:

$ date # date 从不读取 stdin
美国东部时间 2012 年 2 月 16 日星期四 05:48:13
$ date <file # date 从不读取 stdin 并忽略 <file
美国东部时间 2012 年 2 月 16 日星期四 05:48:15

$ echo 30 >file # 首先,将数字 30 存入文件
$ sleep 10 # 睡眠从不读取 stdin
$ sleep 10 <file # 睡眠从不读取 stdin;忽略 <file
$ sleep <file # sleep 从不读取 stdin;忽略 <file
睡眠:参数太少

许多其他常用命令从不读取标准输入,因此为这些命令添加输入重定向功能毫无用处:

$ ls -l /bin # 显示 /bin 下的路径名
$ ls -l /bin <input # 没有区别;ls 从不读取 stdin

cd /bin # 切换到 /bin 目录
$ cd /bin <input # 没有区别;cd 从不读取 stdin

cp foo bar # cp 从 foo 读取数据并写入 bar;忽略 stdin
$ cp foo bar <file # 不变;cp 从不从 stdin 读取文件数据

如果命令行中存在路径名参数,可能包含路径名参数的命令也会忽略标准输入。 如果提供了路径名参数,命令将始终读取路径名并忽略 stdin。

下面是更多不能作为输入重定向运行的例子,因为在添加重定向时,命令**不是从标准输入读取的。下面的命令行都忽略了标准输入,因为所有命令都有文件名参数来代替读取:

$ cat food <file # cat 从文件 "food "中读取,忽略"<file"。
$ sort food <file # 排序从文件 "food "中读取,忽略"<file"。
$ head food <file # head 从文件 "food "中读取,忽略"<file"。
$ tail food <file # tail 从文件 "food "中读取,忽略"<file"。

[......等等,适用于所有获取路径名或从 stdin 读取的命令......]

如果命令行中有路径名,则不使用 stdin。 在上述所有错误示例中,shell 将打开文件 "file",并将其附加到 stdin 上供命令使用;命令本身将忽略 stdin,而从命令行中提供的 "food "路径名读取。 在标准输入端附加"<文件 "将被忽略。

命令从不同时读取路径名标准输入;只能二选一,而且命令参数路径名总是用来代替 stdin。 那么,如果一个文件可以作为命令行路径名提供,也可以通过标准输入附加到命令中,两者有什么区别呢? 请注意 "cat food "和 "cat <food "之间的区别:

$ 猫粮

  • cat 命令有一个路径名参数,这意味着它会忽略 stdin

  • cat "命令打开的是文件参数 "food",而不是 shell

    • 任何错误都将来自 "cat "命令,并将提及文件名 "food",例如 cat: food:权限被拒绝
  • cat 命令从自己打开的文件中读取数据

    $ cat <食物

  • cat 命令没有参数,这意味着它会读取标准输入值

  • shell 正在执行来自文件 "food "的标准输入重定向,这意味着 "cat "的标准输入将来自文件 "food"。

  • 是 shell 本身在打开文件 "food",而不是 "cat "命令

    • 任何错误都将来自 shell,而不是 "cat "命令、
      例如 bash: food:拒绝权限
  • cat 命令从 shell 打开的标准输入端读取数据

对于在输出中显示输入路径名的命令来说,上述差异更为显著。 如果命令行中没有提供路径名,所有数据都来自标准输入,那么命令就无法在输出中显示文件名:

$ wc -l /etc/passwd
44 /etc/passwd

  • wc 将文件名"/etc/passwd "作为命令行路径名参数传给了 wc,因此 wc 必须自己打开该文件

  • wc 知道文件名,因此会在输出中打印出文件名

    $ wc -l </etc/passwd
    44

  • shell 会打开标准输入重定向文件"/etc/passwd",并将其附加到命令 "wc "的标准输入中。

  • wc 没有给定文件参数,因此从标准输入读取数据;wc 不知道文件名;只有 shell 知道文件名,因此 wc 不打印任何文件名!

上述输入重定向技巧在只获取文件行数而不同时获取文件名时非常有用:

$ echo "行数为:" ; wc -l /etc/passwd
行数为
44 /etc/passwd # 错误 - "44 /etc/passwd "不是数字

$ echo "行数为:" ; wc -l </etc/passwd
行数为
44 # 正确 - 只有数字,没有名称

警告:以下命令行如何处理?
当命令完成时?

cat <myfile >myfile # 错误!
sort <myfile >myfile # 错!
head <myfile >myfile # 错误!

既然如此,为什么在下面的情况中 "myfile "不是空的?

wc <myfile >myfile # 错误!

3.重定向到程序(管道)

由于 shell 可以重定向程序的输出和输入,因此它可以将一个程序的输出连接(重定向)到另一个程序的输入中。 这被称为 "管道",并使用 "管道 "元字符"|"(shift-'\'),例如:$ date | wc

3.1.管道规则

1.管道重定向首先由 shell 完成,然后才是文件重定向。
2.管道左边的命令必须产生一些标准输出。
3.管道右侧的命令必须要读取标准输入。

shell 元字符"|"("管道")表示命令行上另一条命令的开始。 紧靠"|"左边的命令的标准输出(只有 stdout,没有 stderr)被连接到紧靠右边的命令的标准输入:

$ date
Mon Feb 27 06:37:52 EST 2012
$ date | wc
1 6 29

(请注意,行尾的换行符会被 wc 计算在内)。

在使用第二条命令之前,你可以使用临时文件作为中间存储,以近似管道的某些行为:

$ date >out ; wc <out # 保存日期的输出,并将其交给 wc 的输入
  1 6 29

如果使用临时文件,左边的命令必须在 shell 运行右边的命令读取文件之前,完成并将其输出*到临时文件中。 如果左边的命令从未执行完毕,右边的命令就永远不会运行。 管道则没有这个问题。 输出会立即通过管道开始流动,因为两个命令实际上是同时*运行的:

$ find / >out ; less out # 必须先完成查找的大量输出(速度慢)
$ find / | less # 查找的大量输出直接进入 "less"。

管道不需要临时文件,因此只要管道左边的命令开始产生标准输出,它就会直接进入右边命令的标准输入。 如果左边的命令从未完成,那么右边的命令将继续等待更多输入,并在输入出现时进行处理。 如果左边的命令执行完毕,右边的命令就会在管道(其标准输入)上看到 EOF(文件结束)。 与文件的 EOF 一样,EOF 通常意味着右边的命令将完成处理,产生最后的输出并退出。 在进行文件重定向之前,首先要识别管道并将命令行拆分成管道命令。 文件重定向发生在第二位(管道分割之后),如果存在,文件重定向优先于管道重定向。 (文件重定向是在管道分割之后**进行的,因此文件重定向总是胜出,而管道重定向则什么也不会留下)。

$ ls -l | wc # 正确 - ls 的输出进入管道
2 11 57

$ ls -l >out | wc # 错!- ls 的输出会进入文件
0 0 0 # wc 读取空管道并输出 0

  • shell 首先分割管道上的行,将左侧命令的输出重定向到右侧命令的输入中,但是: - 然后 shell 处理左侧 "ls "上的标准输出文件重定向,并将 "ls "标准输出变为文件 "out"。
  • 最后,shell 会找到并同时运行这两条命令
  • ls "的所有标准输出都进入文件 "out",没有任何内容可以进入管道
  • wc 计数管道中的空输入并输出:0 0 0

与文件输出重定向一样,你只能将你能看到的标准输出重定向到管道中;重定向永远不会创建输出:

$ cp /etc/passwd x # 在标准输出中看不到输出结果
$ cp /etc/passwd x | cat # "cat "不会收到输出结果

$ cd /tmp # 标准输出上看不到输出
$ cd /tmp | head # 没有输出传递到 "head"。

$ touch x ; rm x # 标准输出上没有 rm 的输出结果
$ touch x ; rm x | wc # "wc "不会收到输出结果
  0 0 0 # wc 对管道中的空输入进行计数

与文件重定向一样,需要使用特殊语法 "2>&1 "才能将 stdout 和 stderr 都重定向到管道中。 回想一下,"2>&1 "的意思是 "将标准错误重定向到与标准输出相同的位置",因此如果标准输出已经进入管道,"2>&1 "也会将标准错误发送到管道:

$ ls /etc/passwd nosuchfile # 未使用重定向功能
  ls:无法访问 nosuchfile:没有此类文件或目录 # STDERR unit 2
  /etc/passwd # STDOUT 单位 1

$ ls /etc/passwd nosuchfile | wc # 只有 stdin 被重定向到 "wc"。
  ls:无法访问 nosuchfile:没有此类文件或目录 # STDERR unit 2
  1 1 12 # stdout 进入 "wc "管道

$ ls /etc/passwd nosuchfile 2>&1 | wc # stdin 和 stderr 都已重定向
  2 10 68 # 卫生纸从管道中计数两条线

记住重定向只能指向*一个地方,文件重定向总是胜过管道重定向,因为文件重定向是在管道分割之后进行的。

$ ls /bin >out # ls 的所有输出都进入文件 "out"。
$ ls /bin >out | wc # 错!输出进入 "out",而不是管道
    0 0 0 # wc 对管道中的空输入进行计数

3.2.将命令用作过滤器

请注意,许多 Unix 命令都可以充当 "过滤器"--从 stdin 读取并写入 stdout,全部由 shell 提供,而不需要打开任何路径名。 在命令行上没有文件名的情况下,命令从标准输入读取并写入标准输出。shell 为标准输入和标准输出提供了重定向功能。
输出:

grep "/bin/sh" /etc/passwd | sort | head -5

上面的 "grep "命令是从命令行中给出的文件名参数/etc/passwd 中读取的。 (从文件读取时,命令不从标准输入读取。 文件名优先于标准输入)。

sort "和 "head "命令没有要读取的文件名;这意味着它们从标准输入中读取,而标准输入已被 shell 设置为管道。sort "和 "head "都是过滤器;它们从 stdin 读取并写入 stdout。 (从技术上讲,"grep "命令不是过滤器--它是从提供的参数路径名读取数据,而不是从 stdin 读取数据)。

记住:如果在命令行中给出文件名,命令将忽略标准输入,只对文件名执行操作。 请看
对上述管道稍作改动:

grep "/bin/sh" /etc/passwd | sort | head -5 /etc/passwd # 错误!

上面的命令行与之前的示例相同,只是 "head "命令现在忽略了标准输入,而是直接从/etc/passwd 文件名参数中读取。 grep "和 "sort "命令白白做了很多工作,因为 "head "并没有读取管道中的 sort 输出。 head 命令读取的是提供的文件名参数 /etc/passwd。 文件名优先于标准输入。

命令在读取文件名时会忽略标准输入。

如果命令确实从命令行提供的文件名中读取数据,那么让命令打开自己的文件名比使用 "cat "打开文件并将数据输入到命令的标准输入端更有效。(数据拷贝的次数更少!)。

这样做

head /etc/passwd
sort /etc/passwd
不要这样做(浪费进程和 I/O):
cat /etc/passwd | head # 切勿这样做--无效
cat /etc/passwd | sort # 切勿这样做--无效

建议让命令自己打开文件,不要用 "猫 "喂它们。

3.3 管道实例

问题:只显示密码文件的第 6-10 行:

head /etc/passwd | tail -n 5 # 前十行中的最后五行:第 6-10 行

问题:只显示密码文件的倒数第二行:

tail -n 2 /etc/passwd | head -n 1 # 最后两行的第一行

问题:当前目录中哪五个文件最大:

$ ls -s | sort -nr | head -n 5

$ ls -la | sort -k 5,5nr | head -n 5

  • 排序命令是按第五个字段的数字反向排序的

问题:"计算 /etc/passwd 中 shell 的数量"。

cut -d : -f 7 /etc/passwd | sort | uniq -c
  • 剪切命令会在密码文件中挑出以冒号分隔(-d :)的第 7 个(-f 7)字段
  • 排序命令将所有 shell 名称按顺序排列
  • uniq -c 命令对相邻名称进行计数,并去重

问题:"计算 /etc/passwd 中 shell 的数量,并以降序显示结果"。

cut -d : -f 7 /etc/passwd | sort | uniq -c | sort -nr
  • 使用之前的管道,并对其进行补充:
  • 对上述输出结果进行数值排序和反向排序

问题:"计算/etc/passwd 和/etc/passwd 中每种 shell 的数量。
显示按降序数字排序的前两个结果"。

$ cut -d : -f 7 /etc/passwd | sort | uniq -c | sort -nr | head -n 2

- 使用之前的管道,并对其进行补充:
- 只选取上述输出中的前两行

问题:哪十个 IP 地址最频繁地试图入侵我的机器?

# RHEL /var/log/secure
grep 'refused connect' /var/log/auth.log \
      | awk '{print $NF}' \
      | sort | uniq -c | sort -nr | head
  • grep 命令会选取包含 IP 地址的 sshd 行
  • awk 命令只显示每行输入的最后一个字段
  • 第一条(最左侧)排序命令将所有 IP 地址按顺序排列
  • uniq 命令正在计算有多少相邻地址相同
  • 第二个排序命令是对计数进行反向数字排序
  • 负责人只选取前十名的地址

问题:显示课程说明中的练习测试和每周文件日期:

alias ee='elinks -dump -no-numbering -no-references'
ee 'http://teaching.idallen.com/cst8207/12w/notes/' | grep 'practice'
ee 'http://teaching.idallen.com/cst8207/12w/notes/' | grep 'week'

问题:在主页上显示期中测试的日期:

ee 'http://teaching.idallen.com/cst8207/12w/' | grep 'Midterm'

问题:显示当前渥太华天气温度和预报:

$ ee 'http://text.www.weatheroffice.gc.ca/forecast/city_e.html?on-118' \
| grep -A1 "Temp
$ ee 'http://text.www.weatheroffice.gc.ca/forecast/city_e.html?on-118' \
| grep -A2 '今天:'
$ ee 'http://text.www.weatheroffice.gc.ca/forecast/city_e.html?on-118' \
| grep -A2 "今晚:

问题:显示渥太华明天的天气预报:

$ ee 'http://text.www.weatheroffice.gc.ca/forecast/city_e.html?on-118' \
| grep -A8 'Tonight:' | tail -n 5| tail -n 5

问题:显示 BBC 的第一条头条新闻:

$ ee 'http://www.bbc.co.uk/' | grep -A9 '热门新闻

问题:显示 BBC 每个主题的第一条头条新闻:

$ ee 'http://www.bbc.co.uk/' | grep -A3 'Top Story

问题:显示 BBC 当前的温哥华天气:

$ ee 'http://www.bbc.co.uk/weather/6173331' \
| grep -A19 'Observations' | tail -n 20

问题: 显示加拿大当前的空间天气预报:

$ ee 'http://www.spaceweather.gc.ca/index-eng.php' \
| grep -A10 "ISES 区域预警中心

问题:显示当前的月相:

$ ee 'http://www.die.net/moon/' \
| grep -A2 'Moon Phase' | head -n 3 | tail -n 1

3.4 滥用程序重定向

人们常常被误导,以为在命令中添加重定向会产生重定向之前没有的输出。事实并非如此。

管道规则

1.管道重定向首先由 shell 完成,然后才是文件重定向。
2.管道左边的命令必须产生一些标准输出。
3.管道右侧的命令必须要读取标准输入。

如果一个可以打开并读取路径名内容的 Unix 命令没有给出任何要打开的路径名,它通常会从标准输入(stdin)中读取输入行:

wc /etc/passwd # wc 读取 /etc/passwd,忽略 stdin 和你的键盘
wc # 没有文件名,wc 读取 stdin(你的键盘)

如果命令给定了路径名,它就会从路径名读取数据,并始终忽略标准输入,即使你试图向它发送一些信息:

$ wc # 没有文件名,wc 读取标准输入(键盘)
$ date | wc # wc 打开并读取标准输入,计算日期输出

$ wc foo # wc 读取 foo;wc 不读取 stdin
错误!wc 打开并读取 foo;wc 忽略 stdin

上述内容适用于所有读取文件内容的命令,例如

date | head foo  # WRONG! head opens and reads foo; head ignores stdin
date | less foo  # WRONG! less opens and reads foo; less ignores stdin

如果想让命令读取 stdin,不能给它任何文件名参数。 带有文件名参数的命令忽略标准输入;它们不应在管道右侧使用。

忽略标准输入的命令(因为它们在命令行上打开并读取路径名)将始终忽略标准输入,无论你试图在标准输入上向它们发送什么愚蠢的内容:

echo hi | head /etc/passwd   # WRONG: head has a pathname and ignores stdin
echo hi | tail /etc/group    # WRONG: tail has a pathname and ignores stdin
echo hi | wc .vimrc          # WRONG:   wc has a pathname and ignores stdin
sort a | cat b               # WRONG:  cat has a pathname and ignores stdin
cat a | sort b               # WRONG: sort has a pathname and ignores stdin

如果标准输入被发送到一个忽略它的命令,它就会被丢弃。shell 无法命令读取 stdin;这取决于命令。命令必须读取标准输入,只有省略所有文件名,命令才想*读取标准输入。

不打开和处理文件内容的命令通常会忽略标准输入,无论你试图在标准输入上向它们发送什么愚蠢的东西。 所有这些命令都不会读取标准输入:

echo hi | pwd # NO: pwd 不会打开文件 - 总是忽略 stdin
echo hi | ls # NO: ls 不会打开文件 - 总是忽略 stdin
echo hi | cd # NO: cd 不会打开文件 - 总是忽略 stdin
echo hi | date # NO: date 不会打开文件 - 总是忽略 stdin
echo hi | chmod +x .  # NO: chmod 不会打开文件 - 总是忽略 stdin
echo hi | rm foo # NO: rm 不会打开文件 - 总是忽略 stdin
echo hi | rmdir dir # NO: rmdir 不会打开文件 - 总是忽略 stdin
echo hi | echo me # NO: echo 不会打开文件 - 总是忽略 stdin
echo hi | mv a b # NO: mv 不会打开文件 - 总是忽略 stdin
echo hi | ln a b # NO: ln 不会打开文件 - 总是忽略 stdin

有些命令只对文件名参数进行操作,从不读取 stdin:

$ echo hi | cp a b # NO: cp 会打开参数 - 总是忽略 stdin

如果标准输入被发送到忽略它的命令,它就会被丢弃。
shell 不能*让命令读取 stdin;这取决于命令。

只有在命令行中没有文件名参数的情况下,可能读取标准输入的命令才会读取标准输入。 任何文件参数的存在都会导致命令忽略标准输入,转而处理文件,这意味着它们不能用于管道右侧读取标准输入。 文件名参数总是优先于标准输入。

误用重定向的例子:

下面很长的管道序列毫无意义--最后一条(最右边的)命令("head")有一个路径名,它将打开并读取该路径名,而忽略来自左边所有管道的所有标准输入:

$ head /etc/passwd | sort | tail | sort -r | head /etc/passwd

上述畸形流水线等同于此(输出相同):

$ head /etc/passwd

如果给命令提供一个要处理的文件,它就会忽略标准输入,因此带有文件名的命令不得用于任何管道的右侧。

4.特殊的 STDIN 和 STDOUT

每个命令只有一个标准输入和一个标准输出。每一个都只能重定向到*一个其他地方。 你不能从两个不同的地方重定向标准输入,也不能将标准输出重定向到两个不同的地方。

Bourne shells(包括 bash)不会警告你正试图重定向来自两个或多个不同地方的命令输入(而且只有一个重定向有效,其他重定向将被忽略):

bash$ wc <a <b <c <d <e

  • wc "的 stdin 只来自文件 "e "*。
  • 其他文件名必须存在(shell 会打开每一个文件名),但它们会被忽略,因为只有最后的重定向才会获胜

bash$ date | cat <food

  • date "输出消失;cat 从文件 "food "中读取 stdin(文件重定向是第二项工作,总是优于管道重定向)。

Bourne shell(包括 bash)不会警告你,你正试图将一条命令的输出重定向到两个或多个不同的地方,而且只有其中一个重定向有效,其他重定向将被忽略:

bash$ date >a >b >c >d >e

  • 日期 "输出到文件 "e "中;其他四个输出文件由 shell 分别创建并截断,但它们都是空的,因为只有最后重定向到文件 "e "才是赢家。

bash$ date >out | wc
0 0 0

  • 日期 "输出进入文件 "out",没有任何内容进入管道(文件重定向是第二步,并且总是优于管道重定向)。

有些 shell(包括 "C "shell,但不包括 Bourne shell)会试图警告你一些愚蠢的 shell 重定向错误:

csh% date <a <b <c <d
Ambiguous input redirect.

csh% date | cat <food
Ambiguous input redirect.

csh% date >a >b >c
Ambiguous output redirect.

csh% date >a | wc
Ambiguous output redirect.

C shell 会告诉你,不能同时将 stdin 或 stdout 重定向到多个地方。 Bourne shells 则不会告诉你--它们会忽略 "额外 "的重定向,只执行每个重定向的最后一个。

5.tr - 不接受路径名的命令

Unix的 "tr "命令是少数(唯一?)读取标准输入的Unix命令之一,但不允许在命令行上输入任何路径名--你必须在标准输入上为 "tr "提供输入:

$ tr 'a-z' 'A-Z' file1 file2 >out         # *** WRONG - ERROR ***
tr: too many arguments
$ cat file1 file2 | tr 'a-z' 'A-Z' >out   # correct for multiple files
$ tr 'a-z' 'A-Z' <tmp >out                # correct for a single file

注意:System V 版本的 "tr "要求字符范围位于方括号内,例如:tr '[a-z]' '[A-Z]' Berkeley Unix 和 Linux 不使用方括号。任何版本的 "tr "都不接受在命令行上输入路径名。所有版本的 "tr读取标准输入。

下面是一个使用相同输出文件作为输入的错误示例:

date >out
tr ' ' '_' <out >out # WRONG! Redirection output file is used as input file!
  1. shell 打开文件 "out",作为 "tr "的标准输入(由于"<out")。
  2. shell 接下来截断文件 "out"--文件现在为空(由于">out")。
    • out "的原始内容会丢失--被截断--消失!
    • 之前,shell 甚至还没有运行 "tr "命令!
  3. shell 查找并运行带有两个字符串参数的命令 "tr" ==> 即 tr(' ','_')
  4. 命令 "tr "读取标准输入(tr 总是读取 stdin)

    • 标准输入连接到 "out",现在是一个空文件
      标准输出已被 shell 重定向到文件 "out "中
    • 翻译一个空的输入文件,在 "out "中没有输出

    结果:文件 "out "总是空的。 文件 "out "获得空输入文件的翻译副本;文件 "out "始终为空。

    正确方法(使用两个命令): $ tr ' ' '_' <out >tmp && mv tmp out

问题:将 "who "命令中的小写字母转换为大写字母:

who | tr 'a-z' 'A-Z'

外壳问题:两个参数是否需要单引号?(参数中是否有需要保护的特殊字符?)

使用重定向功能,你可以使用类似的命令将小写文本文件转换为大写文本文件。

实验:为什么不能将文件 "myfile "转换为大写字母?

$ date >myfile
$ tr 'a-z' 'A-Z' <myfile >myfile                  # WRONG!
$ wc myfile
  0 0 0 myfile                 # what happened?

运行此命令后,为什么文件 "myfile "是空的?

下面的命令行不起作用,因为程序员不懂 "tr "命令的语法:

tr 'a-z' 'A-Z' myfile >new # 错误!

为什么 "tr "会产生错误信息? (tr "命令在处理命令行路径名方面很不寻常。 RTFM)

下面的命令行重定向是错误的(输入文件也是输出文件);但有时对小文件也有效:

cat foo bar | tr 'a' 'b' | grep "lala" | sort | head >foo # 错!

在第一条 "cat "命令试图从 "foo "文件中读取数据之前,当 shell 在管道末尾启动 "head "命令时,会将 "foo "文件截断为零,这中间存在一个关键的竞赛。 根据系统负载和文件大小的不同,"cat "命令可能会也可能不会在 shell 在管道末尾重定向时截断或更改 "foo "文件之前读出所有数据。

不要依赖长管道来避免错误重定向!切勿将输出重定向到在同一命令或命令管道中用作输入的文件中。

6.不要重定向全屏程序,如 VIM

全屏键盘交互程序(如 VIM 文本编辑器)在重定向其输入或输出时会表现不佳--它们真的希望与你的键盘和屏幕对话;不要重定向它们或尝试使用"&"在后台运行它们。 否则会挂起终端。

7.仅将 stderr 重定向到管道中(高级!)

如何只将 stderr 重定向到管道中,而让 stdout 进入终端? 这很棘手;在管道的左边,你必须交换 stdout(连接到管道)和 stderr(连接到终端)。 你需要一个临时输出单元(使用 "3")来记录并记住终端的位置(将单元 3 重定向到与单元 2 相同的位置:"3>&2"),然后将 stderr 重定向到管道中(将单元 2 重定向到与单元 1 相同的位置:"2>&1"),然后将 stdout 重定向到终端(将单元 1 重定向到与单元 3 相同的位置:"1>&3"):

$ ls /etc/passwd nosuchfile 3>&2 2>&1 1>&3 | wc # 切换 STDOUT 和 STDERR
/etc/passwd # STDOUT 显示在终端上
1 9 56 # STDERR 进入管道

即使在脚本中,也很少需要使用这种高级技巧。


THE END
分享
海报
Unix Shell I/O重定向
Unix Shell I/O 重定向(包括管道) 原文:270_redirection (idallen.com) 作者:Ian! D. Allen - idallen@idallen.ca - 译者:Alan 目录 1.输出重定向 - 标准输出和标准错误 1.1 将输出重定向到文件中 1.1.1 标准输出("stdout")和标准错误("stderr) 1.2 ……
<<上一篇
下一篇>>