Wie lese ich das gesamte Shell-Skript, bevor ich es ausführe?

35

Wenn Sie ein Skript bearbeiten, sind in der Regel alle ausgeführten Verwendungen des Skripts fehleranfällig.

Soweit ich weiß, liest bash (auch andere Shells?) Das Skript inkrementell. Wenn Sie also die Skriptdatei extern geändert haben, werden die falschen Informationen gelesen. Gibt es eine Möglichkeit, dies zu verhindern?

Beispiel:

sleep 20

echo test

Wenn Sie dieses Skript ausführen, liest bash die erste Zeile (etwa 10 Byte) und geht in den Ruhezustand. Wenn das Skript fortgesetzt wird, kann es ab dem 10. Byte unterschiedliche Inhalte im Skript geben. Ich bin möglicherweise mitten in einer Zeile im neuen Skript. Dadurch wird das laufende Skript unterbrochen.

VasyaNovikov
quelle
Was meinst du mit "Ändern des Skripts von außen"?
Maultinglawns
1
Vielleicht gibt es eine Möglichkeit, den gesamten Inhalt in eine Funktion oder so etwas zu packen, damit die Shell zuerst das gesamte Skript liest? Aber was ist mit der letzten Zeile, in der Sie die Funktion aufrufen, wird sie bis EOF gelesen? Vielleicht \nwürde das Weglassen des letzten den Trick tun? Vielleicht reicht eine Unterschale ()? Ich bin nicht sehr erfahren damit, bitte helfen Sie!
VasyaNovikov
@ maulinglawns Wenn das Skript Inhalte wie hat sleep 20 ;\n echo test ;\n sleep 20und ich mit der Bearbeitung beginne, kann es sich schlecht verhalten . Bash könnte beispielsweise die ersten 10 Bytes des Skripts lesen, den sleepBefehl verstehen und in den Ruhezustand wechseln. Nach dem Fortsetzen befinden sich ab 10 Byte unterschiedliche Inhalte in der Datei.
VasyaNovikov
1
Sie sagen also, dass Sie ein Skript bearbeiten, das gerade ausgeführt wird? Stoppen Sie zuerst das Skript, nehmen Sie Ihre Änderungen vor und starten Sie es dann erneut.
Maultinglawns
@maulinglawns ja, das wars im grunde. Das Problem ist, dass es für mich nicht bequem ist, die Skripte anzuhalten, und es ist schwierig, sich immer daran zu erinnern, dies zu tun. Vielleicht gibt es eine Möglichkeit, das Lesen des gesamten Drehbuchs zu erzwingen?
VasyaNovikov

Antworten:

43

Yes-Shells bashlesen die Datei insbesondere zeilenweise, sodass sie genauso funktioniert, wie wenn Sie sie interaktiv verwenden.

Sie werden feststellen, dass die Datei, wenn sie nicht suchbar ist (wie eine Pipe), bashsogar jeweils ein Byte liest, um sicherzugehen, dass das \nZeichen nicht überschritten wird . Wenn die Datei suchbar ist, wird sie optimiert, indem ganze Blöcke gleichzeitig gelesen werden. Suchen Sie jedoch nach dem \n.

Das heißt, Sie können Dinge tun wie:

bash << \EOF
read var
var's content
echo "$var"
EOF

Oder schreiben Sie Skripte, die sich selbst aktualisieren. Was Sie nicht könnten, wenn es Ihnen diese Garantie nicht geben würde.

Nun ist es selten, dass Sie solche Dinge tun möchten, und wie Sie herausgefunden haben, wird diese Funktion häufiger im Weg stehen, als es sinnvoll ist.

Um dies zu vermeiden, können Sie versuchen, sicherzustellen, dass Sie die Datei nicht direkt ändern (z. B. eine Kopie ändern und die Kopie an ihren Platz verschieben (wie dies zum Beispiel sed -ioder perl -pieinige Editoren tun)).

Oder Sie könnten Ihr Skript schreiben wie:

{
  sleep 20
  echo test
}; exit

(Beachten Sie, dass es wichtig ist, dass Sie exitsich in der gleichen Zeile wie befinden }. Sie können es jedoch auch in die geschweiften Klammern unmittelbar vor der schließenden setzen.)

oder:

main() {
  sleep 20
  echo test
}
main "$@"; exit

Die Shell muss das Skript erst lesen, exitbevor sie etwas unternimmt. Dadurch wird sichergestellt, dass die Shell nicht erneut aus dem Skript liest.

Das bedeutet, dass das gesamte Skript im Speicher abgelegt wird.

Dies kann sich auch auf das Parsen des Skripts auswirken.

Zum Beispiel in bash:

export LC_ALL=fr_FR.UTF-8
echo $'St\ue9phane'

Würde das in UTF-8 codierte U + 00E9 ausgeben. Wenn Sie es jedoch ändern in:

{
  export LC_ALL=fr_FR.UTF-8
  echo $'St\ue9phane'
}

Das \ue9wird in dem Zeichensatz erweitert, der zu dem Zeitpunkt gültig war, als der Befehl analysiert wurde. In diesem Fall ist dies der Fall, bevor der exportBefehl ausgeführt wird.

Beachten Sie auch, dass bei Verwendung des Befehls sourceaka .bei einigen Shells die gleichen Probleme bei den Quelldateien auftreten.

Dies ist jedoch nicht der Fall, bashwenn der sourceBefehl die Datei vollständig liest, bevor er sie interpretiert. Wenn bashSie speziell dafür schreiben , können Sie tatsächlich davon Gebrauch machen, indem Sie am Anfang des Skripts Folgendes hinzufügen:

if [[ ! $already_sourced ]]; then
  already_sourced=1
  source "$0"; exit
fi

(Darauf würde ich mich jedoch nicht verlassen, da Sie sich vorstellen können, dass zukünftige Versionen von bashdieses Verhalten ändern könnten, das derzeit als Einschränkung angesehen werden kann.) und der already_sourcedTrick ist ein bisschen spröde, da davon ausgegangen wird, dass sich die Variable nicht in der Umgebung befindet, ganz zu schweigen davon, dass er den Inhalt der Variablen BASH_SOURCE beeinflusst.)

Stéphane Chazelas
quelle
@ VasyaNovikov, im Moment scheint mit SE etwas nicht in Ordnung zu sein (oder zumindest für mich). Es gab nur ein paar Antworten, als ich meine hinzufügte, und Ihr Kommentar scheint erst jetzt aufgetaucht zu sein, obwohl es heißt, dass er vor 16 Minuten veröffentlicht wurde (oder vielleicht habe ich nur meine Murmeln verloren). Beachten Sie auf jeden Fall das zusätzliche "Beenden", das hier erforderlich ist, um Probleme zu vermeiden, wenn die Größe der Datei zunimmt (wie in dem Kommentar angegeben, den ich Ihrer Antwort hinzugefügt habe).
Stéphane Chazelas
Stéphane, ich glaube, ich habe eine andere Lösung gefunden. Es ist zu benutzen }; exec true. Auf diese Weise sind keine Zeilenumbrüche am Ende der Datei erforderlich, was für einige Editoren (wie z. B. Emacs) hilfreich ist. Alle Tests, die ich mir }; exec true
vorstellen
@ VasyaNovikov, nicht sicher, was du meinst. Wie ist es besser als }; exit? Sie verlieren auch den Exit-Status.
Stéphane Chazelas
Wie bei einer anderen Frage erwähnt: Es ist üblich, zuerst die gesamte Datei zu analysieren und dann die zusammengesetzte Anweisung auszuführen, wenn der Punktbefehl ( . script) verwendet wird.
Schily
@schily, ja ich erwähne das in dieser Antwort als Einschränkung von AT & T ksh und bash. Andere POSIX-Shells haben diese Einschränkung nicht.
Stéphane Chazelas
12

Sie müssen lediglich die Datei löschen (dh kopieren, löschen, die Kopie wieder in den ursprünglichen Namen umbenennen). Tatsächlich können viele Editoren so konfiguriert werden, dass sie dies für Sie tun. Wenn Sie eine Datei bearbeiten und einen geänderten Puffer darin speichern, wird die alte Datei nicht überschrieben, sondern umbenannt, eine neue erstellt und der neue Inhalt in die neue Datei eingefügt. Daher sollte jedes laufende Skript problemlos fortgesetzt werden können.

Wenn Sie ein einfaches Versionskontrollsystem wie RCS verwenden, das für vim und emacs verfügbar ist, haben Sie den doppelten Vorteil, dass Sie einen Verlauf Ihrer Änderungen haben. Das Checkout-System sollte standardmäßig die aktuelle Datei entfernen und sie mit den richtigen Modi neu erstellen. (Achten Sie natürlich darauf, solche Dateien nicht zu verlinken).

meuh
quelle
"delete" ist eigentlich kein Teil des Prozesses. Wenn Sie es ordentlich atomisieren möchten, benennen Sie die Zieldatei um. Wenn Sie einen Löschschritt ausführen, besteht die Gefahr, dass Ihr Prozess nach dem Löschen, aber vor dem Umbenennen abstürzt und keine Datei mehr vorhanden ist ( oder ein Leser versucht, auf die Datei in diesem Fenster zuzugreifen und findet weder alte noch neue Versionen).
Charles Duffy
11

Einfachste Lösung:

{
  ... your code ...

  exit
}

Auf diese Weise liest bash den gesamten {}Block, bevor es ausgeführt wird, und die exitDirektive stellt sicher, dass außerhalb des Codeblocks nichts gelesen wird.

Wenn Sie das Skript nicht "ausführen", sondern als "Quelle" verwenden möchten, benötigen Sie eine andere Lösung. Das sollte dann funktionieren:

{
  ... your code ...

  return 2>/dev/null || exit
}

Oder wenn Sie den Exit-Code direkt steuern möchten:

{
  ... your code ...

  ret="$?";return "$ret" 2>/dev/null || exit "$ret"
}

Voilà! Dieses Skript kann sicher bearbeitet, als Quelle verwendet und ausgeführt werden. Sie müssen immer noch sicherstellen, dass Sie es nicht in diesen Millisekunden ändern, wenn es zum ersten Mal gelesen wird.

VasyaNovikov
quelle
1
Was ich gefunden habe ist, dass es EOF nicht sieht und die Datei nicht mehr liest, sondern sich in seiner "gepufferten Stream" -Verarbeitung verfängt und am Ende nach dem Ende der Datei sucht, weshalb es gut aussieht, wenn die Größe von Die Datei vergrößert sich um nicht viel, sieht aber schlecht aus, wenn Sie die Datei mehr als doppelt so groß wie zuvor machen. Ich werde den Betreuern der Bash in Kürze einen Fehler melden.
Stéphane Chazelas
1
Bug jetzt gemeldet , siehe auch Patch .
Stéphane Chazelas
Kommentare sind nicht für eine längere Diskussion gedacht. Diese Unterhaltung wurde in den Chat verschoben .
terdon
5

Konzeptioneller Beweiß. Hier ist ein Skript, das sich selbst ändert:

cat <<EOF >/tmp/scr
#!/bin/bash
sed  s/[k]ept/changed/  /tmp/scr > /tmp/scr2

# this next line overwites the on disk copy of the script
cat /tmp/scr2 > /tmp/scr
# this line ends up changed.
echo script content kept
EOF
chmod u+x /tmp/scr
/tmp/scr

Wir sehen die geänderte Version drucken

Dies liegt daran, dass beim Laden von Bashs ein Datei-Handle für das Skript geöffnet bleibt, sodass Änderungen an der Datei sofort angezeigt werden.

Wenn Sie die speicherinterne Kopie nicht aktualisieren möchten, heben Sie die Verknüpfung der Originaldatei auf und ersetzen Sie sie.

Eine Möglichkeit, dies zu tun, ist die Verwendung von sed -i.

sed -i '' filename

konzeptioneller Beweiß

cat <<EOF >/tmp/scr
#!/bin/bash
sed  s/[k]ept/changed/  /tmp/scr > /tmp/scr2

# this next line unlinks the original and creates a new copy.
sed -i ''  /tmp/scr

# now overwriting it has no immediate effect
cat /tmp/scr2 > /tmp/scr
echo script content kept
EOF

chmod u+x /tmp/scr
/tmp/scr

Wenn Sie zum Ändern des Skripts einen Editor verwenden, ist möglicherweise nur das Aktivieren der Funktion "Sicherungskopie aufbewahren" erforderlich, damit der Editor die geänderte Version in eine neue Datei schreibt, anstatt die vorhandene zu überschreiben.

Jasen
quelle
2
Nein, bashöffnet die Datei nicht mit mmap(). Es ist nur vorsichtig, nach Bedarf eine Zeile nach der anderen zu lesen, genau wie wenn die Befehle von einem Endgerät abgerufen werden, wenn es interaktiv ist.
Stéphane Chazelas
2

Das Einschließen Ihres Skripts in einen Block {}ist wahrscheinlich die beste Option, erfordert jedoch das Ändern Ihrer Skripts.

F=$(mktemp) && cp test.sh $F && bash $F; rm $F;

wäre die zweitbeste Option (vorausgesetzt, tmpfs ) der Nachteil ist, dass sie $ 0 kaputt macht, wenn Ihre Skripte dies verwenden.

Die Verwendung von so etwas F=test.sh; tail -n $(cat "$F" | wc -l) "$F" | bashist weniger ideal, da die gesamte Datei im Speicher bleiben muss und $ 0 kaputt geht.

Das Berühren der Originaldatei sollte vermieden werden, damit die letzte Änderung, Lesesperren und feste Links nicht gestört werden. Auf diese Weise können Sie einen Editor geöffnet lassen, während die Datei ausgeführt wird, und rsync prüft nicht unnötigerweise, ob die Datei wie erwartet auf Sicherungen und feste Verknüpfungen überprüft wird.

Das Ersetzen der Datei beim Bearbeiten würde funktionieren, ist jedoch weniger robust, da sie für andere Skripte / Benutzer / nicht erzwingbar ist, oder man könnte es vergessen. Und wieder würde es harte Verbindungen brechen.

user1133275
quelle
Alles, was eine Kopie macht, würde funktionieren. tac test.sh | tac | bash
Jasen