Zum Hauptinhalt springen
S-EDV news
← Alle Anleitungen
📘 Anleitung Linux 02.06.2026 · 13 min Lesezeit

Bash-Skripting für Admins – die Grundlagen

Bash-Skripting ist das Schweizer Taschenmesser jedes Linux-Admins. Diese Anleitung zeigt die Grundlagen: Shebang, Variablen und Quoting, Positionsparameter, Bedingungen, Schleifen, Funktionen, Exit-Codes und den robusten Strict Mode – plus zwei Mini-Skripte für Backup und Healthcheck.

Infografik zu Bash-Skripting für Admins mit Terminal, Shell-Befehlen, Variablen, Bedingungen, Schleifen und der Automatisierung typischer Administrationsaufgaben.

Wer Linux-Server betreut, kommt an Bash-Skripting nicht vorbei: wiederkehrende Aufgaben automatisieren, Backups wegschreiben, Dienste prüfen, alles per Cron oder systemd-Timer planen. Diese Anleitung bringt dir die Grundlagen praxisnah bei – Shebang, Variablen und Quoting, Positionsparameter, Bedingungen, Schleifen, Funktionen, Exit-Codes und den robusten set -euo pipefail. Am Ende baust du zwei kleine, aber produktionstaugliche Skripte: einen Backup-Wrapper und einen Healthcheck. Alle Beispiele laufen auf Debian 12, Ubuntu 24.04 LTS und in Docker-Containern und sind copy-paste-fertig.

Kurzfassung: Erste Zeile #!/usr/bin/env bash, danach set -euo pipefail für robuste Skripte. Variablen immer in doppelte Anführungszeichen setzen ("$var"), zum Iterieren "$@" statt $*. Bedingungen mit [[ ... ]] und Dateitests wie -f, -d, -x. Exit-Code des letzten Befehls steht in $?; Fehler nach stderr mit >&2 und passendem exit. Command Substitution mit $(...). Für Cron/systemd: absolute Pfade, eigenes Logging, Temp-Dateien mit mktemp plus trap ... EXIT, Überlappung per flock verhindern. Skripte vor dem Ausrollen mit shellcheck prüfen.

Voraussetzungen

Für diese Anleitung brauchst du nur wenig Vorbereitung:

  1. Ein Linux-System mit Bash: Debian 12, Ubuntu 24.04 LTS oder ein Container. Bash ist dort vorinstalliert; prüfe die Version mit bash --version.
  2. Shell-Zugang und ein Texteditor: z. B. nano oder vim. Skripte legst du als .sh-Datei an und machst sie mit chmod +x ausführbar.
  3. shellcheck (empfohlen): der statische Linter für Shell-Skripte deckt einen Großteil typischer Fehler ab. Installation: sudo apt install shellcheck.
  4. Grundkenntnisse der Shell: du solltest dich auf der Kommandozeile bewegen können (Verzeichnisse, Dateien, Pipes).

Wichtig für Debian/Ubuntu: /bin/sh zeigt dort auf dash, nicht auf Bash. Sobald du Bash-Features wie Arrays, [[ ... ]], local oder == nutzt, musst du Bash als Interpreter erzwingen – siehe Schritt 1.

Schritt 1: Shebang setzen und das Skript ausführbar machen

Die allererste Zeile eines Skripts, der Shebang, bestimmt den Interpreter. Für portable Bash-Skripte ist #!/usr/bin/env bash die beste Wahl: Damit wird bash über den PATH gesucht, was auf Systemen wie macOS/Homebrew, FreeBSD oder NixOS zuverlässiger ist als der feste Pfad #!/bin/bash.

#!/usr/bin/env bash
# Shebang in Zeile 1: portable bash-Suche ueber PATH
echo "Hallo von Bash"

Speichere das als hallo.sh, mach es ausführbar und starte es:

chmod +x hallo.sh
./hallo.sh
Nimm niemals #!/bin/sh, wenn du Bash-Features brauchst. Auf Debian/Ubuntu ist /bin/sh gleich dash, und bash-only Konstrukte wie Arrays, [[ ... ]], local oder == schlagen dann fehl.

Schritt 2: Robusten Strict Mode aktivieren

Direkt nach dem Shebang gehört in jedes ernsthafte Skript der Strict Mode. Drei Optionen machen Skripte deutlich robuster:

#!/usr/bin/env bash
set -euo pipefail
# -e      : Abbruch, sobald ein Befehl mit Exit-Code ungleich 0 endet
# -u      : Abbruch bei Referenz auf eine undefinierte Variable
# pipefail: eine Pipeline liefert den Fehlercode, sobald IRGENDEIN Glied scheitert

OptionLangformWirkung

-e

errexit

Skript bricht sofort ab, wenn ein Befehl einen Exit-Code ungleich 0 liefert.

-u

nounset

Zugriff auf eine nicht gesetzte Variable führt zum Abbruch (deckt Tippfehler auf).

-o pipefail

pipefail

Eine Pipeline schlägt fehl, sobald irgendein Glied fehlschlägt – nicht nur das letzte.

Optional kannst du zusätzlich den Internal Field Separator härten. IFS=$'\n\t' verhindert Word-Splitting an einfachen Leerzeichen und macht die Verarbeitung von Dateinamen mit Leerzeichen sicherer:

IFS=$'\n\t'  # optional: sicheres Field-Separator, verhindert Word-Splitting an Leerzeichen

Vorsicht, kein Allheilmittel: set -e hat Tücken. Befehle in if- oder while-Bedingungen, mit || verkettet oder unter bestimmten Umständen in Funktionen lösen keinen Abbruch aus. Setze set -e also nicht blind ein, sondern kenne die Nebenwirkungen. Und set -u bricht bei optionalen Variablen ab – dafür gibt es eine saubere Lösung im nächsten Schritt.

Schritt 3: Variablen und Quoting beherrschen

Das häufigste Fehlerfeld in Shell-Skripten ist falsches Quoting. Merke dir die zwei Anführungszeichen-Arten:

  1. Doppelte Anführungszeichen ("...") erlauben Variablen- und Dollar-Expansion und bewahren Leerzeichen. Das ist fast immer das, was du willst.
  2. Einfache Anführungszeichen ('...') nehmen den Inhalt komplett literal – keine Expansion, kein $.
name="Welt"
echo "Hallo $name"   # Ausgabe: Hallo Welt   (Expansion in doppelten Anfuehrungszeichen)
echo 'Hallo $name'   # Ausgabe: Hallo $name   (literal in einfachen Anfuehrungszeichen)

Die goldene Regel: Setze Variablen im Zweifel immer in doppelte Anführungszeichen. Unquoted Variablen werden Word-Splitting und Globbing unterzogen – ein Pfad mit Leerzeichen oder ein * im Wert führt sonst zu schwer auffindbaren Fehlern.

datei="mein bericht.txt"
rm "$datei"   # richtig: eine Datei "mein bericht.txt"
rm $datei     # falsch: versucht 'mein' und 'bericht.txt' zu loeschen

Für optionale Variablen umgehst du den set -u-Abbruch mit der Default-Wert-Expansion über Doppelpunkt-Minus:

ziel="${1:-/tmp}"   # nimmt /tmp, wenn $1 leer oder nicht gesetzt ist
echo "Ziel: $ziel"

Schritt 4: Positionsparameter und Sondervariablen nutzen

Argumente, die dein Skript beim Aufruf bekommt, landen in den Positionsparametern. Die wichtigsten:

ParameterBedeutung

$0

Name des Skripts

$1$9

erstes bis neuntes Argument (ab dem 10. mit geschweiften Klammern: ${10})

$#

Anzahl der Argumente (ohne $0)

"$@"

alle Argumente, jedes als eigenes, korrekt gequotetes Wort

"$*"

alle Argumente zu EINEM Wort verbunden, getrennt durch das erste Zeichen von IFS

Zum Iterieren über Argumente nimmst du immer "$@" in doppelten Anführungszeichen. Nur so bleiben Argumentgrenzen bei Leerzeichen erhalten:

for f in "$@"; do
  echo "Argument: $f"
done
Verwendest du $* statt "$@", zerstörst du die Argumentgrenzen, sobald ein Argument ein Leerzeichen enthält. Zum Iterieren ist "$@" fast immer richtig.

Daneben gibt es nützliche Sondervariablen:

VariableInhalt

$?

Exit-Code des zuletzt ausgeführten Befehls (0 = Erfolg, ungleich 0 = Fehler)

$$

PID der aktuellen Shell

$!

PID des letzten Hintergrundprozesses

Schritt 5: Bedingungen, Dateitests und Schleifen schreiben

Für Bedingungen nutzt du in Bash-Skripten am besten das Keyword [[ ... ]]. Es ist kein externes Kommando wie [ bzw. test, macht kein Word-Splitting oder Globbing auf seinen Argumenten und ist damit sicherer bei Leerzeichen. Es unterstützt && und ||, == mit Glob-Mustern sowie =~ für Regex. (Für reine POSIX-Portabilität wäre [/test nötig – in Bash-Skripten ist [[ ... ]] die Empfehlung.)

Die wichtigsten Datei- und String-Tests:

Testwahr, wenn …

-e PFAD

Pfad existiert

-f PFAD

reguläre Datei

-d PFAD

Verzeichnis

-r / -w / -x

lesbar / schreibbar / ausführbar

-s PFAD

Datei nicht leer (Größe größer 0)

-L PFAD

Symlink

-z STR / -n STR

String leer / String nicht leer

Zahlen vergleichst du mit -eq -ne -lt -le -gt -ge, Strings mit = bzw. == und !=. Ein paar typische Bedingungen:

if [[ -d "$DIR" ]]; then
  echo "Verzeichnis existiert: $DIR"
fi

if [[ ! -f "$KONFIG" ]]; then
  echo "Konfig fehlt: $KONFIG" >&2
  exit 1
fi

if [[ "$anzahl" -gt 10 ]]; then
  echo "mehr als 10"
fi

Die zwei wichtigsten Schleifen sind for und while. Eine for-Schleife läuft über eine Liste, eine while-Schleife liest etwa eine Datei zeilenweise. Beim zeilenweisen Lesen ist IFS= read -r line Pflicht: -r verhindert die Interpretation von Backslashes, IFS= das Trimmen von Leerzeichen am Zeilenrand.

# for-Schleife ueber eine feste Liste
for dienst in nginx mariadb cron; do
  echo "pruefe $dienst"
done

# while-Schleife: Datei zeilenweise lesen (sicher mit -r)
while IFS= read -r line; do
  echo "Zeile: $line"
done < datei.txt

Schritt 6: Funktionen, Exit-Codes und Fehlerprüfung

Funktionen bündeln wiederkehrende Logik. Variablen innerhalb einer Funktion machst du mit local lokal, damit sie nicht versehentlich globalen Zustand überschreiben. Eine kleine Logging-Funktion ist in fast jedem Skript nützlich:

log() {
  printf '%s [%s] %s\n' "$(date '+%F %T')" "$1" "$2"
}
# Aufruf: log INFO "Backup gestartet"

backup_dir() {
  local src="$1" dst="$2"
  tar -czf "$dst" -C "$src" .
}
# Aufruf: backup_dir /etc /tmp/etc.tar.gz

Jeder Befehl liefert beim Beenden einen Exit-Code: 0 für Erfolg, alles andere für einen Fehler. Den Code des letzten Befehls findest du in $?. So wertest du ihn gezielt aus:

cmd
rc=$?
if [[ $rc -ne 0 ]]; then
  echo "Fehler rc=$rc" >&2
  exit "$rc"
fi

Eine einfache Argument- und Fehlerprüfung am Anfang des Skripts erspart später viel Ärger. Schreibe Fehlermeldungen nach stderr (>&2) und beende mit einem Exit-Code ungleich 0:

if [[ $# -lt 1 ]]; then
  echo "Usage: $0 <ziel>" >&2
  exit 1
fi

DIR="$1"
if [[ ! -d "$DIR" ]]; then
  echo "Verzeichnis fehlt: $DIR" >&2
  exit 1
fi

Für Ausgaben und Werte aus anderen Befehlen nutzt du die Command Substitution mit $(...). Sie ist gegenüber den alten Backticks zu bevorzugen – verschachtelbar und besser lesbar. Das Ergebnis setzt du immer in doppelte Anführungszeichen:

out="$(date +%F)"            # Command Substitution mit Dollar-Klammern statt Backticks
echo "Datum heute: $out"
anzahl="$(ls -1 "$DIR" | wc -l)"

Schritt 7: Mini-Skript 1 – Backup-Wrapper

Jetzt setzt du das Gelernte zu einem ersten praxistauglichen Skript zusammen: einem Backup-Wrapper, der ein Verzeichnis in ein komprimiertes Archiv packt. Es nutzt Strict Mode, Argumentprüfung, Logging, eine sichere Temp-Datei mit mktemp und garantiertes Aufräumen per trap ... EXIT sowie absolute Pfade – damit ist es Cron- und systemd-tauglich.

#!/usr/bin/env bash
set -euo pipefail

# --- Konfiguration (absolute Pfade fuer Cron/systemd) ---
BACKUP_ROOT="/var/backups/myapp"
LOGFILE="/var/log/backup.log"

log() {
  printf '%s [%s] %s\n' "$(date '+%F %T')" "$1" "$2" >> "$LOGFILE"
}

# --- Argumentpruefung ---
if [[ $# -lt 1 ]]; then
  echo "Usage: $0 <quell-verzeichnis>" >&2
  exit 1
fi

SRC="$1"
if [[ ! -d "$SRC" ]]; then
  echo "Quelle ist kein Verzeichnis: $SRC" >&2
  exit 1
fi

# --- sichere Temp-Datei plus garantierte Aufraeumung ---
TMPFILE="$(mktemp)"
trap 'rm -f "$TMPFILE"' EXIT

# --- Backup erstellen ---
mkdir -p "$BACKUP_ROOT"
STAMP="$(date +%F_%H%M%S)"
DST="$BACKUP_ROOT/backup_$STAMP.tar.gz"

log INFO "Starte Backup von $SRC nach $DST"
if tar -czf "$TMPFILE" -C "$SRC" .; then
  mv "$TMPFILE" "$DST"
  trap - EXIT          # Erfolg: kein Cleanup mehr noetig
  log INFO "Backup erfolgreich: $DST"
else
  rc=$?
  log ERROR "tar fehlgeschlagen rc=$rc"
  exit "$rc"
fi

Das Muster mktemp plus trap 'rm -f "$TMPFILE"' EXIT ist wichtig: Der EXIT-Trap feuert auch bei Ctrl-C oder SIGTERM und räumt die Temp-Datei zuverlässig weg. Einzige Ausnahme ist kill -9 (SIGKILL), das nicht abfangbar ist. Lege Temp-Dateien nie mit festen, vorhersagbaren Namen an (Race- und Symlink-Risiko) – immer mktemp.

Schritt 8: Mini-Skript 2 – Healthcheck

Das zweite Skript ist ein Healthcheck, der mehrere Dienste prüft und mit einem passenden Exit-Code endet – ideal für Monitoring oder als systemd-Service. Es nutzt eine for-Schleife über die Dienste, $?-Auswertung und gibt einen Gesamt-Exit-Code zurück.

#!/usr/bin/env bash
set -euo pipefail

LOGFILE="/var/log/healthcheck.log"
DIENSTE=("nginx" "mariadb" "cron")

log() {
  printf '%s [%s] %s\n' "$(date '+%F %T')" "$1" "$2" | tee -a "$LOGFILE"
}

fehler=0
for dienst in "${DIENSTE[@]}"; do
  if systemctl is-active --quiet "$dienst"; then
    log OK "$dienst laeuft"
  else
    log FAIL "$dienst laeuft NICHT"
    fehler=$((fehler + 1))
  fi
done

if [[ $fehler -gt 0 ]]; then
  log ERROR "$fehler Dienst(e) nicht aktiv"
  exit 1
fi

log INFO "Alle Dienste aktiv"
exit 0

Damit nie zwei Läufe gleichzeitig anlaufen (etwa wenn ein Lauf länger dauert als das Intervall), sicherst du das Skript mit flock ab. Setze diese zwei Zeilen direkt nach dem Strict Mode ein:

exec 200>/run/lock/healthcheck.lock
flock -n 200 || { echo 'laeuft bereits' >&2; exit 1; }

Bevor du eines dieser Skripte ausrollst, jagst du es einmal durch shellcheck. Der Linter findet Quoting-Fehler, fragwürdige Konstrukte und vieles mehr:

shellcheck ./backup.sh ./healthcheck.sh

Schritt 9: Skripte Cron- und systemd-tauglich machen

Skripte, die manuell laufen, scheitern oft in Cron – der Klassiker unter den Stolperfallen. Der Grund: Cron läuft in einer minimalen Umgebung. Der PATH ist nicht dein interaktiver PATH, und relative Pfade laufen ins Leere. Die Lösung: absolute Pfade für Binaries und Dateien verwenden und im Crontab den Kopf setzen.

SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
MAILTO="admin@example.com"

# Backup taeglich um 02:00 Uhr, stdout+stderr ins Logfile
0 2 * * * /usr/local/bin/backup.sh /etc >> /var/log/backup.log 2>&1

Wichtig beim Logging: Cron schickt Ausgaben standardmäßig per Mail an den lokalen Mailspool, der selten gelesen wird. Leite deshalb stdout und stderr mit >> (anhängen) und 2>&1 in ein Logfile um. Ein MAILTO oben sorgt zusätzlich dafür, dass echte Fehlerausgaben dich erreichen.

Das Prozent-Zeichen hat in Crontab-Kommandos Sonderbedeutung (es wirkt als Zeilenumbruch bzw. stdin). Datumsformate wie %F musst du im Crontab mit einem Backslash escapen (\%F) – oder du kapselst sie sauber im Skript, wie in unseren Beispielen.

Als moderne Alternative bietet sich ein systemd-Timer an. Er braucht immer zwei Units: eine .service-Unit mit der Aufgabe und eine .timer-Unit mit dem Zeitplan. Vorteile gegenüber Cron sind zentrales Logging über journalctl und saubere Abhängigkeiten.

# /etc/systemd/system/backup.service
[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh /etc
# /etc/systemd/system/backup.timer
[Timer]
OnCalendar=Mon..Fri 22:30
Persistent=true

[Install]
WantedBy=timers.target

Aktiviert wird der Timer mit einem Befehl:

sudo systemctl enable --now backup.timer

OnCalendar definiert den Zeitpunkt, Persistent=true holt verpasste Läufe nach (etwa nach einem Reboot oder Standby) und Type=oneshot markiert eine einmalige Aufgabe. Lässt du Persistent=true weg, werden verpasste Läufe nicht nachgeholt – für Backups meist unerwünscht.

Troubleshooting

  1. Skript läuft im Terminal, scheitert in Cron: fast immer relative Pfade oder die Annahme des interaktiven PATH. Verwende absolute Pfade und setze SHELL/PATH im Crontab.
  2. unbound variable-Abbruch durch set -u: eine optionale Variable ist nicht gesetzt. Nutze die Default-Wert-Expansion "${VAR:-default}". Die Parameter $@ und $* sind von -u ausgenommen.
  3. set -e bricht nicht ab, wo du es erwartest: Befehle in if/while-Bedingungen, mit || verkettet oder teils in Funktionen lösen keinen Abbruch aus. Prüfe in solchen Fällen den Exit-Code explizit über $?.
  4. Dateinamen mit Leerzeichen zerreißen: du hast eine Variable ohne doppelte Anführungszeichen verwendet oder $* statt "$@". Quote konsequent und iteriere mit "$@".
  5. Müll-Dateien in /tmp: mktemp ohne Cleanup-Trap. Ergänze trap 'rm -f "$TMPFILE"' EXIT direkt nach dem mktemp.
  6. Cleanup läuft nicht in einer Subshell/Pipeline: der Trap im Hauptskript greift dort nicht automatisch. Ressourcen-erzeugende Subshells brauchen einen eigenen Trap. kill -9 (SIGKILL) ist generell nicht abfangbar.
  7. systemd-Timer holt verpasste Läufe nicht nach: es fehlt Persistent=true in der .timer-Unit. Ergänzen und mit systemctl daemon-reload sowie erneutem Aktivieren übernehmen.
  8. Bash-Feature schlägt fehl: wahrscheinlich läuft das Skript unter dash, weil der Shebang fehlt oder #!/bin/sh lautet. Setze #!/usr/bin/env bash.

Häufige Fragen

Warum #!/usr/bin/env bash statt #!/bin/bash?

Mit #!/usr/bin/env bash wird bash über den PATH gesucht, das ist portabler – etwa auf macOS/Homebrew, FreeBSD oder NixOS, wo Bash nicht zwingend unter /bin/bash liegt. Auf reinen Debian/Ubuntu-Servern funktionieren beide; env ist die robustere Standardwahl.

Was genau bewirkt set -euo pipefail?

-e bricht das Skript beim ersten Befehl mit Exit-Code ungleich 0 ab, -u beim Zugriff auf eine undefinierte Variable, und pipefail lässt eine Pipeline schon fehlschlagen, wenn irgendein Glied scheitert – nicht erst das letzte. Zusammen machen sie Skripte deutlich robuster für den Produktivbetrieb.

Wann doppelte und wann einfache Anführungszeichen?

Doppelte Anführungszeichen ("..."), wenn Variablen expandiert werden sollen und Leerzeichen erhalten bleiben müssen – das ist der Normalfall. Einfache Anführungszeichen ('...'), wenn der Inhalt völlig literal bleiben soll, ohne jede Expansion.

Was ist der Unterschied zwischen "$@" und "$*"?

"$@" expandiert jedes Argument als eigenes, korrekt gequotetes Wort – ideal zum Iterieren. "$*" verbindet alle Argumente zu einem einzigen Wort, getrennt durch das erste Zeichen von IFS. Bei Argumenten mit Leerzeichen brauchst du fast immer "$@".

Brauche ich [[ ... ]] oder reicht [ ... ]?

[[ ... ]] ist ein Bash-Keyword: kein Word-Splitting oder Globbing auf den Argumenten, sicherer bei Leerzeichen, und es kann &&, ||, Glob-Muster mit == sowie Regex mit =~. [ bzw. test ist POSIX-portabel. Für reine Bash-Skripte ist [[ ... ]] die Empfehlung.

Wie prüfe ich, ob ein Befehl erfolgreich war?

Über seinen Exit-Code in $?: 0 bedeutet Erfolg, alles andere einen Fehler. Du kannst direkt if befehl; then ... schreiben oder den Code in eine Variable sichern (rc=$?) und auswerten.

Fazit

Mit diesen Grundlagen schreibst du Bash-Skripte, die nicht nur im Terminal funktionieren, sondern auch unter Cron und systemd zuverlässig laufen. Die Kernpunkte: Shebang #!/usr/bin/env bash, robuster set -euo pipefail, konsequentes Quoting mit doppelten Anführungszeichen, Iterieren über "$@", saubere Bedingungen mit [[ ... ]] und Dateitests, Funktionen mit local, Exit-Codes über $? und Command Substitution mit $(...). Für den Produktivbetrieb kommen absolute Pfade, eigenes Logging, sichere Temp-Dateien mit mktemp plus trap ... EXIT und flock gegen Überlappung hinzu. Lass jedes Skript einmal durch shellcheck laufen, dann hast du die häufigsten Fehlerklassen schon abgefangen. Die beiden Mini-Skripte – Backup-Wrapper und Healthcheck – sind eine solide Vorlage für deine eigenen Automatisierungen.

Weiterführende Anleitungen und Quellen

  1. Cron und Crontab: zeitgesteuerte Aufgaben unter Linux planen – so legst du deine Skripte als wiederkehrende Cron-Jobs an.
  2. systemd-Service erstellen und verwalten – die moderne Alternative zu Cron mit Service- und Timer-Units.
  3. Logs lesen mit journalctl und /var/log – die Ausgaben deiner Skripte und Timer auswerten.
  4. Alle Linux-Anleitungen in der Kategorie Linux – weitere Themen rund um Server und Shell.

Quellen: GNU Bash Reference Manual – Special Parameters, Use Bash Strict Mode (Unofficial Bash Strict Mode), redsymbol.net, Bash Best Practices: Writing Safer, Cleaner Scripts, Linuxize und systemd/Timers, ArchWiki.