Warum schlagen?
Es gibt Arrays und abgesicherten Modus in Bash. Bei korrekter Verwendung entspricht bash fast den sicheren Codierungspraktiken.
Es ist schwieriger, bei Fischen einen Fehler zu machen, aber es gibt keinen abgesicherten Modus. Daher sollte das Prototyping in Fischen und die anschließende Übersetzung von Fisch zu Bash eine gute Idee sein, wenn Sie wissen, wie man es richtig macht.
Vorwort
Dieses Handbuch liegt ShellHarden bei, der Autor empfiehlt jedoch auch
ShellCheck, damit die ShellHarden-Regeln nicht von ShellCheck abweichen.
Bash ist keine Sprache, in der
der einfachste Weg, ein Problem gleichzeitig zu lösen, der einfachste ist . Wenn Sie die Bash-Safe-Programmierprüfung ablegen,
lautet die erste Regel von
BashPitfalls : Verwenden Sie immer Anführungszeichen.
Die Hauptsache, die Sie über das Programmieren in Bash wissen müssen
Manische Anführungszeichen! Eine nicht zitierte Variable sollte als gespannte Bombe betrachtet werden: Sie explodiert bei Kontakt mit einem Raum. Ja, es explodiert in dem Sinne, dass
ein String in ein Array unterteilt wird . Insbesondere Variablenerweiterungen wie
$var
und Befehlssubstitutionen wie
$(cmd)
werden
wortgeteilt, wenn die innere Zeichenfolge aufgrund der Aufteilung in eine spezielle
$IFS
Variable mit einem Standardraum in ein Array erweitert wird. Dies ist normalerweise unsichtbar, da das Ergebnis meistens ein Array von 1 Element ist, das nicht von der erwarteten Zeichenfolge zu unterscheiden ist.
Dies wird nicht nur erweitert, sondern auch Platzhalter (
*?
). Dieser Vorgang wird ausgeführt, nachdem das Wort geteilt wurde. Wenn das Wort mindestens einen Platzhalter enthält, wird das Wort zu einem Platzhalter, der für alle geeigneten Dateipfade gilt. Diese Funktion beginnt also, auf das Dateisystem angewendet zu werden!
Das Anführungszeichen unterdrückt die Wortteilung und Mustererweiterung für Variablen und Befehlsersetzungen.
Variable Erweiterung:
- Gut:
"$my_var"
- Schlecht:
$my_var
Befehlsersetzung:
- Gut:
"$(cmd)"
- Schlecht:
$(cmd)
Es gibt Ausnahmen mit optionalen Anführungszeichen, aber Anführungszeichen werden niemals schaden. Die allgemeine Regel lautet, keine nicht zitierten Variablen in Anführungszeichen zu setzen, damit wir nicht nach Randausnahmen für Sie suchen. Es sieht falsch aus und die falsche Praxis ist weit genug verbreitet, um Verdacht zu erregen: Viele Skripte wurden mit fehlerhafter Verarbeitung von Dateinamen und Leerzeichen geschrieben ...
ShellHarden erwähnt nur wenige Ausnahmen - sind diese Variablen mit numerischen Inhalten wie
$?
,
$#
und
${#array[@]}
.
Muss ich Backticks verwenden?
Befehlsersetzungen können auch die folgende Form haben:
- Richtig:
"`cmd`"
- Schlecht:
`cmd`
Obwohl dieser Stil korrekt verwendet werden kann, ist er in Anführungszeichen weniger praktisch und im verschachtelten Zustand weniger lesbar. Der Konsens hier ist ziemlich klar: Vermeiden Sie es.
ShellHarden schreibt solche Häkchen in Klammern in Dollar um.
Müssen geschweifte Klammern verwendet werden?
Klammern werden zum Interpolieren von Zeichenfolgen verwendet, daher sind sie normalerweise redundant:
- Schlecht:
some_command $arg1 $arg2 $arg3
- Schlecht und ausführlich:
some_command ${arg1} ${arg2} ${arg3}
- Gut, aber ausführlich:
some_command "${arg1}" "${arg2}" "${arg3}"
- Gut:
some_command "$arg1" "$arg2" "$arg3"
Theoretisch ist die Verwendung von geschweiften Klammern kein Problem, aber nach den Erfahrungen Ihres Autors besteht eine starke negative Korrelation zwischen der unnötigen Verwendung von geschweiften Klammern und der korrekten Verwendung von Anführungszeichen - fast jeder wählt die Form "schlecht und ausführlich" anstelle der Form "gut, aber ausführlich"!
Theorien Ihres Autors:
- Aus Angst, etwas falsch zu machen: Anstelle der tatsächlichen Gefahr (fehlende Anführungszeichen) können Anfänger befürchten, dass die Variable
$prefix
Variable "$prefix_postfix"
erweitert, dies funktioniert jedoch nicht. - Frachtkult: Schreiben von Code im Bund der falschen Angst, die ihm vorausging.
- Klammern konkurrieren mit Anführungszeichen um die Grenze der zulässigen Ausführlichkeit.
Daher wurde beschlossen, unnötige geschweifte Klammern zu verbieten: ShellHarden ersetzt diese Optionen durch die einfachste gute Form.
Und nun zur String-Interpolation, bei der geschweifte Klammern wirklich nützlich sind:
- Schlecht (Verkettung):
$var1"more string content"$var2
- Gut (Verkettung):
"$var1""more string content""$var2"
- Gut (Interpolation):
"${var1}more string content${var2}"
Verkettung und Interpolation in Bash sind selbst in Arrays gleichwertig (was lächerlich ist).
Da ShellHarden keine Stile formatiert, soll der richtige Code nicht geändert werden. Dies gilt für die Option „gut (Interpolation)“: Aus Sicht von ShellHarden ist dies die kanonisch korrekte Form.
ShellHarden fügt jetzt nach Bedarf geschweifte Klammern hinzu und entfernt sie: In einem schlechten Beispiel wird var1 mit Klammern geliefert, aber für var2 sind sie auch im Fall von „gut (Interpolation)“ nicht zulässig, da sie am Ende der Zeile nie benötigt werden. Die letzte Anforderung kann durchaus umgekehrt werden.
Gotcha: nummerierte Argumente
Im Gegensatz zu normalen Variablenbezeichnernamen (in
[_a-zA-Z][_a-zA-Z0-9]*
:
[_a-zA-Z][_a-zA-Z0-9]*
) erfordern nummerierte Argumente Klammern (
[_a-zA-Z][_a-zA-Z0-9]*
nicht). ShellCheck sagt:
echo "$10" ^-- SC1037: Braces are required for positionals over 9, eg ${10}.
ShellHarden weigert sich, das Problem zu beheben (hält den Unterschied für zu subtil).
Da Klammern bis zu 9 zulässig sind, lässt ShellHarden sie für alle nummerierten Argumente zu.
Arrays verwenden
Um alle Variablen zitieren zu können, müssen Sie echte Arrays verwenden, keine pseudomassiven Zeichenfolgen, die durch Leerzeichen getrennt sind.
Die Syntax ist ausführlich, aber Sie müssen damit umgehen. Dieser Bashismus ist nur ein Grund, die POSIX-Kompatibilität für die meisten Shell-Skripte aufzugeben.
Gut:
array=( a b ) array+=(c) if [ ${#array[@]} -gt 0 ]; then rm -- "${array[@]}" fi
Schlecht:
pseudoarray=" \ a \ b \ " pseudoarray="$pseudoarray c" if ! [ "$pseudoarray" = '' ]; then rm -- $pseudoarray fi
Deshalb sind Arrays eine so grundlegende Funktion für eine Shell: Die
Argumente von Befehlen sind im Grunde Arrays (und Shell-Skripte sind Befehle und Argumente). Wir können sagen, dass die Hülle, die es künstlich unmöglich macht, mehrere Argumente vorzubringen, komisch und wertlos sein wird. Einige gängige Muscheln aus dieser Kategorie sind
Dash und Busybox Ash. Dies sind minimale POSIX-kompatible Shells - aber was nützt die Kompatibilität, wenn das Wichtigste
nicht auf POSIX ist?
Ausnahmefälle, in denen Sie wirklich eine Linie brechen werden
Beispiel mit
\v
als Datentrennzeichen (beachten Sie das zweite Vorkommen):
IFS=$'\v' read -d '' -ra a < <(printf '%s\v' "$s") || true
Auf diese Weise vermeiden wir die Erweiterung von Vorlagen, und die Methode funktioniert auch dann, wenn das Datentrennzeichen
\n
. Das zweite Auftreten des Datentrennzeichens schützt das letzte Element, wenn es sich als Leerzeichen herausstellt. Aus irgendeinem Grund sollte die Option
-d
erster Stelle stehen.
-rad ''
verlockend,
-rad ''
Optionen in
-rad ''
, aber es funktioniert nicht. Da read in diesem Fall einen Wert ungleich Null zurückgibt, sollte es vor errexit (
|| true
) geschützt werden, falls aktiviert. Getestet in Bash 4.0, 4.1, 4.2, 4.3 und 4.4.
Alternative für Bash 4.4:
readarray -td $'\v' a < <(printf '%s\v' "$s")
Wo man ein Bash-Skript startet
Von so etwas:
Dies beinhaltet:
- Shebang:
- Portabilitätsprobleme: Der absolute Pfad zu
env
wahrscheinlich besser für die Portabilität als der absolute Pfad zu bash
. Sie können sich das Beispiel von NixOS ansehen . POSIX erfordert env , aber nicht bash. - Sicherheitsprobleme: Für keine Sprache werden Optionen wie
-euo pipefail
hier nicht positiv akzeptiert! Dies wird unmöglich, wenn Sie die env
Umleitung verwenden, aber selbst wenn Ihr Shebang mit #!/bin/bash
beginnt, ist dies nicht der Ort für Parameter, die sich auf den Wert des Skripts auswirken, da sie überschrieben werden können, wodurch das Skript falsch ausgeführt werden kann. Als Bonus können jedoch Optionen neu definiert werden, die den Wert des Skripts nicht beeinflussen, z. B. set -x
, falls verwendet.
- Was brauchen wir aus dem inoffiziellen Bash-Strict-Modus mit dem
set -u
Feature-Check. Wir brauchen nicht den strengen Bash-Modus, da Shellcheck / Shellharden-Kompatibilität bedeutet, alles und alles zu zitieren, was viel strenger ist. Darüber hinaus sollte die Option set -u
in Bash 4.3 und früheren set -u
nicht verwendet werden . Da diese Option leere Arrays in diesen Versionen als verworfen betrachtet , können Arrays nicht für die hier beschriebenen Zwecke verwendet werden. Die Verwendung von Arrays ist der zweitwichtigste Tipp in diesem Handbuch (nach Anführungszeichen) und der einzige Grund, warum wir die Kompatibilität mit POSIX opfern. Dies ist also keineswegs inakzeptabel: Verwenden Sie entweder gar nicht set -u
oder Bash 4.4 oder eine andere normale Shell wie Zsh. Dies ist leichter gesagt als getan, da die Möglichkeit besteht, dass jemand Ihr Skript in der alten Version von Bash noch ausführt. Glücklicherweise funktioniert alles, was mit set -u
funktioniert, ohne es (für set -e
kann man das nicht sagen). Aus diesem Grund ist es wichtig, die Versionsprüfung zu verwenden. Beachten Sie die Annahme, dass das Testen und Entwickeln in einer mit Bash 4.4 kompatiblen Shell stattfindet (daher wird der Aspekt set -u
getestet). Wenn Sie dies stört, können Sie auch die Kompatibilität verweigern (das Skript schlägt fehl, wenn die Versionsüberprüfung fehlschlägt) oder set -u
ablehnen. shopt -s nullglob
erzwingt, for f in *.txt
korrekt funktioniert, wenn *.txt
keine Dateien findet. Das Standardverhalten (auch Passglob genannt ) übergibt die Vorlage unverändert, was im Falle eines Null-Ergebnisses aus mehreren Gründen gefährlich ist. Für Globstar aktiviert dies die rekursive Suche. Substitution ist einfacher zu verwenden als zu find
. Also benutze es.
Aber nicht:
IFS='' set -f shopt -s failglob
- Wenn Sie das interne Feldtrennzeichen auf eine leere Zeichenfolge setzen, kann das Wort nicht geteilt werden. Klingt nach der perfekten Lösung. Leider ist dies ein unvollständiger Ersatz für das Zitieren von Variablen und Befehlsersetzungen, und da Sie Anführungszeichen verwenden, gibt es nichts. Der Grund, warum Anführungszeichen weiterhin verwendet werden müssen, liegt darin, dass ansonsten leere Zeichenfolgen zu leeren Arrays werden (wie im
test $x = ""
) und eine indirekte Vorlagenerweiterung weiterhin möglich ist. Darüber hinaus verursachen Probleme mit dieser Variablen auch Probleme mit Befehlen wie read
, wodurch Konstruktionen wie cat /etc/fstab | while read -r dev mnt fs opt dump pass; do echo "$fs"; done'
cat /etc/fstab | while read -r dev mnt fs opt dump pass; do echo "$fs"; done'
cat /etc/fstab | while read -r dev mnt fs opt dump pass; do echo "$fs"; done'
. - Die Vorlagenerweiterung ist deaktiviert: nicht nur die berüchtigte indirekte Erweiterung, sondern auch die problemlose direkte Erweiterung, die Sie, wie gesagt, verwenden sollten. Es ist also schwer zu akzeptieren. Dies ist auch für ein Shellcheck / Shellharden-kompatibles Skript völlig optional.
- Im Gegensatz zu nullglob schlägt failglob mit einem Null-Ergebnis fehl. Obwohl dies für die meisten Befehle sinnvoll ist, z. B.
rm -- *.txt
(da für die meisten Befehle immer noch keine Ausführung mit einem Ergebnis von Null erwartet wird), kann failglob offensichtlich nur verwendet werden, wenn Sie kein Ergebnis von Null erwarten. Dies bedeutet, dass Sie normalerweise keine Gruppenvorlagen in Befehlsargumenten platzieren, es sei denn, Sie nehmen dasselbe an. Was jedoch immer passieren kann, ist die Verwendung von nullglob und die Erweiterung der Vorlage auf null Argumente in Konstrukten, die diese annehmen können, z. B. eine Schleife oder das Zuweisen von Werten zu einem Array ( txt_files=(*.txt)
).
So vervollständigen Sie ein Bash-Skript
Der Skript-Exit-Status ist der Status des zuletzt ausgeführten Befehls. Stellen Sie sicher, dass es sich um echten Erfolg oder Misserfolg handelt.
Das Schlimmste ist, die Lösung einem nicht verwandten Zustand in Form einer UND-Liste am Ende des Skripts zu überlassen. Wenn die Bedingung falsch ist, ist der zuletzt ausgeführte Befehl die Bedingung selbst.
Für den Errexit werden die Bedingungen in Form einer UND-Liste überhaupt nicht verwendet. Wenn errexit nicht verwendet wird, sollten Sie die Behandlung von Fehlern auch für den letzten Befehl in Betracht ziehen, damit der Beendigungsstatus nicht maskiert wird, wenn dem Skript zusätzlicher Code hinzugefügt wird.
Schlecht:
condition && extra_stuff
Gut (errexit Option):
if condition; then extra_stuff fi
Gut (Fehlerbehandlungsoption):
if condition; then extra_stuff || exit fi exit 0
Wie man errexit benutzt
Wie
set -e
.
Verzögerte Bereinigung auf Programmebene
Wenn errexit ordnungsgemäß funktioniert, installieren Sie auf diese Weise alle erforderlichen Bereinigungen beim Beenden.
tmpfile="$(mktemp -t myprogram-XXXXXX)" cleanup() { rm -f "$tmpfile" } trap cleanup EXIT
Gefangen: errexit wird in Befehlsargumenten ignoriert
Hier ist eine sehr knifflige "Bombe", deren Verständnis mir sehr viel wert war. Mein Build-Skript hat auf verschiedenen Entwicklungsmaschinen einwandfrei funktioniert, aber den Build-Server in die Knie gezwungen:
set -e
Richtig (Befehlsersetzung in der Aufgabe):
set -e
Warnung: Integrierte
local
und
export
Befehle bleiben Befehle, daher bleibt dies immer noch falsch:
set -e
ShellCheck warnt nur vor speziellen Befehlen wie in diesem Fall
local
.
Um
local
, trennen Sie die Deklaration vom Job:
set -e
Gefangen: errexit wird je nach Kontext des Anrufers ignoriert
Manchmal ist POSIX schrecklich. Errexit wird in Funktionen, Gruppenbefehlen und sogar Subshells ignoriert, wenn der Aufrufer den Erfolg überprüft. Alle diese Beispiele drucken
Unreachable
und
Great success
, wie seltsam es auch erscheinen mag.
Unterschale:
( set -e false echo Unreachable ) && echo Great success
Gruppenteam:
{ set -e false echo Unreachable } && echo Great success
Funktion:
f() { set -e false echo Unreachable } f && echo Great success
Aus diesem Grund ist bash mit errexit praktisch nicht zum Verknüpfen geeignet: Ja,
es ist möglich, errexit-Funktionen so
zu verpacken, dass sie funktionieren, aber es gibt Zweifel, dass sich der eingesparte Aufwand (bei der expliziten Fehlerbehandlung) lohnt. Ziehen Sie stattdessen die Aufteilung in vollständig autonome Skripte in Betracht.
Vermeiden Sie es, die Shell mit falschen Anführungszeichen aufzurufen
Wenn Sie Befehle aus anderen Programmiersprachen aufrufen, ist es am einfachsten, einen Fehler zu machen und die Shell implizit aufzurufen. Wenn dieser Shell-Befehl statisch ist, ist er gut - er funktioniert entweder oder nicht. Aber wenn Ihr Programm die Zeilen zum Erstellen dieses Befehls irgendwie verarbeitet, müssen Sie verstehen - Sie
generieren ein Shell-Skript ! Ich möchte das selten tun und es ist sehr anstrengend, alles richtig zu arrangieren:
- zitiere jedes Argument;
- Escape die entsprechenden Zeichen in den Argumenten.
Unabhängig davon, in welcher Programmiersprache Sie dies tun, gibt es mindestens drei Möglichkeiten, ein Team korrekt aufzubauen. In der Reihenfolge der Präferenz:
Plan A: Verzichten Sie auf eine Muschel
Wenn dies nur ein Befehl mit Argumenten ist (dh keine Shell-Funktionen wie Pipelining oder Redirecting), wählen Sie eine Array-Option aus.
- Bad (python3): subprocess.check_call
subprocess.check_call('rm -rf ' + path)
- Gut (python3): subprocess.check_call
subprocess.check_call(['rm', '-rf', path])
Schlecht (C ++):
std::string cmd = "rm -rf "; cmd += path; system(cmd);
Gut (C / POSIX), minus Fehlerbehandlung:
char* const args[] = {"rm", "-rf", path, NULL}; pid_t child; posix_spawnp(&child, args[0], NULL, NULL, args, NULL); int status; waitpid(child, &status, 0);
Plan B: Ein statisches Shell-Skript
Wenn eine Shell erforderlich ist, lassen Sie die Argumente Argumente sein. Sie könnten denken, dass es umständlich war, ein spezielles Shell-Skript in Ihre eigene Datei zu schreiben und darauf zuzugreifen, bis Sie einen solchen Trick sehen:
Bad (python3): subprocess.check_call
subprocess.check_call('docker exec {} bash -ec "printf %s {} > {}"'.format(instance, content, path))
Gut (python3): subprocess.check_call
subprocess.check_call(['docker', 'exec', instance, 'bash', '-ec', 'printf %s "$0" > "$1"', content, path])
Können Sie das Shell-Skript bemerken?
Richtig, der Befehl printf wird umgeleitet. Achten Sie auf korrekt zitierte nummerierte Argumente. Das Implementieren eines statischen Shell-Skripts ist in Ordnung.
Diese Beispiele werden in Docker ausgeführt, da sie sonst nicht so nützlich sind. Docker ist jedoch auch ein hervorragendes Beispiel für einen Befehl, der andere Befehle basierend auf Argumenten ausführt. Im Gegensatz zu Ssh, wie wir später sehen werden.
Letzte Option: Zeilenverarbeitung
Wenn es
sich um eine Zeichenfolge handeln sollte (z. B. weil sie über
ssh
funktionieren muss), kann sie nicht umgangen werden. Sie müssen jedes Argument zitieren und alle Zeichen maskieren, die zum Beenden dieser Anführungszeichen erforderlich sind. Am einfachsten ist es, in einfache Anführungszeichen zu wechseln, da diese die einfachsten Escape-Regeln enthalten. Nur eine Regel:
'
→
'\"
.
Typischer Dateiname in einfachen Anführungszeichen:
echo 'Don'\''t stop (12" dub mix).mp3'
Wie verwende ich diesen Trick, um ssh-Befehle sicher auszuführen? Es ist unmöglich! Hier ist die „oft richtige“ Lösung:
- Die "oft korrekte" Lösung (python3): subprocess.check_call
subprocess.check_call(['ssh', 'user@host', "sha1sum '{}'".format(path.replace("'", "'\\''"))])
Wir selbst müssen alle Argumente zu einer Zeichenfolge kombinieren, damit Ssh es nicht falsch macht: Wenn Sie versuchen, mehrere ssh-Argumente zu übergeben, werden die Argumente ohne Anführungszeichen auf verräterische Weise kombiniert.
Der Grund, warum dies normalerweise nicht möglich ist, liegt darin, dass die richtige Entscheidung von den Vorlieben des Benutzers am anderen Ende abhängt, nämlich von der Remote-Shell, die alles sein kann. Im Grunde könnte es sogar deine Mutter sein. Es ist „oft richtig“ anzunehmen, dass die Remote-Shell Bash oder eine andere POSIX-kompatible Shell ist, aber
Fisch ist zu diesem Zeitpunkt nicht kompatibel .