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

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

私は、ユーザーが誤って Ctrl + C を押した場合に、「終了しますか?(y/n)」というメッセージが表示されるように ( ) を処理しようとしています。ユーザーが「はい」と入力すると、スクリプトが終了します。「いいえ」の場合は、割り込みが発生した場所から続行します。基本的に、私はSIGINT( )と同様に作業する必要がありますが、少し異なる方法で作業する必要があります。これを実現するためにさまざまな方法を試しましたが、期待どおりの結果が得られませんでした。以下は、私が試したいくつかのシナリオです。CtrlCCtrlCSIGTSTPCtrlZ

ケース: 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.shplay.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
$

プロセスがシグナルを受信すると、そのシグナルがすべての子プロセスに伝播されるため、2 番目のケースは失敗します。子プロセスに伝播されることを回避して、1 番目のケースとまったく同じように動作させるSIGINT方法はありますか?SIGINTloop.sh

注記: これは実際のアプリケーションの例にすぎません。私が取り組んでいるアプリケーションには、play.shおよびに複数の子スクリプトがありますloop.sh。 を受信したときにアプリケーションが終了せず、ユーザーにメッセージを表示するようにする必要がありますSIGINT

答え1

ジョブとシグナルの管理に関する素晴らしい典型的な質問と良い例です。シグナル処理の仕組みに焦点を当てるために、簡素化されたテスト スクリプトを開発しました。

これを実現するには、バックグラウンドで子プロセス (loop.sh) を起動した後、 を呼び出しwait、INT シグナルを受信すると、killPGID が 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
    

$$スクリプトがプロセスを開始すると、その子は親シェルにある親プロセスの 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 の一部の実装で「不正なオプション」または「無効なシグナル指定」というメッセージが表示されます。

以下の簡単なコードは、このすべてを示しています。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 から) を使用しています。

答え2

「プロセスが 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) または終了キー (多くの場合、Control-バックスラッシュ) を入力すると、割り込み信号または終了信号がフォアグラウンド プロセス グループ内のすべてのプロセスに送信されます。」 -- W. Richard Stevens。『Advanced Programming in the UNIX® Environment』。Addison-Wesley。1993 年。p.246。

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

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

SIGINTフォアグラウンド プロセス グループのすべてのプロセスは からを消費するためControl+C、必要に応じてそのシグナルを適切に処理または無視するようにすべてのプロセスを設計するか、サブプロセスが新しいフォアグラウンド プロセス グループになるようにして、フォアグラウンド プロセス グループになくなったために親 (シェルなど) がシグナルを認識しないようにする必要があります。

答え3

ソース内のplay.shループ ファイルは次のようになります。

source ./loop.sh

トラップにより一度終了したサブシェルは、トラップを終了しても戻ることはできません。

答え4

問題はsleep

あなたがCTRL+Cあなたは殺すsleep- あなたのではないsh (たとえあなたの構文が#!/bin/sh少し疑わしいとしても)ただし、ハンドラーをインストールしていないためloop.sh(サブシェル)、プロセス グループ全体がシグナルを受け取り、その後すぐに終了します。

トラップが必要ですloop.sh。トラップは、明示的に指定されない限り、サブシェルが開始されるたびにクリアされます。trap ''SIG親に無視される。

また、-m親プロセスでジョブ制御の監視も必要になります。そうすることで、子プロセスを追跡できるようになります。waitたとえば、はジョブ制御でのみ機能します。-m監視モードは、シェルが端末と対話する方法です。

sh  -cm '  . /dev/fd/3' 3<<""                     # `-m`onitor is important
     sh -c ' kill -INT "$$"'&                     # get the sig#
     wait %%;INT=$?                               # keep it in $INT
     trap  " stty $(stty -g;stty -icanon)" EXIT   # lose canonical input
     loop()( trap    exit INT                     # this is a child
             while   [ "$#" -le 100 ]             # same as yours
             do      echo "$#"                    # writing the iterator
                     set "" "$@"                  # iterating
                     sleep 3                      # sleeping
             done
     )
     int(){  case    $1      in                   # handler fn
             ($INT)  printf  "\33[2K\rDo you want to stop playing? "
                     case    $(dd bs=1 count=1; echo >&3)  in
                     ([Nn])  return
             esac;   esac;   exit "$1"
     }       3>&2    >&2     2>/dev/null
     while   loop ||
             int "$?"
     do :;   done

0
1
2
3
4
Do you want to stop playing? n
0
1
2
3
Do you want to stop playing? N
0
1
Do you want to stop playing? y
[mikeserv@desktop tmp]$

関連情報