如何編寫一個可靠地退出(具有指定狀態)目前程序的函式?
下面的腳本是該問題的最小(儘管是人為的)說明。
## 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在實踐中,此類函式會在終止目前程序之前執行其他任務,例如診斷日誌記錄或清理操作。
我曾預計這
exit
會殺死目前程序(即具有 PID 的程序$$
)“目前程序”與“PID 給定的程序”不是一回事
$$
。exit
退出目前的subshell,如果未在子shell 中呼叫,則退出原始 shell。某些構造,例如
( … )
(與子shell 分組)的內容、命令替換($(…)
或…
)以及管道的每一側(或僅在某些shell 中的左側),在子shell 中執行。子shell 的行為就好像它是用創建的單獨程序一樣fork()
,並且通常以這種方式實現(某些shell 在某些情況下不使用子程序,作為性能優化)。子shell有自己的變數副本、自己的重定向等。呼叫exit
退出子shell,就像exit()
標準C庫中的函式退出程序一樣。有關子shell 的更多資訊,請參閱什麼是子shell(在 make 文件的上下文中)?,“子shell”和“子程序”之間的確切區別是什麼?$() 是一個子外殼嗎?.
$$
始終是原始 shell 程序的程序 ID。它不會在子shell中改變。一些 shell 有一個在子 shell 中發生變化的變數,例如$BASHPID
在 bash 和 mksh 中,或者${.sh.subshell}
在 ksh 或$ZSH_SUBSHELL
zsh¹$sysparams[pid]
中。有沒有一種方法可以編寫
exitfn
,即使在管道中呼叫它也會退出周圍的腳本?不完全是。您需要做更多的工作,要麼在腳本創建子外殼的地方,要麼在頂層,或兩者兼而有之。有關近似值,請參見從子 shell 退出 shell 腳本。
¹ 請注意,儘管與其他 shell 不同,
zsh
它在父程序(從左到右)的管道中執行擴展,而不是在管道的每個成員中:zsh -c 'echo $ZSH_SUBSHELL | cat'
輸出0
(即使echo
在子程序中執行)和zsh -c 'n=0; echo $((++n)) | echo $((++n))'
輸出 2。