需要解释Linux bash builtin exec命令行为

art*_*rxe 36 linux bash flow exec

Bash参考手册我得到以下关于execbash builtin命令:

如果提供了命令,它将替换shell而不创建新进程.

现在我有以下bash脚本:

#!/bin/bash
exec ls;
echo 123;
exit 0
Run Code Online (Sandbox Code Playgroud)

执行了,我得到了这个:

cleanup.sh  ex1.bash  file.bash  file.bash~  output.log
(files from the current directory)
Run Code Online (Sandbox Code Playgroud)

现在,如果我有这个脚本:

#!/bin/bash
exec ls | cat
echo 123
exit 0
Run Code Online (Sandbox Code Playgroud)

我得到以下输出:

cleanup.sh
ex1.bash
file.bash
file.bash~
output.log
123
Run Code Online (Sandbox Code Playgroud)

我的问题是:

如果在exec调用它取代了壳,而无需创建一个新的进程,为什么放时| cat,将echo 123被打印出来,但是没有它,它不是.所以,如果有人能解释这种行为的逻辑,我会很高兴.

谢谢.

编辑:@torek回复后,我更难解释行为:

1. exec ls>out命令创建out文件并输入ls命令结果;

2. exec ls>out1 ls>out2只创建文件,但不要放入任何结果.如果命令按照建议工作,我认为命令编号2应该与命令编号1具有相同的结果(更多,我认为它不应该创建out2文件).

tor*_*rek 39

在这种特殊情况下,你有exec一个管道.为了执行一系列管道命令,shell最初必须fork,制作一个子shell.(具体来说,它必须创建管道,然后是fork,以便管道的"左侧"运行的所有内容都可以将其输出发送到管道的"右侧".)

要看到这实际上是发生了什么,请比较:

{ ls; echo this too; } | cat
Run Code Online (Sandbox Code Playgroud)

有:

{ exec ls; echo this too; } | cat
Run Code Online (Sandbox Code Playgroud)

前者在ls不离开子壳的情况下运行,因此这个子壳因此仍然可以运行echo.后者ls通过离开子壳来运行,因此子壳不再存在echo,并且this too不打印.

(花括号的使用{ cmd1; cmd2; }通常会抑制用括号获得的子shell分叉动作(cmd1; cmd2),但是在管道的情况下,分叉是"强制"的,就像它一样.)

当前shell的重定向仅在没有"无法运行"的情况下发生,就像在单词之后一样exec.因此,例如,exec >stdout 4<input 5>>append修改当前shell,但exec foo >stdout 4<input 5>>append尝试执行exec命令foo.[注意:这不是严格准确的; 见附录.]

有趣的是,在一个交互式shell中,exec foo >output由于没有命令失败后foo,shell会一直存在,但是stdout仍会被重定向到文件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
Run Code Online (Sandbox Code Playgroud)

(注意:cat -E从我平常的简化cat -vET,这是我方便的"让我以一种可识别的方式看到非打印字符").运行此脚本时,lscat -E应用输出(在Linux上,这使得行尾可见为$符号),但发送到stdout和stderr(在其余两行上)的输出未被重定向.更改| cat -Eto > out和脚本运行后,观察文件的内容out:最后两个echo不在那里.

现在更改lsto foo(或其他一些无法找到的命令)并再次运行脚本.这次输出是:

$ ./demo.sh
./demo.sh: line 3: exec: foo: not found
this goes to stderr
Run Code Online (Sandbox Code Playgroud)

并且文件out现在具有第一echo行生成的内容.

这使得exec"真正做到"尽可能明显(但没有更明显,正如阿尔伯特爱因斯坦没有说的那样:-)).

通常,当shell执行"简单命令"(参见精确定义的手册页,但这明确排除了"管道"中的命令)时,它会准备用<,指定的任何I/O重定向操作>,等等.打开所需的文件.然后shell调用fork(或者一些等效但更有效的变体,如vforkclone取决于底层操作系统,配置等),并在子进程中重新排列打开的文件描述符(使用dup2调用或等效)以实现所需的最终安排:> out将打开的描述符移动到fd 1-stdout-同时6> out将打开的描述符移动到fd 6.

exec但是,如果指定关键字,则shell会禁止该fork步骤.它像往常一样执行所有文件打开和文件描述符重新排列,但这一次,它会影响任何和所有后续命令.最后,在完成所有重定向后,shell会尝试execve()(在系统调用意义上)命令(如果有的话).如果没有命令,或者如果execve()调用失败并且 shell应该继续运行(是交互式的或者你已经设置好execfail),那么shell士兵就可以了.如果execve()成功,则shell不再存在,已被新命令替换.如果execfail未设置且shell不是交互式的,则shell将退出.

(还有command_not_found_handleshell函数增加的复杂性:bash exec似乎根据测试结果禁止运行它.exec关键字通常使shell不看其自己的函数,即,如果你有一个shell函数f,f作为一个运行simple命令运行shell函数,就像(f)在子shell中运行它一样,但运行会(exec f)跳过它.)


至于为什么ls>out1 ls>out2创建两个文件(有或没有exec),这很简单:shell打开每个重定向,然后用于dup2移动文件描述符.如果你有两个普通的>重定向,shell打开两个,将第一个移动到fd 1(stdout),然后将第二个移动到fd 1(stdout再次),关闭过程中的第一个.最后,它运行ls ls,因为这是删除后留下的>out1 >out2.只要没有命名文件ls,该ls命令就会向stderr抱怨,并且不会向stdout写入任何内容.