web-dev-qa-db-ja.com

bashでループしながら安全に終了する

次のようなbashスクリプトがあるとします。

while :
do
    foo
done

このスクリプトをコンソールから実行し、2回のfooの実行の間に発生する限り、任意の時間にスクリプトを終了できるようにしたいと思います。したがって、たとえば、私が Ctrl+C (スクリプトを終了させる別のアクションである可能性があります。 Ctrl+C 単なる例です)fooが実行された後、次の利用可能なポイントで終了します

while :
do
    foo
    if [pressed_ctrl_c]:
        break
done
7
Henry Henrinson

次のような構成を試すことができます。

#!/bin/bash
#
INTR=
trap 'INTR=yes; echo "** INTR **" >&2' INT

while :
do
    (
        # Protect the subshell block
        trap '' INT

        # Protected code here
        echo -n "The date/time is: "
        sleep 2
        date
        read -t2 -p 'Continue (y/n)? ' YN || echo
        test n = "$YN" && echo "Asked for BREAK" >&2 && exit 90
    )
    SS=$?
    test 90 -eq $SS && echo "Matched BREAK" >&2 && break

    # Ctrl/C, perhaps?
    test yes = "$INTR" && echo "Matched INTR" >&2 && break
done
exit 0

注意事項

  • readtestのペアは、( ... )ブロック内の保護されたコードセグメントへのインタラクティブな制御を示しています。
  • exit 90breakと同等ですが、サブシェルの内部からのものです。サブシェルブロックが終了した直後のtest 0 != $? ...行は、exit 90ステータスをキャプチャし、コードが実際に必要としたbreakを実装するためにあります。
  • サブシェルは、さまざまな終了ステータス値を使用して、さまざまなタイプの必要な制御フロー(breakexitなど)を示すことができます。
  • これは、プログラムが独自のシグナルハンドラーをインストールすることを妨げるものではありません。たとえば、gdbSIGINTの独自のハンドラーをインストールします(CtrlC)。ユーザーがセッションから抜け出さないようにすることが目的の場合、割り込みキーを変更すると、状況がわかりにくくなる可能性があります(以下のコードを参照)。エレガントではありませんが、潜在的に効果的です。

端末のSIGINTキーを変更する

G=$(stty -g)                    # Save settings
test -n "$G" && stty intr ^A    # That is caret and A, not Ctrl/A

# ... SIGINT generated with Ctrl/A rather than Ctrl/C ...

test -n "$G" && stty "$G"       # Restore original settings
5
roaima

これはうまくいくようです:

#!/bin/sh
pressed_ctrl_c=
trap "pressed_ctrl_c=1" INT
while true
do
        (trap "" INT; foo)&
        wait  ||  wait
        if [ "$pressed_ctrl_c" ]
        then
                # echo
                break
        fi
done
  • pressed_ctrl_cをnullに初期化します。これはおそらくスクリプトでは必要ありません。
  • trap command signumは、信号番号をキャッチするように設定するようにシェルに指示しますsignumそしてそれがキャッチしたときにcommandを実行します。 「INT」は「SIGINT」の略で、「interruptsignal」の略です。これはによって生成される信号の専門用語です。 Ctrl+Cなので、入力すると Ctrl+C、シェルはpressed_ctrl_cを1に設定します(SIGINTの数値は2なので、タイピングを節約したい場合はtrap "pressed_ctrl_c=1" 2と言うことができます)。
  • ループ内には、括弧内にコマンドラインがあります。これにより、サブシェルが作成されます。
  • 再びtrapコマンドを使用します。今回はcommandはnull文字列です。これにより、シェルは信号番号を無視するようになりますsignum。これは括弧内にあるため、サブシェルにのみ影響します。
  • fooを実行します。サブシェルから実行されたため、fooは無視します Ctrl+Cつまり、入力しても実行され続けます。
  • サブシェルをバックグラウンドに配置します…。
  • …そしてそれが完了するのを待ちます。
  • waitコマンドが成功した場合は、ifステートメントに進みます。失敗した場合は、別のを実行します。 (これに戻ります。)
  • $pressed_ctrl_cが設定されている場合は、ループを抜けます。必要に応じて、echoコマンドのコメントを解除します。 Ctrl+C ターミナルに^Cと表示され、次の行に移動します。

コマンドをバックグラウンドで実行し、すぐにwaitを実行します。これは、フォアグラウンドでコマンドを実行するのとよく似ています(少なくとも、スクリプトで実行した場合)。 waitコマンドは、サブシェルが終了すると正常に終了します。つまり、fooコマンドが終了したときです。 (waitがエラーを返しても、fooコマンドは正常に終了します。)最初のwaitコマンドが正常に終了すると、2番目のコマンドをスキップしてif

ループを実行しているシェルは割り込みをキャッチしていますが、サブシェル、つまりfooプロセスは割り込みを無視しています。したがって、入力すると Ctrl+C、シェルはpressed_ctrl_cを1に設定し、(最初の)waitコマンドを中止します。最初のwaitコマンドが失敗したため、2番目のコマンドに進みます。 fooサブシェルはまだ実行中であるため、このwaitにはまだ処理が必要です(つまり、fooが完了するまで待機します)。

最後に、変数が次のことを示すように設定されている場合 Ctrl+Cfooの実行中に押された場合、ループの出力を中断します。

を押すと Ctrl+C 2回、2番目のコマンドは2番目のwaitコマンドを中断して中止します。これにより、fooをバックグラウンドで実行したまま、スクリプトが終了してシェルプロンプトに戻ります。これを軽減するには、wait  ||  wait  ||  wait  ||  waitと言います。必要なだけ拡張します。入力する必要があります Ctrl+Cwaitごとに1回、スクリプトを途中で終了します。

D’oh!

この問題は、スクリプトによってバックグラウンドに置かれるプロセスの標準入力が/dev/nullに設定されていることです。 fooがキーボードから読み取られる場合は、上記を修正する必要があります。

4
Scott