シェルスクリプトでユーザープロンプトを使用してSIGINTトラップを処理する方法は?

シェルスクリプトでユーザープロンプトを使用してSIGINTトラップを処理する方法は?

ユーザーが誤ってctrl-cを押したときに「終了しますか?(y / n)」というメッセージが表示されるように処理しようとしていSIGINTます。CtrlCyes と入力するとスクリプトが終了します。そうでない場合は、中断が発生した時点から続行してください。基本的に()とCtrlC同じように作業する必要がありますが、少し異なる方法で作業する必要があります。これを達成するためにさまざまな方法を試しましたが、期待した結果は得られませんでした。以下は私が試したいくつかのシナリオです。SIGTSTPCtrlZ

ケース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.shloop.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ユーザーにメッセージを表示する必要があることを確認する必要があります。

ベストアンサー1

素晴らしい例と一緒に仕事と信号管理に関する古典的な質問です!私は信号処理メカニズムに焦点を当てるために合理化されたテストスクリプトを開発しました。

これを行うには、バックグラウンドで子プロセス(loop.sh)を起動して呼び出しwait、INT信号を受信した後、killPIDと同じPGIDを使用してプロセスグループを呼び出します。

  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
    

$$スクリプトがプロセスを開始すると、子プロセスは親シェルの親プロセスのPIDと同じPGIDを持つプロセスグループのメンバーになります。

たとえば、スクリプトはバックグラウンドでtrap.sh3つのプロセスを開始し、現在のプロセスグループID列(PGID)が親プロセスのPIDと同じであることに注意してください。sleepwait

  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

killUnixとLinuxでは、負の値で呼び出してプロセスグループ内の各プロセスにシグナルを送信できますPGID。負の数を指定すると、kill-PGID として使用されます。スクリプトのPID($$)はPGIDと同じであるため、killシェルでプロセスグループを使用できます。

kill -TERM -$$    # note the dash before $$

信号番号または名前を入力する必要があります。それ以外の場合、kill の一部の実装では、「無効なオプション」または「無効な信号仕様」と通知されます。

以下の簡単なコードはすべてを説明します。シグナルハンドラを設定し、3つのサブプロセスを作成し、シグナルハンドラのプロセスグループコマンドが自分で終了するのをtrap待つ無限待機ループに入ります。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)を使用します。

おすすめ記事