下面的脚本是该问题的最小(尽管是人为的)说明。
## demo.sh
exitfn () {
printf -- 'exitfn PID: %d\n' "$$" >&2
exit 1
}
printf -- 'script PID: %d\n' "$$" >&2
exitfn | :
printf -- 'SHOULD NEVER SEE THIS (0)\n' >&2
exitfn
printf -- 'SHOULD NEVER SEE THIS (1)\n' >&2
在此示例脚本中,exitfn
代表一个函数,其工作需要终止当前进程1。
不幸的是,作为实施,exitfn
并不能可靠地完成这个任务。
如果运行此脚本,输出如下所示:
% bash ./demo.sh
script PID: 26731
exitfn PID: 26731
SHOULD NEVER SEE THIS (0)
exitfn PID: 26731
(当然,每次调用显示的 PID 值都会不同。)
这里的关键点是,在第一次调用该exitfn
函数时,exit 1
其主体中的命令无法终止封闭脚本printf
的执行(紧随其后的第一个命令的执行证明了这一点)。相反,在第二次调用 时exitfn
,此命令确实使脚本的执行结束(如第二个命令未执行exit 1
的事实所证明的)。printf
两次调用之间的唯一区别exitfn
是第一个调用作为双组件管道的第一个组件出现,而第二个调用是“独立”调用。
我对此感到困惑。我曾预计这exit
会导致终止当前进程(即具有由 给出的 PID 的进程$$
)。显然,这并不总是正确的。
尽管如此,有没有一种方法可以exitfn
让它退出周围的脚本,即使在管道中调用时也是如此?
顺便说一句,上面的脚本也是一个有效的 zsh 脚本,并产生相同的结果:
% zsh ./demo.sh
script PID: 26799
exitfn PID: 26799
SHOULD NEVER SEE THIS (0)
exitfn PID: 26799
我也对 zsh 的这个问题的答案感兴趣。
最后,我应该指出,exitfn
像这样实现根本行不通:
exitfn () {
printf -- 'exitfn PID: %d\n' "$$" >&2
exit 1
kill -9 "$$"
}
...因为在任何情况下,该exit 1
命令始终是该函数执行的最后一行。(替换 exit 1
为kill -9 $$
是不可接受的:我想控制脚本的退出状态,以及它到 stderr 的输出。)
1实际上,这样的功能会在终止当前进程之前执行其他任务,例如诊断日志记录或清理操作。
“当前进程”与“PID由 给出的进程”不是一回事
$$
。exit
退出当前subshell或原始 shell(如果未在子 shell 中调用)。某些构造,例如
( … )
(使用子 shell 分组)的内容、命令替换($(…)
或`…`
)和管道的每一侧(或仅在某些 shell 中为左侧),在子 shell 中运行。子 shell 的行为就好像它是使用创建的单独进程fork()
,并且通常以这种方式实现(某些 shell 在某些情况下不使用子进程,作为性能优化)。子shell有自己的变量副本,自己的重定向等,调用exit
退出子shell,就像exit()
标准C库中的函数退出进程一样。有关子外壳的更多信息,请参阅什么是子外壳(在 make 文档的上下文中)?,“子外壳”和“子进程”之间的确切区别是什么?$() 是一个子外壳吗?.$$
始终是原始 shell 进程的进程 ID。它在子外壳中不会改变。一些 shell 有一个在子 shell 中变化的变量,例如$BASHPID
在 bash 和 mksh 中,或者${.sh.subshell}
在 ksh 或$ZSH_SUBSHELL
zsh¹$sysparams[pid]
中。不完全是。您将需要做更多的工作,或者在脚本创建子 shell 的地方,或者在顶层,或者两者兼而有之。有关近似值,请参阅从子 shell 退出 shell 脚本。
¹ 请注意,尽管与其他 shell 相反,
zsh
它在父进程中的管道中执行扩展(从左到右),而不是在管道的每个成员中执行扩展:zsh -c 'echo $ZSH_SUBSHELL | cat'
输出0
(即使echo
在子进程中运行)和zsh -c 'n=0; echo $((++n)) | echo $((++n))'
输出 2。