在这种特殊情况下,您有exec
一个管道。为了执行一系列管道命令,shell 必须首先 fork,制作一个子 shell。(具体来说,它必须创建管道,然后分叉,以便在管道“左侧”运行的所有内容都可以将其输出发送到管道“右侧”的任何内容。)
要查看这实际上正在发生什么,请比较:
{ ls; echo this too; } | cat
和:
{ exec ls; echo this too; } | cat
前者在ls
不离开子 shell 的情况下运行,因此这个子 shell 因此仍然在周围运行echo
. 后者ls
通过离开子外壳运行,因此不再存在执行echo
, 并且this too
不打印。
(大括号的使用{ cmd1; cmd2; }
通常会抑制您使用括号获得的子外壳叉子操作(cmd1; cmd2)
,但在管道的情况下,叉子是“强制”的,就像它一样。)
当前 shell 的重定向只有在单词exec
. 因此,例如,exec >stdout 4<input 5>>append
修改当前 shell,但exec foo >stdout 4<input 5>>append
尝试执行 command foo
。[注意:这并不完全准确;见附录。]
有趣的是,在交互式 shell 中,exec foo >output
由于没有 command 失败后foo
,shell 仍然存在,但 stdout 仍然重定向到 file output
。(您可以使用 恢复exec >/dev/tty
。在脚本中,失败会exec foo
终止脚本。)
向@Pumbaa80 致敬,这里有一些更能说明问题的东西:
#! /bin/bash
shopt -s execfail
exec ls | cat -E
echo this goes to stdout
echo this goes to stderr 1>&2
(注意:cat -E
从我通常的简化cat -vET
,这是我方便的“让我以可识别的方式看到非打印字符”的方法)。运行此脚本时,ls
已cat -E
应用来自的输出(在 Linux 上,这使行尾显示为 $ 符号),但发送到 stdout 和 stderr 的输出(在其余两行上)未重定向。更改| cat -E
为> out
,脚本运行后,观察 file 的内容out
:最后两个echo
s 不在其中。
现在更改ls
to foo
(或其他一些找不到的命令)并再次运行脚本。这次的输出是:
$ ./demo.sh
./demo.sh: line 3: exec: foo: not found
this goes to stderr
并且该文件out
现在具有第一echo
行生成的内容。
这使得exec
“真正做”的事情尽可能明显(但不是更明显,因为阿尔伯特爱因斯坦没有说:-))。
通常,当 shell 执行“简单命令”时(请参阅手册页以了解精确定义,但这特别排除了“管道”中的命令),它会准备任何使用<
、>
等指定的 I/O 重定向操作打开所需的文件。然后外壳调用fork
(或一些等效但更有效的变体,例如vfork
或clone
取决于底层操作系统、配置等),并在子进程中重新排列打开的文件描述符(使用dup2
调用或等效)以实现所需的最终安排:> out
将打开的描述符移动到 fd 1——stdout——同时6> out
将打开的描述符移动到 fd 6。
但是,如果您指定exec
关键字,shell 将禁止该fork
步骤。它像往常一样执行所有文件打开和文件描述符重新排列,但这一次,它会影响任何和所有后续命令。最后,在完成所有重定向之后,shell 会尝试execve()
(在系统调用意义上)该命令(如果有的话)。如果没有命令,或者execve()
调用失败并且shell 应该继续运行(是交互式的或者你已经设置execfail
了),shell 士兵就会启动。如果execve()
成功,则 shell 不再存在,已被新命令替换。如果execfail
未设置且 shell 不是交互式的,则 shell 退出。
(还有shell函数的附加复杂性:根据测试结果,command_not_found_handle
bash似乎禁止运行它。关键字通常使shell不查看自己的函数,即如果你有一个shell函数f,作为一个简单命令运行 shell 函数,就像在子 shell 中运行它一样,但运行会跳过它。)
exec
exec
f
(f)
(exec f)
至于为什么要
ls>out1 ls>out2
创建两个文件(有或没有
exec
),这很简单:shell 打开每个重定向,然后用于
dup2
移动文件描述符。如果您有两个普通
>
重定向,shell 打开两个,将第一个移动到 fd 1(stdout),然后将第二个移动到 fd 1(再次 stdout),在此过程中关闭第一个。最后,它运行
ls ls
,因为这是删除
>out1 >out2
. 只要没有名为 的文件
ls
,该
ls
命令就会向 stderr 抱怨,并且不会向 stdout 写入任何内容。