ctrl + cを正しく処理するために私のシェルを取得しようとしています。

ctrl + cを正しく処理するために私のシェルを取得しようとしています。

Ctrl Cを押したときに実行中のプログラムがSIGINTを受け取るようにシェルを設定する方法を理解しようとしていますが、bashを実行して内部で別のプログラムを実行してからCtrl + Cを押すと、シェルはSIGINTを受け取りません。実行中のプログラムは停止しますが、bashは停止しません。

これは、他のシェルでbashを起動するときにも当てはまります。

私は任意のキーを押すとサブルーチンを実行する独自の特別なシェルプログラムを作成しました。

#! /usr/bin/env bash

set -e

while true
do
    echo press any key to run $@
    read -n 1 -r -s key
    code=$(echo $key | cat -v)
    if [[ "$code" == "^D" ]]
    then
        echo bye
        exit 0
    fi
    echo running $@
    $@ || echo oops
done

これがすることです:

$ ./shell.sh sleep 1
press any key to run sleep 1
running sleep 1
press any key to run sleep 1
running sleep 1
press any key to run sleep 1
bye

任意のキーを押すと、引数として渡されたプログラムが実行されます。次に、プロンプトでCtrl + Dを押して終了します。

ただし、Ctrl + Cを使用して睡眠プログラムを停止しようとすると、次のようになります。

$ ./shell.sh sleep 10
press any key to run sleep 10
running sleep 10
^C
$

また、シェルも停止します。これは予想される現象です。

ただし、bash単独で実行すると、このように動作しません。

私はインタラクティブbashのように動作したいと思います。 「sleep」コマンドを終了し、プロンプトから続行します。

私も-mこれが成功した場合に備えてこれを実行してみました。

$ bash -m shell.sh sleep 10
press any key to run sleep 10
running sleep 10
^C$

しかし、それも終了します。

だから、2つの質問があります。 (おそらく同じ答えがあるでしょう。)

  • ctrl + cを押したときにbashがインタラクティブに実行され終了しないようにするにはどうすればよいですか?サブルーチンが終了し、プロンプトを続行できるようにするだけです。

  • Bashは内部的に何をしていますか?たとえば、上記の質問に答えるbashオプションがないので、Pythonで同じシェルプログラムを作成したい場合(私も作成しましたがここには含まれていません)、どうすればいいですか?シグナルを渡して動作させることができることを知っていますが、これを行う方法が実際のシェルが実行するのと同じであることを知りたいです。

私は「シェル」でシグナルを処理して動作させることができると思いましたが、実際のシェルがどのように機能するのかわかりません。私の例にリンクされています。 - または少なくとも私の例に接続されていない理由の説明があります。

ベストアンサー1

予備説明

「特殊シェルプログラム」には主な問題とは関係のないいくつかの問題がありますが、プログラムを引き続き使用するには問題を解決することをお勧めします。

  1. 正しい引用

  2. 拡張機能$@"$@"上記を参照)をコマンドに渡すと、ユーザーはシェル自体の状態を解釈する組み込み機能の変更(try./shell.sh set -xまたは)を含むシェル組み込み機能を実行できます./shell.sh exit。少なくとも"$@"サブシェルで実行します(シェル組み込み機能を許可します)、サブシェルexec -- "$@"で実行することをお勧めします(そうではありません)。

  3. echo固定文字列をエコーし​​たくない場合は避けてください。使用printf

  4. あなたは必要ありませんcat -vBashはEOT文字(^D)を生成できます。そしてあなたの$keyものと比較してください。

[[ここも必要なくて[大丈夫(彼らは同等ではありませんしかし)。文字列比較の基本演算子は、[Bash=が次のものを理解することができますが、==他のシェルではこれを理解することも理解できないかもしれません。使いやすく、十分な場所で使用しないのが私の個人的な好みです。これにより、移植不可能なコードに慣れておらず、Bashほど豊富でないシェルで何ができるかがわかります。はっきり言えば、これは[[[=[[[私の個人的な好み[[コードに問題はありません。

以下は改善されたスクリプトです(ただし、元の(error)と同様にCtrl+を処理し続けるので、c後で修正される予定です)。

#! /usr/bin/env bash
set -e
while true
do
    printf 'press any key to run %s\n' "${*@Q}"
    read -n 1 -r -s key
    if [ "$key" = $'\cD' ]
    then
        echo bye
        exit 0
    fi
    printf 'running %s\n' "${*@Q}"
    (exec -- "$@") || echo oops
done

コードも$*適切な場合とともに@Q修飾子コマンドを明示的に印刷します。./shell.sh echo "a b"元のスクリプトの動作と比較してみてください。

それでは、ポイントを見てみましょう。


ポイントを言うと

Ctrl+c端末のラインルールは、(通常)フォアグラウンドプロセスグループのプロセスに SIGINT を送信します。シェルはグループ内にある場合と存在しない場合があるため、信号を受信したり受信したりすることはできません。信号を中継しません。

前景プロセスグループは、現在の制御端末で「前景にある」プロセスグループです。通常、これは制御端末に、どのグループが前景にあるのかを常に知らせるインタラクティブシェルです。

一般的に言えば、子プロセスが実行されている将来の親プロセス(「シェル」など)は、新しいプロセスが同じプロセスグループ(以前のプロセス、親プロセスなど)にあるのか、新しいプロセスグループにあるのかを判断する必要があります。この回答をご覧ください。実行中のプロセスのプロセスグループを変更する方法はありますか?。新しいプロセスグループがフォアグラウンドプロセスグループでなければならない場合は、端末に通知する必要があります;後で子プロセスが終了した場合は、前景プロセスグループを親プロセスグループにリセットする必要があります。

したがって、「シェル」がプロセスグループを処理するかどうか、および方法によってCtrl+cによってSIGINTがフォアグラウンドプロセスグループに送信されると、信号が転送されます。

  • 子プロセスの(新しい)グループがフォアグラウンドプロセスグループで、シェルが別の(通常は古い)プロセスグループにある場合は、子プロセスに送信されますが、「シェル」に送信しないでください。 (一部または全部)子孫の一部または全部が信号を受信します。
  • フォアグラウンドプロセスグループにある場合は、「シェル」とサブプロセス(+子孫)に適用されます。これを達成する最も簡単な方法は、サブプロセスグループを変更しないことです。プロセスグループの概念は存在しません)。
  • 「シェル」と指定しますが、別のプロセスグループにあり、前景プロセスグループが「シェル」の1つである場合は、子プロセス(+子孫)として指定しません。

これは、「シェル」を回避しCtrlc中断する1つの方法は、「シェル」がフォアグラウンドプロセスグループに存在しないようにすることです。

別のアプローチは、「シェル」が前景プロセスグループに配置され、信号を処理できるようにすることである。あなたのオプションは次のとおりです。

  • あなたの「シェル」はおそらく特別な仕事をしません。 SIGINTはそれを終了します。これがあなたが避けたいものです。厳密に言うと、「何もしない」「シェル」は、親から継承された無視マスクがSIGINTを無視しますが、これは極端な場合です。
  • あなたの「シェル」は、SIGINTを無視するように明示的に選択できます。スクリプトがによって解釈された場合、サブプロセスがSIGINTを無視しないようにするには(継承のため)、サブシェル(;)から元の設定にリセットする必要がありますbash。ただし、「元の構成」では、「inには項目に「シェル」値があるため、極端な場合、子がSIGINTを無視しないようにすることはできません。trap '' INTexec(trap - INT; exec -- "$@")
  • 「シェル」はSIGINTをキャプチャし、いくつかの操作を実行できます。スクリプトが解釈される場合は、ランダム()またはスペースがあるかもしれませんが、空の文字列はbash必要ありません。子供はまるで罠が存在しないかのように振る舞うでしょう。 Bashはシェルに入ったときに無視される信号をキャプチャまたはリセットすることはできません。trap 'shell code' INTshell code:

トラップを含むスクリプトは次のとおりです。

#! /usr/bin/env bash
set -e
trap 'echo "the shell got SIGINT"' INT
while true
do
    printf 'press any key to run %s\n' "${*@Q}"
    read -n 1 -r -s key
    if [ "$key" = $'\cD' ]
    then
        echo bye
        exit 0
    fi
    printf 'running %s\n' "${*@Q}"
    (exec -- "$@") || echo oops
done

さまざまな段階で+を./shell.sh sleep 10押してCtrl実行して実験します。実行時に+のSIGINT信号がそれに達して中断されるcことに注意してください。sleepCtrlc返品解釈シェルに入ります(トラップが実行されていることがわかります)。これは、同じプロセスグループで実行され、bash両方のプロセスがSIGINTを受け取るためです。sleep

-m実行して強制(ジョブ制御)を実行すると、bash -m shell.sh sleep 10+のSIGINTは転送プロセス中に解析シェルに到達しますが、転送プロセス中にインタプリタシェルには到達しません。別のプロセスグループで実行されるためです(確認できます)。 Bashに組み込まれた関数です。前景にあれば前景にもあります。前景にいるときはそうではありません。 SIGINT到着時に出荷されません。 SIGINTが到着するとトラップは役に立ちませんが、SIGINTが到着するとトラップはまだ役に立ちます。トラップでない場合、+ピリオドは全体を中断します。Ctrlcreadsleep-m bashsleepps -ao pid,pgrp,cmdreadbashsleepbashsleepbashsleepbashreadCtrlcreadbash

やや驚くべきことに、私たちのものbash -m(特に私がテストしたBash 5.2.15)するsleepSIGINTで殺されたら終了します。echo oopsシェル自体がSIGINTによって終了したように、またはシェルが通常実行する最後の実行コマンドの終了状態を渡すように、130の終了状態で実行せずに終了しますsleep。 SIGINTによって殺されます。

この行動は実際に意味があります。この記事ではこれについて説明します。SIGINT/SIGQUITを正しく処理。今は読む必要はありません。 「シェル」を書くには、いつか読んでおくべきだと思います。私たちには、Bashマニュアルから抜粋した以下の内容が関連しています。

SIGINTによってコマンドが終了すると、Bashはユーザーがスクリプト全体を終了しようとしたと結論付け、SIGINTに対してアクションを実行します(例えば、SIGINTトラップを実行したり、自己終了)。

源泉)

これは「ジョブ制御が有効になっていない場合」です(より広いスニペットが興味を持つ可能性があります)。ジョブ制御が有効になっているときに何が起こるのかについての説明が見つかりませんでしたが、私たちが観察したアクションは、bash -m「Bashはユーザーがスクリプト全体を終了しようとしていると結論付けました」がまだ適用されていると推論しました。私の説明は次のとおりです。bashSIGINTを受け取っていないという事実にもかかわらず(現在のフォアグラウンドプロセスグループにはないため)、SIGINTが終了したことがわかるsleepので、ユーザーはスクリプト全体を終了しようとしたと結論付けます。実際に信号を受信しないため、トラップを実行しません。自分で終了せず( で確認しましたstrace)、終了状態のみを渡しますsleep

「シェル」を作成するときは、シェルのプロセスグループでコマンドを実行するかどうかを決定します。 SIGINTを処理します。その後、「シェル」が最後のコマンドが SIGINT によって終了されたのか、それとも終了したのかを検出したいと思うかもしれません。通常、実際にSIGINTを処理(無視するのではなくキャッチ)し、コマンドの実行中に発生することに気づくことは、コマンドが終了した後に実行するアクションを決定するのに役立ちます。

trapはい、実際のシェルはユーザーが明示的にsを定義していない場合でも信号をキャプチャします。たとえば、Linuxでは、grep ^Sig /proc/"$$"/status次の答えに基づいてSigCgtを実行して解釈できます。プロセスがどのシグナルを聞いているかを確認する方法は?

今は読む時間ですリンクされた記事「本物の殻」が何を扱っているのか見てください。


その他の注意事項

  • Ctrl「Send SIGINT in +cターミナル回線規則(通常)」と書いています。プログラム(「シェル」で実行されるプログラムを含む)がこれを構成できるため、「通常」です(ただし、すべてのプログラムが普遍的なわけではありません)。たとえば、./shell.sh stty intr ^Dこれを複数回実行すると、+がSIGINTを生成して読み取らないstty intr ^Dため、ループを簡単に終了できません。Ctrldread^D

  • シェルでは、bash「バックグラウンドで実行」などがコマンド出口&に関連付けられていますが、&プロセスがフォアグラウンドプロセスグループで開始されないという意味ではありません。フォアグラウンドプロセスグループ、SIGTTIN、SIGTTOU、および全体的なタスク制御の概念は、制御端末への特定のプロセスアクセスを拒否し、同時に要求時にプロセスを許可する(つまりフォアグラウンドに移動する)方法ですfg。ジョブ制御がないと、開始されたプロセス&は非同期であり(シェルはプロセスが完了するのを待ちません)、標準入力がまたは/dev/null一部の同等のファイルにリダイレクトされるため、端末から「盗む」ことはできません。その意味で "in the background";ただし、フォアグラウンドプロセスグループに存在する可能性があります。たとえば、bash -c 'sleep 500 & sleep 600'新しいプロセスと2つのプロセスは単一のプロセスグループ(親プロセスの設定方法に応じて親プロセスグループまたは新しいプロセスグループ)で実行され、bashそのプロセスグループはフォアグラウンドプロセスになります。グループ。sleepbash

  • これらをテストするためにシェルスクリプトを引き続き使用するには、慎重に実行してくださいset -e。 SIGINTのためにスクリプトが終了した場合、set -eSIGINTのために終了したと仮定しやすくなります。私たちのスクリプトの場合|| echo oops

おすすめ記事