下面的脚本是问题的最小(尽管是人为的)说明。
## 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 为 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
withkill -9 $$
是不可接受的:我想控制脚本的退出状态及其对 stderr 的输出。)
1实际上,此类函数会在终止当前进程之前执行其他任务,例如诊断日志记录或清理操作。
答案1
我原以为这
exit
会杀死当前进程(即具有由 给出的 PID 的进程$$
)
“当前进程”与“PID 给定的进程”不是一回事$$
。exit
退出当前的子外壳,或者原始 shell(如果未在子 shell 中调用)。
某些构造,例如( … )
(与子 shell 分组)、命令替换($(…)
或`…`
)以及管道的每一侧(或某些 shell 中仅左侧)的内容,在子 shell 中运行。子 shell 的行为就好像它是使用以下命令创建的单独进程fork()
,并且通常是这样实现的(某些 shell 在某些情况下不使用子进程,作为性能优化)。子shell有自己的变量副本、自己的重定向等。调用exit
退出子shell,就像exit()
标准C库中的函数退出进程一样。有关子 shell 的更多信息,请参阅什么是子 shell(在 make 文档的上下文中)?,“子shell”和“子进程”之间的确切区别是什么?和$() 是子 shell 吗?。
$$
始终是原始 shell 进程的进程 ID。它在子 shell 中不会改变。某些 shell 具有在子 shell 中更改的变量,例如$BASHPID
在 bash 和 mksh、${.sh.subshell}
ksh 或$ZSH_SUBSHELL
zsh$sysparams[pid]
中。
有没有一种方法可以编写
exitfn
,以便即使在管道中调用它也可以退出周围的脚本?
不完全是。您需要做更多的工作,无论是在脚本创建子 shell 的地方,还是在顶层,或者两者兼而有之。看从子 shell 退出 shell 脚本用于近似值。
¹ 请注意,与其他 shell 相反,它zsh
在父进程中的管道中执行扩展(从左到右),而不是在管道的每个成员中执行扩展:zsh -c 'echo $ZSH_SUBSHELL | cat'
输出0
(即使echo
在子进程中运行)和zsh -c 'n=0; echo $((++n)) | echo $((++n))'
输出 2。