Robuste Skripte
Deine Skripte funktionieren, solange alles gut geht. Robust ist ein Skript, wenn es auch dann vernünftig reagiert, wenn etwas schiefläuft: ein fehlendes Argument, ein Dateiname mit Leerzeichen, ein Befehl, der scheitert. Diese Seite sammelt die Techniken dafür. Am Ende wendest du sie auf addscript.sh und menu.sh an.
Quoting
Abschnitt betitelt „Quoting“Die häufigste Fehlerquelle in Shell-Skripten sind fehlende Anführungszeichen. Die Shell zerlegt Werte ohne Anführungszeichen an Leerzeichen und wertet * und ? als Muster aus:
datei="mein text.txt"touch "$datei"ls -l $datei # sucht zwei Dateien: mein und text.txtls -l "$datei" # sucht eine Datei| Regel | Beispiel |
|---|---|
| Variablen beim Lesen immer in doppelte Anführungszeichen | "$datei", "${menu[0]}" |
| Befehlssubstitution ebenso | "$(basename "$0")" |
| Alle Argumente oder alle Array-Elemente einzeln | "$@", "${menu[@]}" |
| Einfache Anführungszeichen, wenn nichts ersetzt werden soll | '$HOME bleibt wörtlich' |
| Doppelte Anführungszeichen, wenn Variablen ersetzt werden sollen | "Hallo $USER" |
Innerhalb von [[ ]] und (( )) wären Anführungszeichen nicht nötig |
wir setzen sie trotzdem, der Konsistenz zuliebe |
Im Zweifel: Anführungszeichen setzen. Es gibt praktisch keinen Fall, in dem "$var" falsch und $var richtig ist.
Bei Fehlern abbrechen: set -euo pipefail
Abschnitt betitelt „Bei Fehlern abbrechen: set -euo pipefail“Ohne weitere Massnahmen läuft ein Skript nach einem gescheiterten Befehl einfach weiter. Ein cd ins falsche Verzeichnis, gefolgt von rm -rf *, ist der klassische Unfall. Drei Shell-Optionen ändern das. Sie stehen direkt nach dem Kopf, vor den Variablen:
set -euo pipefail| Option | Wirkung |
|---|---|
-e |
Das Skript endet sofort, wenn ein Befehl einen Status ungleich 0 liefert |
-u |
Der Zugriff auf eine nicht gesetzte Variable ist ein Fehler, Tippfehler in Variablennamen fallen sofort auf |
-o pipefail |
Eine Pipe gilt als gescheitert, wenn irgendein Befehl darin scheitert, nicht nur der letzte |
Das ändert das Verhalten spürbar, darum ein paar Stolperfallen und ihre Lösungen:
- Befehle, die scheitern dürfen.
grepliefert Status 1, wenn es nichts findet, und-ebeendet das Skript. Entwedergrep muster datei || true, oder den Befehl direkt als Bedingung verwenden:if grep -q muster datei; then. In Bedingungen vonifundwhilesowie links von&&und||greift-enicht. - Nicht gesetzte Argumente.
dateiname="$1"ohne Argument ist mit-uein Fehler. Erst$#prüfen, dann zuweisen, oder einen Standardwert angeben:"${1:-}"(siehe Parameter-Expansion unten). - Umgebungsvariablen.
$EDITORist nicht überall gesetzt."${EDITOR:-vim}"nimmtvim, wenn die Variable fehlt. - Zähler.
(( i++ ))liefert beii=0den Status 1, weil der Ausdruck den alten Wert 0 ergibt, und beendet das Skript. Schreibe(( i += 1 ))oderi=$(( i + 1 )). - Interaktive Eingaben.
readliefert Status 1 bei Ctrl+D. Das Skript endet dann, was meist in Ordnung ist.
Fehlermeldungen und Exit-Codes
Abschnitt betitelt „Fehlermeldungen und Exit-Codes“Normale Ausgabe geht auf die Standardausgabe, Fehlermeldungen gehören auf die Standardfehlerausgabe. Das erreichst du mit >&2. Wer dein Skript in eine Pipe oder eine Datei umleitet, bekommt die Meldungen trotzdem zu sehen, und sie landen nicht in den Daten:
echo "$programm: Fehler, $dateiname existiert bereits." >&2exit 1Da sich das wiederholt, lohnt sich eine Funktion dafür:
fehler() { echo "$programm: $1" >&2 exit "${2:-1}"}Aufgerufen als fehler "Datei existiert bereits" oder mit eigenem Status als fehler "Argument fehlt" 2.
Bei den Exit-Codes gibt es keine strenge Norm, aber eine verbreitete Praxis:
| Code | Bedeutung |
|---|---|
| 0 | Erfolg |
| 1 | allgemeiner Fehler (Datei fehlt, Befehl gescheitert) |
| 2 | falscher Aufruf (Argumente, Optionen), so machen es auch grep und die Bash |
| 126, 127 | von der Shell reserviert: nicht ausführbar, nicht gefunden |
Wichtig ist nicht die genaue Zahl, sondern dass 0 nur bei Erfolg kommt und der Aufrufer Fehler erkennt.
Aufräumen mit trap und mktemp
Abschnitt betitelt „Aufräumen mit trap und mktemp“Temporäre Dateien legt man nicht in /tmp/meinskript.tmp an, sondern mit mktemp, das einen eindeutigen, sicheren Namen erzeugt. Und man räumt sie auf, auch wenn das Skript vorzeitig endet. Dafür registriert trap eine Funktion, die beim Beenden läuft, egal ob regulär, durch exit 1 oder durch Ctrl+C:
set -euo pipefail
# Variablentmpdir=$(mktemp -d)
# Funktionencleanup() { rm -rf "$tmpdir"}
# Hauptprogrammtrap cleanup EXITecho "Arbeite in $tmpdir"# ... hier passiert die eigentliche Arbeit ...mktemp ohne -d erzeugt eine Datei statt eines Verzeichnisses. Das Muster «anlegen, trap setzen, arbeiten» gehört zu jedem Skript, das Zwischendateien braucht.
Debugging
Abschnitt betitelt „Debugging“Drei Werkzeuge in dieser Reihenfolge:
bash -n skript.sh # nur Syntax prüfen, nichts ausführenbash -x skript.sh # jede Zeile vor der Ausführung anzeigenbash -x zeigt mit vorangestelltem + jeden Befehl, nachdem die Shell Variablen ersetzt hat. So siehst du, was wirklich ausgeführt wird. Wenn nur ein Abschnitt interessiert, schaltest du die Anzeige im Skript ein und aus:
set -x# ... verdächtiger Abschnitt ...set +xUnd Fehlermeldungen lesen: ./menu.sh: line 42: syntax error near unexpected token nennt die Zeile. Der eigentliche Fehler liegt oft ein paar Zeilen davor, etwa ein fehlendes fi oder ;;.
ShellCheck
Abschnitt betitelt „ShellCheck“ShellCheck findet Fehler, die der Bash selbst nicht auffallen: fehlende Anführungszeichen, Variablen, die nie benutzt werden, Konstrukte, die anders funktionieren, als man denkt. Es läuft lokal, in VS Code über die gleichnamige Erweiterung und in der Pipeline für jede Abgabe unter labs/linux/.
sudo apt install shellcheckshellcheck labs/linux/08-skripte/*.shJede Meldung hat eine Nummer, die du im Wiki nachschlagen kannst. Die häufigsten:
| Code | Meldung | Beispiel und Lösung |
|---|---|---|
| SC2086 | Double quote to prevent globbing and word splitting | chmod +x $dateiname wird zu chmod +x "$dateiname" |
| SC2046 | Quote this to prevent word splitting | rm $(cat liste) zerlegt die Ausgabe an Leerzeichen, besser eine Schleife über die Zeilen |
| SC2162 | read without -r will mangle backslashes | read antwort wird zu read -r antwort |
| SC2035 | Use ./*glob* or -- *glob* so names with dashes won’t become options |
ls -l *.sh wird zu ls -l ./*.sh |
| SC2034 | Variable appears unused | Tippfehler im Namen oder Variable tatsächlich überflüssig |
| SC2006 | Use $(…) notation instead of legacy backticks | `date` wird zu $(date) |
Eine Meldung lässt sich mit einem Kommentar in der Zeile davor unterdrücken, zum Beispiel # shellcheck disable=SC2034. Tu das nur, wenn du erklären kannst, warum die Meldung hier nicht zutrifft, und schreibe den Grund dazu.
Parameter-Expansion
Abschnitt betitelt „Parameter-Expansion“Die Bash kann beim Lesen einer Variablen gleich etwas mit dem Wert anstellen. Das erspart viele Aufrufe von basename, cut oder sed:
| Ausdruck | Ergebnis bei pfad="labs/linux/08-skripte/menu.sh" |
|---|---|
${pfad:-standard} |
der Wert, oder standard, falls die Variable leer oder nicht gesetzt ist |
${pfad:?Meldung} |
der Wert, oder Abbruch mit Meldung, falls leer |
${#pfad} |
die Länge: 24 |
${pfad##*/} |
alles bis zum letzten / entfernt: menu.sh (wie basename) |
${pfad%/*} |
ab dem letzten / entfernt: labs/linux/08-skripte (wie dirname) |
${pfad%.sh} |
Endung .sh entfernt: labs/linux/08-skripte/menu |
${pfad/menu/haupt} |
erstes menu durch haupt ersetzt |
${pfad^^}, ${pfad,,} |
alles gross, alles klein |
Merkhilfe: # schneidet vorne ab (auf der Tastatur links von $), % hinten (rechts von $). Einfach heisst kürzeste Übereinstimmung, doppelt längste.
pfad="labs/linux/08-skripte/menu.sh"echo "${pfad##*/}" "${pfad%.sh}" "${#pfad}"echo "${EDITOR:-vim}"Here-Docs
Abschnitt betitelt „Here-Docs“Mehrzeilige Ausgaben oder Dateiinhalte schreibst du nicht mit einem echo über zwölf Zeilen, sondern mit einem Here-Doc. Alles zwischen <<EOF und der Zeile EOF wird zur Eingabe des Befehls:
cat > "$dateiname" <<EOF#!/bin/bash## $dateiname# $zweck## $datum / $verfasser#EOFVariablen werden ersetzt wie in doppelten Anführungszeichen. Soll nichts ersetzt werden, setzt du das Schlusswort in Anführungszeichen: <<'EOF'. Praktisch für Hilfetexte, die Dollarzeichen enthalten. Das Wort EOF ist Konvention, erlaubt ist jedes Wort, es muss nur am Schluss allein auf der Zeile stehen.
Formatierte Ausgabe mit printf
Abschnitt betitelt „Formatierte Ausgabe mit printf“echo reicht für einfache Ausgaben. Sobald Spalten oder Zahlenformate ins Spiel kommen, nimm printf. Der erste Parameter ist der Format-String, die weiteren füllen die Platzhalter:
printf "%-12s %6s\n" "Datei" "Grösse"printf "%-12s %6d\n" "menu.sh" 1234printf "%.2f Prozent\n" 93.456printf "%s\n" "${menu[@]}"%s steht für einen String, %d für eine ganze Zahl, %f für eine Kommazahl. %-12s füllt auf zwölf Zeichen auf, linksbündig. printf hängt keinen Zeilenumbruch an, den schreibst du als \n. Die letzte Zeile zeigt einen Trick: Hat der Format-String weniger Platzhalter als Argumente, wird er wiederholt, so landet jedes Array-Element auf einer eigenen Zeile.
- Einrückung konsequent, in diesem Kurs vier Leerzeichen pro Ebene, keine Tabs.
- Variablen klein mit Unterstrich (
skript_name), Konstanten und exportierte Variablen gross (MAX_VERSUCHE). Funktionen beschreiben eine Tätigkeit (zeige_menu,pruefe_argumente). - Kommentare erklären das Warum, nicht das Was.
# Zähler erhöhenüber(( i += 1 ))hilft niemandem. - Ein Skript, eine Aufgabe. Wird es länger als ein Bildschirm, gehört die Logik in Funktionen, das Hauptprogramm bleibt eine lesbare Abfolge von Aufrufen.
- Kopfzeilen nach der Konvention, Benutzungshinweis bei jedem Skript mit Argumenten.
Lösung anzeigen
Das gehärtete addscript.sh, identisch mit scripts/linux/08-skripte/addscript.sh:
#!/bin/bash## addscript.sh# Erstellt ein neues Skript aus dem Kurs-Template, macht es# ausführbar und öffnet es im Editor.## Benutzung: addscript.sh skriptname.sh ["Zweck des Skripts"]## 2026-11-05 / Vorname Nachname#
set -euo pipefail
# Variablenprogramm="${0##*/}" # Dateiname ohne Pfadbenutzung="Benutzung: $programm skriptname.sh [\"Zweck des Skripts\"]"datum="$(date +%d.%m.%Y)"# Vollständiger Name aus /etc/passwd (Feld 5, bis zum ersten Komma),# falls dort nichts steht der Login-Nameverfasser="$(getent passwd "$USER" | cut -d: -f5 | cut -d, -f1)"verfasser="${verfasser:-$(whoami)}"editor="${EDITOR:-vim}" # Editor aus der Umgebung, sonst vim
# Hauptprogramm
# Aufruf prüfen: mindestens ein Parameterif [[ $# -lt 1 ]]; then echo "$benutzung" >&2 exit 2fidateiname="$1"
# Abbruch, falls die Datei schon existiertif [[ -e "$dateiname" ]]; then echo "$programm: Fehler, $dateiname existiert bereits." >&2 exit 1fi
# Zweck als zweiter Parameter, sonst nachfragenif [[ -n "${2:-}" ]]; then zweck="$2"else read -r -p "Zweck des Skripts: " zweckfi
# Template schreiben (Here-Doc: alles bis EOF landet in der Datei)cat > "$dateiname" <<EOF#!/bin/bash## ${dateiname##*/}# $zweck## $datum / $verfasser#
# Variablen
# Funktionen
# HauptprogrammEOF
# Ausführbar machen und im Editor öffnenchmod +x "$dateiname""$editor" "$dateiname"Was sich gegenüber Teil 1 geändert hat und warum:
dateiname="$1"steht erst nach der Argumentprüfung, sonst meldet-ueine nicht gesetzte Variable."${2:-}"statt"$2"aus demselben Grund.${0##*/}ersetztbasename "$0", ein Prozess weniger.${verfasser:-$(whoami)}fängt ein leeres Namensfeld in/etc/passwdab.
In menu.sh sind zwei Stellen betroffen, beide im case:
0) echo "=> ${menu[0]}" ls -l ./*.sh ls -l ./*.sh 2>/dev/null || echo "Keine Skripte in $PWD gefunden." ;; 1) echo "=> ${menu[1]}" read -r -p "Name des Skripts: " skriptname read -r -p "Zweck des Skripts: " skriptzweck "$skriptpfad/addscript.sh" "$skriptname" "$skriptzweck" "$skriptpfad/addscript.sh" "$skriptname" "$skriptzweck" \ || echo "addscript.sh hat einen Fehler gemeldet." ;;Ohne das || würde mit set -e ein Verzeichnis ohne Skripte oder ein Fehler in addscript.sh das ganze Menü beenden. Das vollständige Skript liegt unter scripts/linux/08-skripte/menu.sh.
Selbstcheck · 6 Fragen
Weiterführend
Abschnitt betitelt „Weiterführend“- Bash Reference Manual, die offizielle Referenz, besonders die Abschnitte zu Shell Parameter Expansion und Conditional Constructs.
- BashGuide und die BashPitfalls, die bekannteste Sammlung von Stolperfallen.
- ShellCheck Wiki, jede Meldung mit Beispiel und Begründung.