web-dev-qa-db-ja.com

シェルスクリプトのユーザープロンプトでSIGINTトラップを処理するにはどうすればよいですか?

ユーザーが誤ってctrl-cを押すと、「終了しますか?(y/n)」というメッセージが表示されるように、SIGINT/CTRL + C割り込みを処理しようとしています。彼が「はい」と入力した場合は、スクリプトを終了します。いいえの場合は、割り込みが発生したところから続行します。基本的に、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 !!! 
$  

Case: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シグナルを受信すると、すべての子プロセスにシグナルを伝播するため、2番目のケースが失敗することを理解しています。 SIGINTが子プロセスに伝達されないようにして、loop.shを最初のケースとまったく同じように機能させる方法はありますか?

注:これは私の実際のアプリケーションのほんの一例です。私が取り組んでいるアプリケーションでは、play.shとloop.shにいくつかの子スクリプトがあります。 SIGINTの受信時にアプリケーションが終了しないことを確認する必要がありますが、ユーザーにメッセージを表示する必要があります

6
Venkat Madhav

良い例を使ってジョブとシグナルを管理することについての古典的な質問です私は、信号処理のメカニズムに焦点を当てるために、簡略化されたテストスクリプトを開発しました。

これを行うには、バックグラウンドで子(loop.sh)を開始した後、waitを呼び出し、INT信号の受信時にkill PGIDがPIDと等しいプロセスグループ。

  1. 問題のスクリプト_play.sh_の場合、これは次の方法で実現できます。

  2. stop()関数で_exit 1_を

    _kill -TERM -$$  # note the dash, negative PID, kills the process group
    _
  3. _loop.sh_をバックグラウンドプロセスとして開始(ここで複数のバックグラウンドプロセスを開始し、_play.sh_で管理できます)

    _loop.sh &
    _
  4. スクリプトの最後にwaitを追加して、すべての子を待機します。

    _wait
    _

スクリプトがプロセスを開始すると、その子は、PGIDが親シェルの_$$_である親プロセスのPIDと等しいプロセスグループのメンバーになります。

たとえば、スクリプト_trap.sh_は3つのsleepプロセスをバックグラウンドで開始し、現在それらをwaitingしています。プロセスグループID列(PGID)は、親プロセス:

_  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

_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_)を再び中断し、今度はnotを選択して無視します。

_$ 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から)を使用しています。

5
RobertL

play.shループファイルを次のようにソースします。

source ./loop.sh

トラップが原因でいったん終了したサブシェルは、トラップの終了時に戻ることはできません。

1
ferdy

「プロセスがSIGINTシグナルを受信すると、すべての子プロセスにシグナルを伝播することを理解しています」

この間違った考えはどこから得ましたか?

$ Perl -E '$SIG{INT}=sub { say "ouch $$" }; if (fork()) { say "parent $$"; sleep 3; kill 2, $$ } else { say "child $$"; sleep 99 }'
parent 25831
child 25832
ouch 25831
$

主張されているように、伝播があった場合、子プロセスから「痛ましい」と予想された可能性があります。

実際には:

「端末の割り込みキー(多くの場合、DELETEまたはControl-C)または終了キー(多くの場合、Ctrl-バックスラッシュ)を入力すると、割り込みシグナルまたは終了シグナルがフォアグラウンドプロセスグループ内のすべてのプロセスに送信されます」-Wリチャード・スティーブンス。 「UNIX®環境での高度なプログラミング」。 Addison-Wesley。 1993. p.246。

これは、次の方法で確認できます。

$ Perl -E '$SIG{INT}=sub { say "ouch $$" }; fork(); sleep 99'
^Couch 25971
ouch 25972
$

フォアグラウンドプロセスグループのすべてのプロセスは、SIGINTを Control+C、適切にその信号を適切に処理または無視するように、またはサブプロセスが新しいフォアグラウンドプロセスグループになるようにすべてのプロセスを設計する必要があります。これにより、親(シェルなど)は信号のために信号を認識しません。フォアグラウンドプロセスグループには含まれていません。

1
thrig