Was passiert, wenn Sie ein Skript während der Ausführung bearbeiten?

31

Ich habe eine allgemeine Frage, die möglicherweise auf Missverständnisse im Umgang mit Prozessen unter Linux zurückzuführen ist.

Für meine Zwecke definiere ich ein 'Skript' als einen Ausschnitt aus Bash-Code, der in einer Textdatei gespeichert ist und für den aktuellen Benutzer die Ausführungsberechtigung aktiviert hat.

Ich habe eine Reihe von Skripten, die sich nacheinander aufrufen. Der Einfachheit halber werde ich sie Skripte A, B und C nennen. Skript A führt eine Reihe von Anweisungen aus und hält dann an, führt dann Skript B aus, hält dann an und führt dann Skript C aus. Mit anderen Worten, die Reihe of steps ist ungefähr so:

Führen Sie Skript A aus:

  1. Reihe von Aussagen
  2. Pause
  3. Führen Sie Skript B aus
  4. Pause
  5. Führen Sie Skript C aus

Ich weiß aus Erfahrung, dass, wenn ich Skript A bis zur ersten Pause ausführe und dann Änderungen in Skript B vornehme, sich diese Änderungen auf die Ausführung des Codes auswirken, wenn ich zulasse, dass er fortgesetzt wird. Wenn ich Skript C bearbeite, während Skript A noch angehalten ist, kann es nach dem Speichern der Änderungen fortgesetzt werden. Diese Änderungen wirken sich auch auf die Ausführung des Codes aus.

Hier ist die eigentliche Frage: Gibt es eine Möglichkeit, Skript A zu bearbeiten, während es noch ausgeführt wird? Oder ist eine Bearbeitung nach Beginn der Ausführung nicht mehr möglich?

KoffeinConnoisseur
quelle
2
Ich würde denken, es hängt von der Shell ab. obwohl Sie angeben, dass Sie Bash verwenden. scheint davon abhängig zu sein, wie die Shell Skripte intern lädt.
Strugee
Das Verhalten kann sich auch ändern, wenn Sie die Datei als Quelle angeben, anstatt sie auszuführen.
Strugee
1
Ich denke, Bash liest ein ganzes Skript in den Speicher, bevor es ausgeführt wird.
w4etwetewtwet
2
@handuel, nein, tut es nicht. Als würde es nicht warten, bis Sie an der Eingabeaufforderung "exit" eingeben, um mit der Interpretation der eingegebenen Befehle zu beginnen.
Stéphane Chazelas
1
@StephaneChazelas Ja, das Lesen vom Terminal aus funktioniert nicht, aber das unterscheidet sich vom Ausführen eines Skripts.
w4etwetewtwet

Antworten:

21

In Unix erstellen die meisten Editoren eine neue temporäre Datei mit den bearbeiteten Inhalten. Beim Speichern der bearbeiteten Datei wird die Originaldatei gelöscht und die temporäre Datei in den Originalnamen umbenannt. (Es gibt natürlich verschiedene Sicherheitsvorkehrungen, um Datenverlust zu verhindern.) Dies ist beispielsweise der Stil, der von sedoder perlbeim Aufrufen mit dem -iFlag ("an Ort und Stelle") verwendet wird, das überhaupt nicht wirklich "an Ort und Stelle" ist. Es hätte "neuer Ort mit altem Namen" heißen sollen.

Dies funktioniert gut, da Unix (zumindest für lokale Dateisysteme) sicherstellt, dass eine geöffnete Datei weiterhin vorhanden ist, bis sie geschlossen wird, auch wenn sie "gelöscht" und eine neue Datei mit demselben Namen erstellt wird. (Es ist kein Zufall, dass der Unix-Systemaufruf zum "Löschen" einer Datei tatsächlich als "Verknüpfung aufheben" bezeichnet wird.) Wenn also in einem Shell-Interpreter eine Quelldatei geöffnet ist und Sie die Datei auf die oben beschriebene Weise "bearbeiten" sieht die Shell die Änderungen nicht einmal, da die Originaldatei noch geöffnet ist.

[Hinweis: Wie bei allen auf Standards basierenden Kommentaren unterliegt das Obige mehreren Interpretationen, und es gibt verschiedene Eckfälle, wie z. B. NFS. Pedanten sind herzlich eingeladen, die Kommentare mit Ausnahmen zu füllen.]

Es ist natürlich möglich, Dateien direkt zu ändern. Dies ist für Bearbeitungszwecke nicht besonders praktisch, da Sie zwar Daten in einer Datei überschreiben können, diese jedoch nicht löschen oder einfügen können, ohne alle folgenden Daten zu verschieben, was eine erhebliche Umschreibung zur Folge hätte. Außerdem wäre der Inhalt der Datei während dieser Verschiebung unvorhersehbar, und Prozesse, bei denen die Datei geöffnet war, würden darunter leiden. Um damit durchzukommen (wie zum Beispiel bei Datenbanksystemen), benötigen Sie einen ausgeklügelten Satz von Änderungsprotokollen und verteilten Sperren. Dinge, die weit über den Rahmen eines typischen Dateibearbeitungsprogramms hinausgehen.

Wenn Sie also eine Datei bearbeiten möchten, während sie von einer Shell verarbeitet wird, haben Sie zwei Möglichkeiten:

  1. Sie können an die Datei anhängen. Das sollte immer funktionieren.

  2. Sie können die Datei mit neuen Inhalten genau gleicher Länge überschreiben . Dies kann funktionieren oder auch nicht, je nachdem, ob die Shell diesen Teil der Datei bereits gelesen hat oder nicht. Da die meisten Datei-E / A-Vorgänge Lesepuffer beinhalten und alle mir bekannten Shells einen gesamten zusammengesetzten Befehl lesen, bevor sie ausgeführt werden, ist es ziemlich unwahrscheinlich, dass Sie damit durchkommen. Es wäre sicherlich nicht zuverlässig.

Ich kenne keine Formulierung im Posix-Standard, die tatsächlich das Anhängen einer Skriptdatei erfordert, während die Datei ausgeführt wird. Daher funktioniert sie möglicherweise nicht mit jeder Posix-kompatiblen Shell, geschweige denn mit dem aktuellen Angebot von fast und manchmal posix-konforme Schalen. Also YMMV. Aber meines Wissens funktioniert es zuverlässig mit bash.

Als Beweis ist hier eine "schleifenfreie" Implementierung des berüchtigten 99-Flaschen-Bier-Programms in bash, das ddzum Überschreiben und Anhängen verwendet wird (das Überschreiben ist vermutlich sicher, da es die aktuell ausgeführte Zeile ersetzt, die immer die letzte Zeile der Datei, mit einem Kommentar von genau der gleichen Länge; ich habe das getan, damit das Endergebnis ohne das selbstmodifizierende Verhalten ausgeführt werden kann.)

#!/bin/bash
if [[ $1 == reset ]]; then
  printf "%s\n%-16s#\n" '####' 'next ${1:-99}' |
  dd if=/dev/stdin of=$0 seek=$(grep -bom1 ^#### $0 | cut -f1 -d:) bs=1 2>/dev/null
  exit
fi

step() {
  s=s
  one=one
  case $beer in
    2) beer=1; unset s;;
    1) beer="No more"; one=it;;
    "No more") beer=99; return 1;;
    *) ((--beer));;
  esac
}
next() {
  step ${beer:=$(($1+1))}
  refrain |
  dd if=/dev/stdin of=$0 seek=$(grep -bom1 ^next\  $0 | cut -f1 -d:) bs=1 conv=notrunc 2>/dev/null
}
refrain() {
  printf "%-17s\n" "# $beer bottles"
  echo echo ${beer:-No more} bottle$s of beer on the wall, ${beer:-No more} bottle$s of beer.
  if step; then
    echo echo Take $one down, pass it around, $beer bottle$s of beer on the wall.
    echo echo
    echo next abcdefghijkl
  else
    echo echo Go to the store, buy some more, $beer bottle$s of beer on the wall.
  fi
}
####
next ${1:-99}   #
rici
quelle
Wenn ich das starte, fängt es mit "No more" an und geht dann auf -1 und unbegrenzt in die negativen Zahlen über.
Daniel Hershcovich
Wenn ich das export beer=100vor dem Ausführen des Skripts mache , funktioniert es wie erwartet.
Daniel Hershcovich
@ DanielHershcovich: ganz richtig; schlampige Tests meinerseits. Ich glaube, ich habe es behoben. Es ist jetzt ein optionaler Parameter count zulässig. Eine bessere und interessantere Lösung wäre das automatische Zurücksetzen, wenn der Parameter nicht mit der zwischengespeicherten Kopie übereinstimmt.
rici
18

bash stellt sicher, dass Befehle gelesen werden, bevor sie ausgeführt werden.

Zum Beispiel in:

cmd1
cmd2

Die Shell liest das Skript blockweise. Lesen Sie also wahrscheinlich beide Befehle, interpretieren Sie den ersten und gehen Sie dann zum Ende des cmd1Skripts zurück und lesen Sie das Skript erneut, cmd2um es zu lesen und auszuführen.

Sie können es leicht überprüfen:

$ cat a
echo foo | dd 2> /dev/null bs=1 seek=50 of=a
echo bar
$ bash a
foo

(Wenn man sich die straceAusgabe darüber ansieht , scheint es, als ob sie ausgefallenere Dinge tut (wie das mehrfache Lesen der Daten, das Zurücksuchen ...), als wenn ich es vor ein paar Jahren versucht hätte, also könnte meine obige Aussage über das Zurücksuchen zurück sein gilt nicht mehr für neuere Versionen).

Wenn Sie jedoch Ihr Skript schreiben als:

{
  cmd1
  cmd2
  exit
}

Die Shell muss bis zum Abschluss lesen }, speichern und ausführen. Aus diesem Grund exitliest die Shell das Skript nicht erneut, sodass Sie es sicher bearbeiten können, während die Shell es interpretiert.

Stellen Sie alternativ beim Bearbeiten des Skripts sicher, dass Sie eine neue Kopie des Skripts schreiben. Die Shell liest weiterhin das Original (auch wenn es gelöscht oder umbenannt wurde).

Um dies zu tun, benennen Sie es the-scriptum, the-script.oldkopieren Sie the-script.oldes the-scriptund bearbeiten Sie es.

Stéphane Chazelas
quelle
4

Es gibt wirklich keine sichere Möglichkeit, das Skript während der Ausführung zu ändern, da die Shell das Puffern zum Lesen der Datei verwenden kann. Wenn das Skript durch Ersetzen durch eine neue Datei geändert wird, liest Shells die neue Datei in der Regel erst, nachdem bestimmte Vorgänge ausgeführt wurden.

Wenn ein Skript während der Ausführung geändert wird, meldet die Shell häufig Syntaxfehler. Dies liegt an der Tatsache, dass die Shell beim Schließen und erneuten Öffnen der Skriptdatei den Byte-Offset in der Datei verwendet, um sich bei der Rückkehr neu zu positionieren.

Asche
quelle
4

Sie können dies umgehen, indem Sie eine Falle in Ihrem Skript setzen und dann execdie neuen Skriptinhalte mit abrufen. Beachten Sie jedoch, dass der execAufruf das Skript von Grund auf neu startet und nicht an der Stelle, an der es im laufenden Prozess angekommen ist. Daher wird Skript B aufgerufen (usw.).

#! /bin/bash

CMD="$0"
ARGS=("$@")

trap reexec 1

reexec() {
    exec "$CMD" "${ARGS[@]}"
}

while : ; do sleep 1 ; clear ; date ; done

Dadurch wird das Datum weiterhin auf dem Bildschirm angezeigt. Ich kann dann meinen Skript und Änderung bearbeiten datezu echo "Date: $(date)". Beim Ausschreiben des Skripts wird immer noch nur das Datum angezeigt. Wenn ich jedoch das Signal sende, das ich für die trapErfassung festgelegt habe, ersetzt das Skript exec(den aktuell ausgeführten Prozess durch den angegebenen Befehl) den Befehl $CMDund die Argumente $@. Sie können dies tun, indem Sie kill -1 PID- wobei PID die PID des ausgeführten Skripts ist - eingeben und die Ausgabe so ändern, dass sie Date:vor der dateBefehlsausgabe angezeigt wird.

Sie können den "Status" Ihres Skripts in einer externen Datei (in "say / tmp") speichern und den Inhalt lesen, um zu erfahren, wo er beim erneuten Ausführen des Programms "fortgesetzt" werden soll. Sie können dann eine zusätzliche Trap-Beendigung (SIGINT / SIGQUIT / SIGKILL / SIGTERM) hinzufügen, um diese tmp-Datei zu löschen. Wenn Sie also nach dem Unterbrechen von "Skript A" einen Neustart durchführen, beginnt sie von vorne. Eine zustandsbehaftete Version wäre so etwas wie:

#! /bin/bash

trap reexec 1
trap cleanup 2 3 9 15

CMD="$0"
ARGS=("$@")
statefile='/tmp/scriptA.state'
EXIT=1

reexec() { echo "Restarting..." ; exec "$CMD" "${ARGS[@]}"; }
cleanup() { rm -f $statefile; exit $EXIT; }
run_scriptB() { /path/to/scriptB; echo "scriptC" > $statefile; }
run_scriptC() { /path/to/scriptC; echo "stop" > $statefile;  }

while [ "$state" != "stop" ] ; do

    if [ -f "$statefile" ] ; then
        state="$(cat "$statefile")"
    else
        state='starting'
    fi

    case "$state" in
        starting)         
            run_scriptB
        ;;
        scriptC)
            run_scriptC
        ;;
    esac
done

EXIT=0
cleanup
Drav Sloan
quelle
Ich habe dieses Problem behoben, indem ich das Skript erfasst $0und $@zu Beginn des Skripts diese Variablen in execverwendet habe.
Drav Sloan