Shellscripts schreiben — Vom ersten Skript zur Automatisierung
Ein Bash-Script ist eine Textdatei mit Befehlen, die die Shell der Reihe nach abarbeitet — so, als ob du sie einzeln im Terminal eingeben würdest. Der Unterschied: Statt zwanzig Befehle jedes Mal neu zu tippen, rufst du einmal das Script auf. Dieser Artikel zeigt dir Schritt für Schritt, wie ein Script entsteht, wie du Variablen, Eingaben, Bedingungen und Schleifen einsetzt — und am Ende steht ein vollständiges Praxisbeispiel, das einen Ordner automatisch aufräumt.
Dein erstes Script: Datei anlegen und ausführbar machen
Jedes Bash-Script beginnt mit der sogenannten Shebang-Zeile — sie teilt dem System mit, welcher Interpreter die Datei ausführen soll. Ohne sie ist die Datei nur eine Textdatei.
Es gibt zwei gebräuchliche Varianten:
#!/bin/bash
#!/usr/bin/env bash
#!/bin/bash funktioniert auf allen Standard-Linux-Systemen, wo Bash unter /bin/bash liegt. #!/usr/bin/env bash ist portabler: env sucht Bash im aktuellen PATH, was auf macOS, Arch Linux oder Systemen mit Custom-Bash-Installationen zuverlässiger greift. Für den Desktop-Einsatz unter Ubuntu oder Mint machen beide keinen praktischen Unterschied — wähle eine und bleibe dabei.
Script anlegen, Shebang eintragen, ausführbar machen:
# Neue Datei anlegen und öffnen:
$ nano mein-script.sh
# Inhalt:
#!/bin/bash
echo "Hallo, $(whoami)!"
echo "Heute ist: $(date +%A, %d. %B %Y)"
Speichern (Strg + O), schließen (Strg + X), dann ausführbar machen:
$ chmod +x mein-script.sh
Drei Wege, das Script zu starten:
# 1. Mit ./ im aktuellen Verzeichnis (nutzt die Shebang-Zeile):
$ ./mein-script.sh
Hallo, gunter!
Heute ist: Samstag, 04. April 2026
# 2. Explizit mit bash aufrufen (Shebang wird ignoriert):
$ bash mein-script.sh
Hallo, gunter!
Heute ist: Samstag, 04. April 2026
# 3. Mit source ausführen (läuft in der aktuellen Shell, kein eigener Prozess):
$ source mein-script.sh
Der Unterschied zwischen ./script.sh und source script.sh ist entscheidend: Beim normalen Aufruf startet Bash einen Kindprozess — Variablen, die darin gesetzt werden, verschwinden danach. Bei source läuft das Script in deiner aktuellen Shell-Sitzung. Das ist nützlich, wenn das Script Variablen oder Funktionen setzen soll, die danach noch zur Verfügung stehen — zum Beispiel bei Konfigurationsdateien wie .bashrc.
~/.local/bin/-Verzeichnis ablegen und dort ausführbar machen — dann kannst du sie wie normale Befehle aufrufen, ohne ./ oder den vollen Pfad anzugeben. Das Verzeichnis ist auf modernen Ubuntu-Systemen automatisch im PATH.Variablen: Daten speichern und abrufen
Variablen speichern Werte, die du später im Script wiederverwenden willst. Das wichtigste Detail: Beim Zuweisen gibt es keine Leerzeichen um das Gleichheitszeichen — sonst interpretiert Bash den Variablennamen als Befehl.
#!/bin/bash
# Richtig:
name="Gunter"
anzahl=42
pfad="/home/gunter/dokumente"
# Falsch — führt zu Fehlermeldungen:
# name = "Gunter" # bash: name: Befehl nicht gefunden
Wert einer Variable abrufen mit $variable oder sicherer mit "${variable}":
echo "Hallo, $name!"
echo "Anzahl: ${anzahl} Dateien"
# Doppelte Anführungszeichen verhindern Wortaufteilung bei Leerzeichen im Wert:
datei="mein dokument.txt"
echo "$datei" # Ausgabe: mein dokument.txt — korrekt
ls $datei # Fehler: ls versucht "mein" und "dokument.txt" separat
ls "$datei" # Korrekt: ls bekommt den Wert als einen String
Bash kennt außerdem eine Reihe vordefinierter Sondervariablen:
#!/bin/bash
echo "Scriptname: $0" # Name des Scripts selbst
echo "Erstes Argument: $1" # Erster Wert nach dem Scriptaufruf
echo "Zweites Argument: $2"
echo "Anzahl Argumente: $#" # Wie viele Argumente wurden übergeben?
echo "Alle Argumente: $@" # Alle Argumente als separate Wörter
echo "Letzter Exit: $?" # Exit-Status des letzten Befehls (0 = OK)
echo "Prozess-ID: $$" # PID dieser Shell-Sitzung
$ ./mein-script.sh alpha beta gamma
Scriptname: ./mein-script.sh
Erstes Argument: alpha
Zweites Argument: beta
Anzahl Argumente: 3
Alle Argumente: alpha beta gamma
Letzter Exit: 0
Prozess-ID: 14832
"$variable". Das verhindert unerwartetes Verhalten, wenn der Wert Leerzeichen oder Sonderzeichen enthält — einer der häufigsten Anfängerfehler in Bash-Scripts.Benutzereingaben: read, Argumente und Passwörter
Scripts werden nützlicher, wenn sie auf Eingaben reagieren können — entweder interaktiv über read oder über Argumente beim Aufruf.
read liest eine Zeile von der Standardeingabe und speichert sie in einer Variable:
#!/bin/bash
# Einfache Eingabe:
read benutzername
echo "Du hast eingegeben: $benutzername"
# Mit Prompt-Text (-p) in einer Zeile:
read -p "Wie heißt du? " name
echo "Willkommen, $name!"
# Passwort eingeben (keine Anzeige, -s = silent):
read -s -p "Passwort: " passwort
echo "" # Zeilenumbruch nach der versteckten Eingabe
echo "Passwort hat ${#passwort} Zeichen." # Länge, nicht Inhalt!
$ ./eingabe.sh
Wie heißt du? Gunter
Willkommen, Gunter!
Passwort:
Passwort hat 8 Zeichen.
Für Scripts, die ohne Interaktion auskommen sollen — zum Beispiel in Cron-Jobs — werden Werte als Argumente übergeben:
#!/bin/bash
# Aufruf: ./backup.sh /home/gunter /mnt/backup
quelle="$1"
ziel="$2"
if [ -z "$quelle" ] || [ -z "$ziel" ]; then
echo "Verwendung: $0 "
exit 1
fi
echo "Sichere $quelle nach $ziel ..."
$ ./backup.sh /home/gunter /mnt/backup
Sichere /home/gunter nach /mnt/backup ...
Bedingte Ausführung mit if
Mit if entscheidet das Script, welcher Codeblock ausgeführt wird — abhängig davon, ob eine Bedingung wahr oder falsch ist. Die Grundstruktur:
if BEDINGUNG; then
# wird ausgeführt, wenn Bedingung wahr
elif ANDERE_BEDINGUNG; then
# alternative Bedingung
else
# wenn keine Bedingung zutrifft
fi
Bedingungen werden mit [ ] (POSIX-kompatibel) oder [[ ]] (Bash-spezifisch, leistungsfähiger) formuliert. Für Bash-Scripts empfehle ich [[ ]] — es kennt reguläre Ausdrücke, verarbeitet leere Variablen robuster und braucht keine Anführungszeichen um Variablen mit Leerzeichen.
Dateibedingungen:
#!/bin/bash
datei="/home/gunter/notizen.txt"
if [[ -f "$datei" ]]; then
echo "Datei existiert."
elif [[ -d "$datei" ]]; then
echo "Das ist ein Verzeichnis, keine Datei."
else
echo "Existiert nicht."
fi
# Weitere Test-Ausdrücke für Dateien:
# -e Existiert (Datei oder Verzeichnis)
# -f Ist eine reguläre Datei
# -d Ist ein Verzeichnis
# -r Ist lesbar
# -w Ist schreibbar
# -x Ist ausführbar
# -s Ist nicht leer (size > 0)
String-Bedingungen:
#!/bin/bash
read -p "Eingabe: " wert
if [[ "$wert" = "ja" ]]; then
echo "Bestätigt."
elif [[ "$wert" != "nein" ]]; then
echo "Weder ja noch nein."
fi
# -z String ist leer (zero length)
# -n String ist nicht leer (non-zero)
if [[ -z "$wert" ]]; then
echo "Nichts eingegeben."
fi
Zahlen-Bedingungen:
#!/bin/bash
anzahl=15
if [[ $anzahl -eq 0 ]]; then
echo "Keine Dateien."
elif [[ $anzahl -gt 100 ]]; then
echo "Mehr als 100 Dateien."
else
echo "$anzahl Dateien gefunden."
fi
# -eq gleich (equal)
# -ne ungleich (not equal)
# -lt kleiner als (less than)
# -gt größer als (greater than)
# -le kleiner oder gleich
# -ge größer oder gleich
bc oder awk zurückgreifen — Bash selbst rechnet nur mit Integern.Schleifen: for, while, until
Schleifen wiederholen einen Codeblock — für jedes Element einer Liste, solange eine Bedingung gilt, oder eine bestimmte Anzahl von Malen.
for-Schleife über eine Liste:
#!/bin/bash
# Über eine feste Liste:
for farbe in rot grün blau; do
echo "Farbe: $farbe"
done
# Ausgabe:
# Farbe: rot
# Farbe: grün
# Farbe: blau
for-Schleife mit Zahlbereich:
#!/bin/bash
# Brace Expansion {1..10}:
for i in {1..5}; do
echo "Durchlauf $i"
done
# C-Stil (bash-spezifisch):
for ((i=1; i<=5; i++)); do
echo "Schritt $i von 5"
done
for-Schleife über Dateien:
#!/bin/bash
# Alle .txt-Dateien im aktuellen Verzeichnis:
for datei in *.txt; do
echo "Gefunden: $datei ($(wc -l < "$datei") Zeilen)"
done
while-Schleife:
#!/bin/bash
# Solange Bedingung wahr:
zaehler=1
while [[ $zaehler -le 5 ]]; do
echo "Zaehler: $zaehler"
((zaehler++))
done
# while read: Datei Zeile für Zeile verarbeiten:
while IFS= read -r zeile; do
echo "Zeile: $zeile"
done < eingabe.txt
until-Schleife — Gegenstück zu while, läuft solange die Bedingung falsch ist:
#!/bin/bash
versuch=0
until [[ $versuch -ge 3 ]]; do
echo "Versuch $((versuch+1))"
((versuch++))
done
break bricht die Schleife sofort ab, continue überspringt den Rest des aktuellen Durchlaufs und springt zum nächsten:
#!/bin/bash
for i in {1..10}; do
if [[ $i -eq 4 ]]; then
continue # 4 überspringen
fi
if [[ $i -eq 8 ]]; then
break # bei 8 aufhören
fi
echo "$i"
done
# Ausgabe: 1 2 3 5 6 7
Case-Anweisung: Elegante Mehrfachauswahl
Wenn ein Wert viele mögliche Ausprägungen hat, wird eine Kette von elif-Blöcken schnell unübersichtlich. case ist hier lesbarer und effizienter — Bash übersetzt es intern direkt, ohne jede Bedingung einzeln zu prüfen.
#!/bin/bash
# Einfaches Menü-Script
echo "Was möchtest du tun?"
echo " 1) Festplatte prüfen"
echo " 2) Speicher anzeigen"
echo " 3) Uptime ausgeben"
echo " q) Beenden"
echo ""
read -p "Auswahl: " wahl
case "$wahl" in
1)
df -h
;;
2)
free -h
;;
3)
uptime
;;
q|Q)
echo "Tschüss!"
exit 0
;;
*)
echo "Ungültige Eingabe: $wahl"
exit 1
;;
esac
$ ./menue.sh
Was möchtest du tun?
1) Festplatte prüfen
2) Speicher anzeigen
3) Uptime ausgeben
q) Beenden
Auswahl: 2
total used free shared buff/cache available
Mem: 15Gi 4,2Gi 8,1Gi 312Mi 3,1Gi 10Gi
Swap: 2,0Gi 0B 2,0Gi
Das Pattern q|Q zeigt, dass case mehrere Muster mit | verbinden kann. Der Abschluss *) fängt alles ab, was kein anderes Muster trifft — vergleichbar mit else bei if.
;; nach jedem Block ist Pflicht — es beendet den Zweig. Vergisst du es, gibt Bash eine Fehlermeldung. In neueren Bash-Versionen (ab 4.0) gibt es auch ;& (Fallthrough) und ;;& (weiter testen), aber für Menüscripts reicht ;; immer.Funktionen: Code wiederverwenden
Funktionen bündeln zusammengehörigen Code unter einem Namen. Sie vermeiden Wiederholungen und machen Scripts lesbarer und testbarer. Definiert werden sie vor dem ersten Aufruf — entweder mit function name {} oder einfacher als name() {}:
#!/bin/bash
# Definition:
begruessung() {
local name="$1" # lokale Variable, nur innerhalb der Funktion sichtbar
echo "Hallo, $name!"
}
# Aufruf — wie ein Befehl, Argumente wie beim Script:
begruessung "Gunter"
begruessung "Welt"
Hallo, Gunter!
Hallo, Welt!
Wichtig: local für Variablen innerhalb von Funktionen. Ohne local sind Variablen global — sie überschreiben gleichnamige Variablen außerhalb der Funktion. Das führt zu schwer findbaren Fehlern:
#!/bin/bash
ergebnis="leer"
berechne() {
local ergebnis="42" # betrifft nur diese Funktion
echo "In Funktion: $ergebnis"
}
berechne
echo "Außen: $ergebnis" # gibt "leer" aus, nicht "42"
Rückgabewerte: return setzt nur den Exit-Status (0–255, wie bei Befehlen), keinen String. Um einen Wert zurückzugeben, nutze echo und fange die Ausgabe mit Befehlssubstitution auf:
#!/bin/bash
# Wert per echo zurückgeben:
addiere() {
local summe=$(( $1 + $2 ))
echo "$summe"
}
ergebnis=$(addiere 17 25)
echo "17 + 25 = $ergebnis"
# Exit-Status per return:
ist_gerade() {
if (( $1 % 2 == 0 )); then
return 0 # 0 = wahr (Erfolg)
else
return 1 # 1 = falsch (Fehler)
fi
}
if ist_gerade 8; then
echo "8 ist gerade."
fi
Exit-Status und Fehlerbehandlung
Jeder Befehl in Bash gibt nach seiner Ausführung einen Exit-Status zurück. Null (0) bedeutet Erfolg, alles andere signalisiert einen Fehler. Den letzten Exit-Status findest du in $?:
$ ls /home/gunter
Dokumente Downloads Musik
$ echo $?
0
$ ls /nicht-vorhanden
ls: Zugriff auf '/nicht-vorhanden' nicht möglich: Kein solches Verzeichnis
$ echo $?
2
Mit && und || kannst du Befehle abhängig vom Exit-Status verketten:
# Zweiter Befehl nur wenn erster erfolgreich (Exit 0):
mkdir /tmp/backup && echo "Verzeichnis angelegt."
# Zweiter Befehl nur wenn erster fehlschlägt (Exit != 0):
mkdir /tmp/backup || echo "Konnte Verzeichnis nicht anlegen!"
# Kombiniert — typisches Muster:
cp datei.txt /backup/ && echo "OK" || echo "Fehler beim Kopieren"
Für robustere Scripts gibt es drei wichtige Shell-Optionen:
#!/bin/bash
set -e # Script sofort beenden, wenn ein Befehl fehlschlägt
set -u # Fehler bei Verwendung undefinierter Variablen
set -o pipefail # Fehler in Pipes nicht verschlucken
# Oft zusammen als:
set -euo pipefail
Mit trap kannst du bestimmte Aktionen ausführen, wenn das Script beendet wird — zum Beispiel zum Aufräumen temporärer Dateien:
#!/bin/bash
set -euo pipefail
tmp_datei=$(mktemp)
# Beim Beenden immer aufräumen (auch bei Fehler oder Strg+C):
trap "rm -f '$tmp_datei'" EXIT
echo "Temporäre Datei: $tmp_datei"
echo "Daten..." > "$tmp_datei"
# Hier passiert die eigentliche Arbeit
cat "$tmp_datei"
# Am Ende: trap löscht $tmp_datei automatisch
set -e bricht das Script ab, sobald ein Befehl einen Exit-Status ungleich 0 zurückgibt — auch in if-Bedingungen, was manchmal unerwartet ist. Wenn ein Befehl absichtlich fehlschlagen darf, hänge || true an: befehl || true. Damit bekommt die Zeile immer Exit-Status 0.Praxisbeispiel: Ordner automatisch aufräumen
Das folgende Script verschiebt alle Dateien, die älter als eine bestimmte Anzahl von Tagen sind, in ein Archiv-Unterverzeichnis und schreibt dabei ein Log. Es zeigt, wie die Konzepte aus diesem Artikel zusammenspielen.
#!/bin/bash
set -euo pipefail
# ---- Konfiguration ----------------------------------------
QUELL_ORDNER="${1:-/home/gunter/Downloads}" # Standard falls kein Argument
ARCHIV_ORDNER="$QUELL_ORDNER/archiv"
MAX_ALTER_TAGE="${2:-30}" # Dateien älter als X Tage
LOG_DATEI="$QUELL_ORDNER/aufraeum.log"
# -----------------------------------------------------------
log() {
local meldung="$1"
local zeitstempel
zeitstempel=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$zeitstempel] $meldung" | tee -a "$LOG_DATEI"
}
pruefe_voraussetzungen() {
if [[ ! -d "$QUELL_ORDNER" ]]; then
echo "Fehler: Verzeichnis nicht gefunden: $QUELL_ORDNER" >&2
exit 1
fi
mkdir -p "$ARCHIV_ORDNER"
}
raeume_auf() {
local verschoben=0
local uebersprungen=0
log "=== Aufräumen gestartet (Quelle: $QUELL_ORDNER, Alter: ${MAX_ALTER_TAGE}d) ==="
while IFS= read -r -d '' datei; do
dateiname=$(basename "$datei")
# Archiv selbst und Log-Datei überspringen:
if [[ "$datei" == "$ARCHIV_ORDNER"* ]] || [[ "$datei" == "$LOG_DATEI" ]]; then
((uebersprungen++))
continue
fi
mv "$datei" "$ARCHIV_ORDNER/$dateiname"
log "Verschoben: $dateiname"
((verschoben++))
done < <(find "$QUELL_ORDNER" -maxdepth 1 -type f -mtime +"$MAX_ALTER_TAGE" -print0)
log "=== Fertig: $verschoben Datei(en) verschoben, $uebersprungen übersprungen ==="
}
# ---- Hauptprogramm ----------------------------------------
pruefe_voraussetzungen
raeume_auf
Aufruf mit Standardwerten (Downloads, 30 Tage) oder eigenen Werten:
# Standard: Downloads-Ordner, Dateien älter als 30 Tage:
$ ./aufraeumen.sh
# Eigene Parameter: Dokumente-Ordner, Dateien älter als 60 Tage:
$ ./aufraeumen.sh /home/gunter/Dokumente 60
# Log anschauen:
$ tail -20 /home/gunter/Downloads/aufraeum.log
[2026-04-04 10:45:00] === Aufräumen gestartet (Quelle: /home/gunter/Downloads, Alter: 30d) ===
[2026-04-04 10:45:01] Verschoben: ubuntu-24.04.iso
[2026-04-04 10:45:01] Verschoben: rechnung-februar.pdf
[2026-04-04 10:45:01] === Fertig: 2 Datei(en) verschoben, 0 übersprungen ===
Das Script nutzt: Funktionen (log, pruefe_voraussetzungen, raeume_auf), Standardwerte für Argumente (${1:-/pfad}), find mit -print0 und while read -d '' für sichere Dateinamen mit Leerzeichen, Fehlerbehandlung per set -euo pipefail, Logging mit Zeitstempel und tee. Das ist solider Produktionscode, kein Lern-Spielzeug.
crontab -e öffnet den Cron-Editor. Ein Eintrag wie 0 2 * * 0 /home/gunter/.local/bin/aufraeumen.sh startet das Script jeden Sonntag um 2 Uhr nachts — ohne dein Zutun.Weiterlernen
Du hast jetzt alle grundlegenden Bausteine für eigene Bash-Scripts: Variablen, Eingaben, Bedingungen, Schleifen, Funktionen, Fehlerbehandlung. Das reicht für die meisten Automatisierungsaufgaben im Alltag.
Drei Themen, die direkt an diesen Artikel anknüpfen:
- Komfortfunktionen wie Redirect, Pipes und Wildcards helfen dir, Scripts kompakter und mächtiger zu schreiben: Komfortfunktionen in der Bash
- Die Befehle, die du in deinen Scripts brauchst —
find,grep,cut,sort— sind auf der Kommando-Referenz zusammengefasst: Grundlegende Kommandos - Welchen Editor du für deine Scripts nutzt, ist Geschmackssache — eine Übersicht der gängigen Terminal-Editoren findest du hier: Editoren in der Shell