Sicher beenden, während Schleifen in Bash

7

Angenommen, ich habe ein Bash-Skript, das Folgendes tut:

while :
do
    foo
done

Ich möchte in der Lage sein, dieses Skript von der Konsole aus auszuführen und es zu einem beliebigen Zeitpunkt zu beenden, solange es zwischen zwei foo-Läufen auftritt. Wenn ich also beispielsweise Ctrl+ drücke C(es könnte eine andere Aktion sein, die das Beenden des Skripts bewirkt, Ctrl+ Cist nur ein Beispiel), wird dies am nächsten verfügbaren Punkt beendet, nachdem foo ausgeführt wurde:

while :
do
    foo
    if [pressed_ctrl_c]:
        break
done
Henry Henrinson
quelle
1
Sie können sich Fallen ansehen - tutorialspoint.com/unix/unix-signals-traps.htm
rahul
Wird die Sache "trap <kbd> ctrl </ kbd> + c" sicherstellen, dass ich nicht in foo einbreche?
Henry Henrinson

Antworten:

5

Sie könnten diese Art von Konstrukt ausprobieren:

#!/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

Einige Notizen

  • Das readund test-Paar demonstriert die interaktive Steuerung des geschützten Codesegments innerhalb des ( ... )Blocks.
  • Das exit 90ist das Äquivalent von breakaber aus einer Unterschale heraus. Die test 0 != $? ...Zeile unmittelbar nach dem Ende des Unterschalenblocks dient dazu, den exit 90Status zu erfassen und das zu implementieren break, was der Code tatsächlich wollte.
  • Die Sub - Shell können verschiedene Exit - Status Werte verwenden , um verschiedene Arten der erforderlichen Steuerungsablauf angeben ( break, exit, etc ...)
  • Dies verhindert nicht, dass ein Programm seinen eigenen Signalhandler installiert. Installiert beispielsweise gdbeinen eigenen Handler für SIGINT( CtrlC). Wenn das Ziel darin besteht, zu verhindern, dass ein Benutzer aus der Sitzung ausbricht, kann das Ändern der Interrupt-Taste dazu beitragen, die Situation zu verschleiern (siehe den folgenden Code). Unelegant, aber potenziell wirksam.

Ändern der SIGINT-Taste am Terminal

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
Roaima
quelle
Sie haben die Anforderung, dass der aktuelle Aufruf von foovollständig ausgeführt werden soll, nicht erfüllt , selbst wenn <Strg> + <C> eingegeben wurde.
Scott
Ah ja. Ich werde den Code entsprechend aktualisieren, danke @Scott
Roaima
Lustige Geschichte: Das sieht sehr nach einem meiner frühen Versuche aus. Ich habe es nicht gepostet, weil es nicht funktioniert hat - weil ich es als Einzeiler in meine interaktive Shell eingegeben habe, anstatt es in eine Datei zu schreiben. Eine schnelle Möglichkeit, dies zu testen, besteht darin, das obige Skript sourcenicht auf normale Weise aufzurufen . Ich denke, Sie werden feststellen, dass <Strg> + <C> keine Wirkung hat. Ich kann es nicht erklären (können Sie?) Freundlicher Vorschlag: Ändern Sie Ihre exit 0in (exit 0)(oder einfach true), oder dies führt dazu, dass Ihre interaktive Shell beendet wird. ……… Übrigens, als Sie „ exit 1Status erfassen “ sagten, meinten Sie „… exit 90Status“?
Scott
@Scott, es soll als Teil eines Skripts und nicht über via ausgeführt werden source, vor allem, weil Sie, wenn Sie versuchen, Dinge trapdirekt in der Befehlszeile zu erledigen, alle leicht in Knoten geraten können. Vielen Dank für exit 1- ich dachte, ich hätte sie alle gefangen, als ich zu wechselte 90(ich hatte das Gefühl, dass dieser neue Wert als Fluchtkriterium "offensichtlicher" war).
Roaima
Ja, ich verstehe, dass es ein Skript sein soll. Ich dachte nur, ich könnte die Skriptbefehle schneller debuggen, indem ich es in der Befehlszeile mache, anstatt wiederholt eine Skriptdatei zu bearbeiten, zu speichern und auszuführen. Was das Binden meiner Muschel in Knoten angeht: Ich habe es in Klammern gemacht, wie (trap "pressed_ctrl_c=1" INT; while true …). …
Scott
4

Das scheint zu funktionieren:

#!/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
  • Auf pressed_ctrl_cnull initialisieren . Dies ist in einem Skript wahrscheinlich nicht erforderlich.
  • trap command signumWeist die Shell an, die Signalnummer abzufangen signum und auszuführen, commandwenn sie eine abfängt. "INT" steht für "SIGINT", was für "Interrupt Signal" steht. Dies ist der Fachbegriff für das von Ctrl+ erzeugte Signal. CWenn Sie also Ctrl+ eingeben C, wird die Shell pressed_ctrl_cauf 1 gesetzt. (SIGINT hat einen numerischen Wert von 2, so können Sie sagen, trap "pressed_ctrl_c=1" 2ob Sie beim Tippen sparen möchten.)
  • Innerhalb der Schleife haben wir eine Befehlszeile in Klammern. Dadurch wird eine Unterschale erstellt.
  • Wir verwenden den trapBefehl erneut. Diese Zeit commandist eine Nullzeichenfolge. Dies weist die Shell an, die Signalnummer zu ignorieren signum. Da dies in Klammern steht, betrifft es nur die Unterschale.
  • Ausführen foo. Da es von der Subshell ausgeführt wurde, foowird Ctrl+ ignoriert C, dh es wird auch dann weiter ausgeführt, wenn Sie es eingeben .
  • Stellen Sie die Unterschale in den Hintergrund….
  • … Und warten Sie, bis es fertig ist.
  • Wenn der waitBefehl erfolgreich ist, fahren Sie mit der ifAnweisung fort. Wenn dies fehlschlägt, führen Sie einen anderen aus. (Ich werde darauf zurückkommen.)
  • Wenn $pressed_ctrl_ceingestellt, aus der Schleife ausbrechen. Kommentieren Sie optional den echoBefehl aus, wenn Ctrl+ Cauf Ihrem Terminal als angezeigt wird ^C und Sie zur nächsten Zeile wechseln möchten.

Wir führen einen Befehl im Hintergrund aus und dann sofort waitdafür. Dies ist dem Ausführen des Befehls im Vordergrund sehr ähnlich (zumindest wenn dies in einem Skript ausgeführt wird). Der waitBefehl wird erfolgreich beendet, wenn die Subshell beendet wird. dh wenn der fooBefehl beendet wird. (Der waitBefehl wird erfolgreich beendet, auch wenn fooein Fehler zurückgegeben wird.) Wenn der erste waitBefehl erfolgreich beendet wird, überspringen wir den zweiten und gehen zum if.

Die Shell, die die Schleife ausführt, fängt Interrupts ab, aber die Subshell und damit der fooProzess ignorieren sie. Wenn Sie also Ctrl+ eingeben C, wird die Shell pressed_ctrl_cauf 1 gesetzt und der (erste) waitBefehl abgebrochen . Da der erste waitBefehl fehlgeschlagen ist, fahren wir mit dem zweiten fort. Denken Sie daran, dass die fooSubshell noch ausgeführt wird, sodass dies waitnoch etwas zu tun hat (dh es wird gewartet , bis die Subshell foofertig ist.)

Wenn die Variable so eingestellt wurde, dass angezeigt wird, dass während der Ausführung ein Ctrl+ Cgedrückt foowurde, wird die Schleife unterbrochen.

Wenn Sie zweimal Ctrl+ drücken C, unterbricht der zweite den zweiten waitBefehl und bricht ihn ab . Dadurch wird Ihr Skript beendet und Sie kehren zu Ihrer Shell-Eingabeaufforderung zurück, während es fooim Hintergrund ausgeführt wird. Sie können dies abmildern, indem Sie sagen wait  ||  wait  ||  wait  ||  wait: Erweitern Sie es so weit wie Sie möchten. Sie müssten jeweils Ctrl+ Ceinmal eingeben, wait um das Skript vorzeitig zu beenden.

D'oh!

Ein Problem dabei ist, dass für Prozesse, die von einem Skript in den Hintergrund gestellt werden, die Standardeingabe auf gesetzt ist /dev/null. Wenn Sie foovon der Tastatur lesen, muss das oben genannte überarbeitet werden.

Scott
quelle