Wie kann ich Bash-Skripte gegen zukünftige Änderungen schützen?

46

Also habe ich meinen Home-Ordner (oder genauer gesagt alle Dateien, auf die ich Schreibzugriff hatte) gelöscht. Was passiert ist, dass ich hatte

build="build"
...
rm -rf "${build}/"*
...
<do other things with $build>

in einem Bash-Skript und nachdem Sie $builddie Deklaration und alle ihre Verwendungen nicht mehr benötigt haben , entfernen Sie sie - aber die rm. Bash dehnt sich glücklich aus rm -rf /*. Ja.

Ich fühlte mich dumm, habe das Backup installiert, die Arbeit, die ich verloren habe, überarbeitet. Der Versuch, die Schande hinter sich zu lassen.

Nun frage ich mich: Was sind Techniken, um Bash-Skripte so zu schreiben, dass solche Fehler nicht auftreten können oder zumindest weniger wahrscheinlich sind? Hatte ich zum Beispiel geschrieben

FileUtils.rm_rf("#{build}/*")

In einem Ruby-Skript hätte sich der Dolmetscher darüber beschwert, dass er buildnicht deklariert wurde, daher schützt mich die Sprache.

Was ich in bash außer dem Korralieren betrachtet habe rm(was, wie viele Antworten in verwandten Fragen erwähnen, nicht unproblematisch ist):

  1. rm -rf "./${build}/"*
    Das hätte meine aktuelle Arbeit (ein Git-Repo) getötet, aber sonst nichts.
  2. Eine Variante / Parametrisierung rmdavon erfordert Interaktion, wenn Sie außerhalb des aktuellen Verzeichnisses agieren. (Konnte keine finden.) Ähnliche Wirkung.

Ist es das oder gibt es andere Möglichkeiten, Bash-Skripte zu schreiben, die in diesem Sinne "robust" sind?

Raphael
quelle
3
Es gibt keinen Grund, rm -rf "${build}/*wohin die Anführungszeichen gehen. rm -rf "${build} wird das gleiche wegen der tun f.
Monty Harder
1
Die statische Überprüfung mit shellcheck.net ist ein sehr solider Ausgangspunkt. Es gibt eine Editor-Integration, sodass Sie eine Warnung von Ihren Tools erhalten können, sobald Sie die Definition von etwas entfernen, das noch verwendet wird.
Charles Duffy
@CharlesDuffy hinzufügen zu meiner Schande, ich schrieb das Skript in einem IDEA-Stil IDE mit BashSupport installiert, die sich in diesem Fall warnen. Also ja, gültiger Punkt, aber ich brauchte wirklich einen harten Abbruch.
Raphael
2
Erwischt. Beachten Sie unbedingt die Vorsichtsmaßnahmen in BashFAQ # 112 . set -uist bei set -eweitem nicht so verpönt wie es ist, aber es hat immer noch seine Fallstricke.
Charles Duffy
2
witz antwort: Einfach #! /usr/bin/env rubyoben in jedes shell script schreiben und bash vergessen;)
Pod

Antworten:

75
set -u

oder

set -o nounset

Dies würde die aktuelle Shell veranlassen, Erweiterungen nicht festgelegter Variablen als Fehler zu behandeln:

$ unset build
$ set -u
$ rm -rf "$build"/*
bash: build: unbound variable

set -uund set -o nounsetsind POSIX-Shell-Optionen .

Ein leerer Wert würde jedoch keinen Fehler auslösen.

Verwenden Sie dazu

$ rm -rf "${build:?Error, variable is empty or unset}"/*
bash: build: Error, variable is empty or unset

Die Erweiterung von ${variable:?word}würde sich auf den Wert von erweitern, es variablesei denn, sie ist leer oder nicht gesetzt. Wenn es leer oder nicht gesetzt ist, wird der wordStandardfehler angezeigt und die Shell behandelt die Erweiterung als Fehler (der Befehl wird nicht ausgeführt, und wenn er in einer nicht interaktiven Shell ausgeführt wird, wird dies beendet). Wenn Sie das :Feld verlassen, wird der Fehler nur für einen nicht festgelegten Wert ausgelöst, genau wie unter set -u.

${variable:?word}ist eine POSIX-Parametererweiterung .

Beides würde nicht dazu führen, dass eine interaktive Shell beendet wird, es sei denn, set -e(oder set -o errexit) ist ebenfalls in Kraft. ${variable:?word}Bewirkt, dass Skripte beendet werden, wenn die Variable leer oder nicht gesetzt ist. set -uwürde dazu führen, dass ein Skript beendet wird, wenn es zusammen mit verwendet wird set -e.


Wie für Ihre zweite Frage. Es gibt keine Möglichkeit, die rmArbeit außerhalb des aktuellen Verzeichnisses zu beschränken.

Die GNU-Implementierung von rmhat eine --one-file-systemOption, die das rekursive Löschen von gemounteten Dateisystemen verhindert, aber das ist so nah, wie wir glauben, ohne den rmAufruf in eine Funktion zu packen, die die Argumente tatsächlich prüft.


Als Randnotiz: ${build}Entspricht genau, $buildwenn die Erweiterung nicht als Teil einer Zeichenfolge erfolgt, bei der das unmittelbar folgende Zeichen ein gültiges Zeichen in einem Variablennamen ist, z. B. in "${build}x".

Kusalananda
quelle
Sehr gut, danke! 1) Ist es ${build:?msg}oder ${build?msg}? 2) Im Zusammenhang mit so etwas wie Build-Skripten wäre es meiner Meinung nach in Ordnung, ein anderes Tool als rm für das sichere Löschen zu verwenden: Wir wissen, dass wir nicht außerhalb des aktuellen Verzeichnisses arbeiten möchten, daher machen wir dies explizit durch die Verwendung eines eingeschränkten Befehl. Es besteht keine Notwendigkeit, mich allgemein sicherer zu machen.
Raphael
1
@Raphael Sorry, sollte sein mit:. Ich werde diesen Tippfehler jetzt korrigieren und seine Bedeutung später erwähnen, wenn ich wieder an meinem Computer bin.
Kusalananda
2
@Raphael Ok, ich habe einen kurzen Satz darüber hinzugefügt, was ohne das passiert :(es würde den Fehler nur für nicht gesetzte Variablen auslösen ). Ich wage nicht zu versuchen, ein Tool zu schreiben, das unter allen Umständen das Löschen von Dateien ausschließlich unter einem bestimmten Pfad bewältigt. Das Parsen von Pfaden und das Sorgen / Nicht-Sorgen um symbolische Verknüpfungen usw. ist mir im Moment etwas zu umständlich.
Kusalananda
1
Danke, die Erklärung hilft! In Bezug auf das "lokale RMS": Ich habe meistens nachgedacht, vielleicht nach einem "Sicher, das ist <Toolname>" gefischt - aber sicherlich nicht versucht, Sie dazu zu bewegen, es zu schreiben! OO Alles gut, du hast genug geholfen! :)
Raphael
2
@MateuszKonieczny Ich glaube nicht, dass ich werde, sorry. Ich glaube, dass solche Sicherheitsmaßnahmen bei Bedarf angewendet werden sollten . Wie bei allem, was eine Umwelt sicher macht, werden die Menschen letztendlich immer sorgloser und abhängig von den Sicherheitsmaßnahmen. Es ist besser zu wissen, was jede einzelne Sicherheitsmaßnahme bewirkt, und sie dann nach Bedarf selektiv anzuwenden.
Kusalananda
12

Ich werde normale Validierungsprüfungen mit test/ vorschlagen.[ ]

Sie wären in Sicherheit, wenn Sie Ihr Skript als solches geschrieben hätten:

build="build"
...
[ -n "${build}" ] || exit 1
rm -rf "${build}/"*
...

Das [ -n "${build}" ]prüft, dass "${build}"eine Zeichenfolge ungleich Null ist .

Dies ||ist der logische ODER-Operator in der Bash. Wenn der erste Befehl fehlschlägt, wird ein anderer Befehl ausgeführt.

Auf diese Weise ${build}war leer / undefiniert / etc. Das Skript wurde beendet (mit dem Rückkehrcode 1, was ein allgemeiner Fehler ist).

Dies hätte Sie auch geschützt, falls Sie alle Verwendungen entfernt hätten, ${build}da dies [ -n "" ]immer falsch ist.


Der Vorteil der Verwendung von test/ [ ]besteht darin, dass es viele andere aussagekräftigere Prüfungen gibt, die es auch verwenden kann.

Zum Beispiel:

[ -f FILE ] True if FILE exists and is a regular file.
[ -d FILE ] True if FILE exists and is a directory.
[ -O FILE ] True if FILE exists and is owned by the effective user ID.
Centimane
quelle
Fair, aber ein bisschen unhandlich. Vermisse ich etwas oder funktioniert es so ziemlich genauso, wie ${variable:?word}@Kusalananda es vorschlägt?
Raphael
@Raphael es funktioniert ähnlich im Fall von "hat der String einen Wert", aber test(dh [) hat viele andere Überprüfungen, die relevant sind, wie -d(das Argument ist ein Verzeichnis), -f(das Argument ist eine Datei), -O(die Datei gehört dem aktuellen Benutzer) und so weiter.
Centimane
1
@Raphael Auch die Validierung sollte ein normaler Bestandteil jedes Codes / Skripts sein. Beachten Sie auch, dass Sie, wenn Sie alle Build-Instanzen entfernt hätten, diese auch entfernt hätten ${build:?word}. Das ${variable:?word}Format schützt Sie nicht, wenn Sie alle Instanzen der Variablen entfernen.
Centimane
1
1) Ich denke, es gibt einen Unterschied zwischen dem Sicherstellen, dass Annahmen gelten (Dateien existieren usw.) und dem Überprüfen, ob Variablen gesetzt sind (imho Job eines Compilers / Interpreters). Wenn ich letzteres von Hand erledigen muss, sollte es eine ultraschnelle Syntax dafür geben - was eine ausgewachsene ifnicht ist. 2) "hättest du nicht auch $ {build:? Word} mitgenommen" - das Szenario ist, dass ich eine Verwendung der Variablen verpasst habe. Die Verwendung ${v:?w}hätte mich vor Beschädigungen geschützt. Hätte ich alle Verwendungen entfernt, wäre natürlich auch der einfache Zugriff nicht schädlich gewesen.
Raphael
Trotzdem ist Ihre Antwort eine faire und hilfreiche Antwort auf die allgemeine Titelfrage: Es ist wichtig , sicherzustellen, dass die Annahmen stimmen, wenn Skripte vorhanden bleiben. Das spezifische Problem im Fragenkörper wird imho besser von Kusalananda beantwortet. Vielen Dank!
Raphael
4

In Ihrem speziellen Fall habe ich in der Vergangenheit das Löschen überarbeitet, um stattdessen Dateien / Verzeichnisse zu verschieben (vorausgesetzt, / tmp befindet sich auf derselben Partition wie Ihr Verzeichnis):

# mktemp -d is also a good, reliable choice
trashdir=/tmp/.trash-$USER/trash-`date`
mkdir -p "$trashdir"
...
mv "${build}"/* "$trashdir"
...

Hinter den Kulissen werden dabei die Dateiverweise der obersten Ebene von den Quell- zu den Zielverzeichnisstrukturen $trashdirauf derselben Partition verschoben , und es wird keine Zeit darauf verwendet, die Verzeichnisstruktur zu durchsuchen und die Plattenblöcke pro Datei sofort freizugeben . Dies führt zu einer viel schnelleren Bereinigung, während das System aktiv ist, im Austausch für einen etwas langsameren Neustart (/ tmp wird beim Neustart bereinigt).

Alternativ verhindert ein Cron-Eintrag zum periodischen Bereinigen von /tmp/.trash-$USER, dass / tmp für Prozesse (z. B. Builds), die viel Speicherplatz verbrauchen, voll wird. Wenn sich Ihr Verzeichnis auf einer anderen Partition als / tmp befindet, können Sie ein ähnliches / tmp-ähnliches Verzeichnis auf Ihrer Partition erstellen und cron das stattdessen bereinigen lassen.

Am wichtigsten ist jedoch, dass Sie, wenn Sie die Variablen auf irgendeine Weise vermasseln, den Inhalt wiederherstellen können, bevor die Bereinigung stattfindet.

user117529
quelle
2
"Dies führt zu einer viel schnelleren Bereinigung, während das System aktiv ist" - oder? Ich hätte gedacht, bei beiden geht es nur darum, die betroffenen Inodes zu ändern. Es ist sicherlich nicht schneller, wenn /tmpes sich auf einer anderen Partition befindet (ich denke, es ist immer etwas für mich, zumindest für Skripte, die im Benutzerbereich ausgeführt werden). Dann muss der Papierkorb geändert werden (und profitiert nicht von der Handhabung des Betriebssystems /tmp).
Raphael
1
Sie könnten es verwenden mktemp -d, um dieses temporäre Verzeichnis abzurufen, ohne auf die Zehen eines anderen Prozesses zu treten (und um es richtig zu würdigen $TMPDIR).
Toby Speight
Fügte Ihre genauen und hilfreichen Notizen von Ihnen beiden hinzu, danke. Ich glaube, ich habe das ursprünglich zu schnell gepostet, ohne die von Ihnen angesprochenen Punkte zu berücksichtigen.
user117529
2

Verwenden Sie die Bash-Parameter-Ersetzung, um Standardeinstellungen zuzuweisen, wenn die Variable nicht initialisiert ist. Beispiel:

rm -rf $ {variable: - "/ nonexistent"}

Rackandboneman
quelle
1

Ich versuche immer, meine Bash-Skripte mit einer Linie zu beginnen #!/bin/bash -ue.

-eMittel „auf ersten uncathed versagen e rror“;

-ubedeutet „auf der ersten Nutzung nicht von u variable ndeclared“.

Weitere Details finden Sie in einem großartigen Artikel. Verwenden Sie den inoffiziellen Bash-Strict-Modus (es sei denn, Sie lieben das Debuggen) . Auch der Autor empfiehlt die Verwendung, set -o pipefail; IFS=$'\n\t'aber für meine Zwecke ist dies übertrieben.

niya3
quelle
1

Der allgemeine Rat, um zu überprüfen, ob Ihre Variable festgelegt ist, ist ein nützliches Werkzeug, um solche Probleme zu vermeiden. In diesem Fall gab es jedoch eine einfachere Lösung.

Es ist höchstwahrscheinlich nicht erforderlich, den Inhalt des $buildVerzeichnisses zu glätten , um sie zu löschen, nicht jedoch das eigentliche $buildVerzeichnis. Wenn Sie also das Fremde überspringen, wird *ein nicht festgelegter Wert rm -rf /angezeigt, zu dem sich die meisten RM-Implementierungen im letzten Jahrzehnt standardmäßig verweigern (es sei denn, Sie deaktivieren diesen Schutz mit der GNU RM- --no-preserve-rootOption).

Das Überspringen des Trailings /würde zu folgender rm ''Fehlermeldung führen:

rm: can't remove '': No such file or directory

Dies funktioniert auch dann, wenn Ihr Befehl rm keinen Schutz für implementiert /.

eschwartz
quelle
-2

Sie denken in Programmiersprachen, aber bash ist eine Skriptsprache :-) Verwenden Sie also ein völlig anderes Anweisungsparadigma für ein völlig anderes Sprachparadigma .

In diesem Fall:

rmdir ${build}

Da rmdirdas Entfernen eines nicht leeren Verzeichnisses verweigert wird, müssen Sie zuerst die Mitglieder entfernen. Sie wissen, was diese Mitglieder sind, richtig? Sie sind wahrscheinlich Parameter für Ihr Skript oder von einem Parameter abgeleitet.

rm -rf ${build}/${parameter}
rmdir ${build}

Wenn Sie nun einige andere Dateien oder Verzeichnisse wie eine temporäre Datei oder etwas, das Sie nicht haben sollten, dort ablegen, rmdirwird ein Fehler ausgegeben . Behandle es richtig, dann:

rmdir ${build} || build_dir_not_empty "${build}"

Diese Art zu denken hat mir gute Dienste geleistet, weil ... ja, ich war dort und habe das getan.

Reich
quelle
6
Vielen Dank für Ihre Mühe, aber das ist völlig daneben. Die Annahme "Sie wissen, was diese Mitglieder sind" ist falsch; Denken Sie an Compiler-Ausgabe. make cleanverwendet einige Platzhalter (es sei denn, ein sehr sorgfältiger Compiler erstellt eine Liste aller von ihm erstellten Dateien). Außerdem scheint es mir, dass rm -rf ${build}/${parameter}das Problem nur ein wenig nach unten verschoben wird.
Raphael
Hm. Guck mal wie das liest. "Danke, aber das ist für etwas, das ich in der ursprünglichen Frage nicht offengelegt habe, also werde ich Ihre Antwort nicht nur ablehnen, sondern auch ablehnen, obwohl sie für den allgemeinen Fall zutreffender ist." Beeindruckend.
Rich
"Lassen Sie mich zusätzliche Annahmen treffen, die über das hinausgehen, was Sie in der Frage geschrieben haben, und dann eine spezielle Antwort schreiben." * achselzuckend * (FYI, nicht, dass es dich etwas angeht : Ich habe nicht abgelehnt. Ich hätte es wohl tun sollen, weil die Antwort (zumindest für mich) nicht nützlich war.)
Raphael
Hm. Ich machte keine Annahmen und schrieb eine allgemeine Antwort. Es ist überhaupt nichts makein der Frage. Eine Antwort zu schreiben, die nur auf eine zutreffende Antwort zutrifft, makewäre eine sehr spezifische und überraschende Annahme. Meine Antwort funktioniert auch in anderen Fällen makeals bei der Softwareentwicklung, mit Ausnahme Ihres speziellen Anfängerproblems, bei dem Sie Ihre Daten gelöscht haben.
Rich
1
Kusalananda brachte mir das Fischen bei. Sie kamen und sagten: "Da Sie in der Ebene leben, warum essen Sie nicht stattdessen Rindfleisch?"
Raphael