如何在 shell 腳本中使用使用者提示處理 SIGINT 陷阱?
我正在嘗試以這樣一種方式處理 SIGINT/CTRL+C 中斷,如果使用者不小心按了 ctrl-c,則會提示他一條消息,“你想退出嗎?(y/n)”。如果他輸入yes,則退出腳本。如果否,則從發生中斷的地方繼續。基本上,我需要 Ctrl-C 以類似於 Ctrl-Z/SINTSTP 的方式工作,但方式略有不同。我已經嘗試了各種方法來實現這一點,但我沒有得到預期的結果。以下是我嘗試過的幾個場景。
情況1
腳本:play.sh
#!/bin/sh function stop() { while true; do read -rep $'\nDo you wish to stop playing?(y/n)' yn case $yn in [Yy]* ) echo "Thanks for playing !!!"; exit 1;; [Nn]* ) break;; * ) echo "Please answer (y/n)";; esac done } trap 'stop' SIGINT echo "going to sleep" for i in {1..100} do echo "$i" sleep 3 done echo "end of sleep"
當我執行上面的腳本時,我得到了預期的結果。
輸出:
$ play.sh going to sleep 1 ^C Do you wish to stop playing?(y/n)y Thanks for playing !!! $ play.sh going to sleep 1 2 ^C Do you wish to stop playing?(y/n)n 3 4 ^C Do you wish to stop playing?(y/n)y Thanks for playing !!! $
案例:2 我將 for 循環移動到一個新的腳本 loop.sh,因此 play.sh 成為父程序,而 loop.sh 成為子程序。
腳本:play.sh
#!/bin/sh function stop() { while true; do read -rep $'\nDo you wish to stop playing?(y/n)' yn case $yn in [Yy]* ) echo "Thanks for playing !!!"; exit 1;; [Nn]* ) break;; * ) echo "Please answer (y/n)";; esac done } trap 'stop' SIGINT loop.sh
腳本:loop.sh
#!/bin/sh echo "going to sleep" for i in {1..100} do echo "$i" sleep 3 done echo "end of sleep"
這種情況下的輸出與預期不符。
輸出:
$ play.sh going to sleep 1 2 ^C Do you wish to stop playing?(y/n)y Thanks for playing !!! $ play.sh going to sleep 1 2 3 4 ^C Do you wish to stop playing?(y/n)n $
我知道當一個程序收到 SIGINT 信號時,它會將信號傳播給所有子程序,因此我的第二種情況失敗了。有什麼方法可以避免 SIGINT 被傳播到子程序,從而使 loop.sh 完全按照它在第一種情況下的工作方式工作?
注意: 這只是我實際應用的一個例子。我正在處理的應用程序在 play.sh 和 loop.sh 中有幾個子腳本。我應該確保收到 SIGINT 的應用程序不應該終止,但它應該提示使用者一條消息。
關於管理工作和信號的經典問題,有很好的例子!我開發了一個精簡的測試腳本,專注於信號處理的機制。
為此,在後台啟動子程序 (loop.sh) 後,呼叫
wait
,並在收到 INT 信號後,呼叫kill
PGID 等於您的 PID 的程序組。
- 對於有問題的腳本
play.sh
,這可以通過以下方式完成:- 在
stop()
函式中替換exit 1
為kill -TERM -$$ # note the dash, negative PID, kills the process group
- 作為後台程序啟動
loop.sh
(這裡可以啟動多個後台程序並由 管理play.sh
)loop.sh &
- 在腳本末尾添加
wait
以等待所有孩子。wait
當您的腳本啟動一個程序時,該子程序將成為程序組的成員,其 PGID 等於
$$
父 shell 中父程序的 PID。例如,腳本在後台
trap.sh
啟動了三個sleep
程序,現在正在wait
執行它們,請注意程序組 ID 列(PGID)與父程序的 PID 相同:PID PGID STAT COMMAND 17121 17121 T sh trap.sh 17122 17121 T sleep 600 17123 17121 T sleep 600 17124 17121 T sleep 600
在 Unix 和 Linux 中,您可以通過
kill
呼叫PGID
. 如果你給出kill
一個負數,它將被用作-PGID。由於腳本的 PID ($$
) 與它的 PGID 相同,因此您可以kill
在 shell 中使用kill -TERM -$$ # note the dash before $$
您必須提供信號編號或名稱,否則某些 kill 實現會告訴您“非法選項”或“無效信號規範”。
下面的簡單程式碼說明了所有這些。它設置一個
trap
信號處理程序,產生 3 個孩子,然後進入一個無限等待循環,等待kill
信號處理程序中的程序組命令殺死自己。$ cat trap.sh #!/bin/sh signal_handler() { echo read -p 'Interrupt: ignore? (y/n) [Y] >' answer case $answer in [nN]) kill -TERM -$$ # negative PID, kill process group ;; esac } trap signal_handler INT for i in 1 2 3 do sleep 600 & done wait # don't exit until process group is killed or all children die
這是一個範例執行:
$ ps -o pid,pgid,stat,args PID PGID STAT COMMAND 8073 8073 Ss /bin/bash 17111 17111 R+ ps -o pid,pgid,stat,args $
好的,沒有額外的程序在執行。啟動測試腳本,中斷它(
^C
),選擇忽略中斷,然後掛起它(^Z
):$ sh trap.sh ^C Interrupt: ignore? (y/n) [Y] >y ^Z [1]+ Stopped sh trap.sh $
檢查正在執行的程序,注意程序組號(
PGID
):$ ps -o pid,pgid,stat,args PID PGID STAT COMMAND 8073 8073 Ss /bin/bash 17121 17121 T sh trap.sh 17122 17121 T sleep 600 17123 17121 T sleep 600 17124 17121 T sleep 600 17143 17143 R+ ps -o pid,pgid,stat,args $
將我們的測試腳本帶到前台(
fg
)並再次中斷(^C
),這次選擇不忽略:$ fg sh trap.sh ^C Interrupt: ignore? (y/n) [Y] >n Terminated $
檢查正在執行的程序,不再休眠:
$ ps -o pid,pgid,stat,args PID PGID STAT COMMAND 8073 8073 Ss /bin/bash 17159 17159 R+ ps -o pid,pgid,stat,args $
關於你的外殼的注意事項:
我必須修改你的程式碼才能讓它在我的系統上執行。您
#!/bin/sh
在腳本中擁有第一行,但腳本使用 /bin/sh 中不可用的副檔名(來自 bash 或 zsh)。