Shell-Skripting Grundlagen – Bash-Skripte schreiben und automatisieren

Bash-Skripting von Grund auf lernen: Variablen, Konditionen, Schleifen, Funktionen, Fehlerbehandlung und Best Practices – mit praxisnahen Beispielen für Automatisierungsaufgaben.

13 min Lesezeit

Shell-Skripting Grundlagen – Bash-Skripte schreiben und automatisieren

Bash-Skripte transformieren manuelle, repetitive Aufgaben in automatisierte Prozesse. Was heute 30 Minuten dauert, läuft morgen in 30 Sekunden – zuverlässig, jedes Mal gleich. Dieser Guide führt dich durch die Grundlagen: von deinem ersten Skript bis zu sauberer Fehlerbehandlung.


Das erste Skript

# Skript erstellen
nano hallo.sh
#!/bin/bash
# Shebang: Sagt dem System, dass bash als Interpreter verwendet werden soll
# Kommentare beginnen mit #

echo "Hallo, Welt!"
echo "Heutiges Datum: $(date +%Y-%m-%d)"
echo "Hostname: $(hostname)"
# Ausführbar machen
chmod +x hallo.sh

# Ausführen
./hallo.sh
# Hallo, Welt!
# Heutiges Datum: 2026-02-18
# Hostname: mein-server

Shebang-Varianten

#!/bin/bash          # Bash (am häufigsten, spezifisch)
#!/usr/bin/env bash  # Bash über PATH (portabler)
#!/bin/sh            # POSIX-Shell (kompatibler, weniger Features)

Skript debuggen

# Skript mit Debugging ausführen
bash -x hallo.sh     # zeigt jeden Befehl vor der Ausführung

# Im Skript selbst:
set -x               # Debugging einschalten
set +x               # Debugging ausschalten

# Syntax prüfen (ohne Ausführen)
bash -n hallo.sh     # Keine Ausgabe = kein Syntaxfehler

Variablen und Datentypen

#!/bin/bash

# Variable definieren (KEIN Leerzeichen um =)
NAME="Andre"
ALTER=30
PI=3.14159

# Variable verwenden (mit $)
echo "Name: $NAME"
echo "Alter: $ALTER"
echo "Pi: $PI"

# Geschützte Variablen (geschweifte Klammern)
DATEI="backup"
echo "${DATEI}_2026.tar.gz"   # backup_2026.tar.gz (ohne {} wäre $DATEI_ nicht klar)

# Konstanten (readonly)
readonly MAX_VERSUCHE=3
MAX_VERSUCHE=5   # Error: readonly variable

# Variable löschen
unset NAME
echo "$NAME"   # leer

Variablentypen

# Integer-Arithmetik
ZAHL=10
SUMME=$((ZAHL + 5))
PRODUKT=$((ZAHL * 3))
REST=$((ZAHL % 3))
echo "$SUMME"    # 15

# Float-Arithmetik: bc verwenden
ERGEBNIS=$(echo "scale=2; 22/7" | bc)
echo "$ERGEBNIS"  # 3.14

# Array
FARBEN=("rot" "grün" "blau")
echo "${FARBEN[0]}"          # rot
echo "${FARBEN[@]}"          # alle Elemente
echo "$"         # Anzahl: 3
FARBEN+=("gelb")             # Element hinzufügen

# Assoziatives Array (Bash 4+)
declare -A PERSON
PERSON["name"]="Andre"
PERSON["alter"]="30"
echo "${PERSON["name"]}"

Spezielle Variablen

$0      # Name des Skripts
$1, $2  # Argumente (1. und 2.)
$@      # Alle Argumente (als Liste)
$*      # Alle Argumente (als String)
$#      # Anzahl der Argumente
$?      # Exit-Code des letzten Befehls
$$      # PID des aktuellen Skripts
$!      # PID des letzten Hintergrundprozesses
$RANDOM # Zufallszahl (0-32767)
$LINENO # Aktuelle Zeilennummer

Variablen-Substitution

NAME="Andre"

# Standardwert wenn leer
echo "${NAME:-Max}"         # Andre (NAME ist gesetzt)
echo "${LEER:-Standardwert}"  # Standardwert (LEER ist leer)

# Standardwert setzen wenn leer
echo "${LEER:=Standardwert}"  # setzt LEER auf "Standardwert"

# Fehler wenn leer
echo "${PFLICHT:?'Variable PFLICHT muss gesetzt sein!'}"

# Präfix entfernen
PFAD="/var/log/nginx/error.log"
echo "${PFAD#/var/log/}"    # nginx/error.log (kürzester Präfix)
echo "${PFAD##*/}"          # error.log (längster Präfix bis letztem /)

# Suffix entfernen
echo "${PFAD%.log}"         # /var/log/nginx/error
echo "${PFAD%/*}"           # /var/log/nginx

# Ersetzen
echo "${PFAD/nginx/apache}" # /var/log/apache/error.log

Benutzer-Eingabe und Argumente

Argumente

#!/bin/bash
# Aufruf: ./skript.sh Arg1 Arg2

echo "Skript: $0"
echo "Argument 1: $1"
echo "Argument 2: $2"
echo "Alle Argumente: $@"
echo "Anzahl: $#"

# Argumente validieren
if [ $# -lt 2 ]; then
    echo "Fehler: Zu wenige Argumente"
    echo "Verwendung: $0 <name> <alter>"
    exit 1
fi

Interaktive Eingabe

# Einfache Eingabe
echo -n "Gib deinen Namen ein: "
read NAME
echo "Hallo, $NAME!"

# Mit Prompt
read -p "Name: " NAME

# Passwort (kein Echo)
read -s -p "Passwort: " PASSWORT
echo ""   # Zeilenumbruch nach Passwort

# Mit Timeout
read -t 10 -p "Eingabe (10s): " EINGABE || echo "Timeout!"

# Mehrere Variablen
read -p "Vorname Nachname: " VORNAME NACHNAME

Argumente mit getopts parsen

#!/bin/bash

# Verwendung: ./skript.sh -n NAME -a ALTER [-v]
VERBOSE=false

while getopts "n:a:v" opt; do
    case $opt in
        n) NAME="$OPTARG" ;;
        a) ALTER="$OPTARG" ;;
        v) VERBOSE=true ;;
        ?) echo "Ungültige Option: -$OPTARG"; exit 1 ;;
    esac
done

[ -z "$NAME" ] && { echo "Fehler: -n NAME erforderlich"; exit 1; }
echo "Name: $NAME, Alter: $ALTER"
$VERBOSE && echo "Verbose-Modus aktiv"

Konditionen: if, case

if-elif-else

#!/bin/bash

ZAHL=42

# Zahlenvergleiche
if [ "$ZAHL" -gt 50 ]; then
    echo "Größer als 50"
elif [ "$ZAHL" -eq 42 ]; then
    echo "Genau 42"
else
    echo "Kleiner als 50"
fi

# String-Vergleiche
NAME="Andre"
if [ "$NAME" = "Andre" ]; then
    echo "Hallo Andre!"
fi

if [ -z "$NAME" ]; then    # leer?
    echo "Name ist leer"
fi

if [ -n "$NAME" ]; then    # nicht leer?
    echo "Name ist: $NAME"
fi

# Modernes [[ für Strings (bash-spezifisch, robuster)
if [[ "$NAME" == "Andre" ]]; then
    echo "Name ist Andre"
fi

if [[ "$NAME" =~ ^A ]]; then   # Regex-Match
    echo "Name beginnt mit A"
fi

Datei- und Verzeichnis-Tests

# Datei-Tests
if [ -f "/etc/nginx/nginx.conf" ]; then
    echo "nginx.conf existiert und ist eine Datei"
fi

if [ -d "/var/www" ]; then    echo "Verzeichnis existiert"
fi

if [ -e "/pfad/datei" ]; then     echo "Existiert (Datei oder Verzeichnis)"
fi

if [ -r "/etc/passwd" ]; then     echo "Lesbar"
fi

if [ -w "/tmp/datei" ]; then
    echo "Schreibbar"
fi

if [ -x "/usr/bin/python3" ]; then
    echo "Ausführbar"
fi

if [ -s "/var/log/syslog" ]; then   echo "Nicht leer (Größe > 0)"
fi

# Logische Operatoren
if [ -f "datei.txt" ] && [ -r "datei.txt" ]; then
    echo "Datei existiert und ist lesbar"
fi

if [ -z "$VAR" ] || [ "$VAR" = "default" ]; then
    echo "Leer oder default"
fi

case

#!/bin/bash

AKTION="$1"

case "$AKTION" in
    start)
        echo "Service wird gestartet..."
        ;;
    stop)
        echo "Service wird gestoppt..."
        ;;
    restart|reload)
        echo "Service wird neu gestartet..."
        ;;
    status)
        echo "Service-Status:"
        ;;
    *)
        echo "Unbekannte Aktion: $AKTION"
        echo "Verwendung: $0 {start|stop|restart|reload|status}"
        exit 1
        ;;
esac

Schleifen: for, while, until

for-Schleifen

#!/bin/bash

# Über Liste iterieren
for FARBE in rot grün blau gelb; do
    echo "Farbe: $FARBE"
done

# Über Dateien
for DATEI in /etc/*.conf; do
    echo "Konfiguration: $DATEI"
    wc -l "$DATEI"
done

# Über Array
SERVERS=("web1" "web2" "db1" "cache1")
for SERVER in "${SERVERS[@]}"; do
    echo "Prüfe $SERVER..."
    ping -c1 -W1 "$SERVER" &>/dev/null && echo "OK" || echo "FEHLER"
done

# C-Stil for-Schleife
for ((i=1; i<=10; i++)); do
    echo "Iteration $i"
done

# Sequenz
for i in {1..5}; do
    echo "$i"
done

for i in {0..100..10}; do   # 0, 10, 20, ... 100
    echo "$i"
done

# Ausgabe eines Befehls
for DATEI in $(find /tmp -name "*.tmp" -mtime +1); do
    rm -v "$DATEI"
done

while-Schleife

#!/bin/bash

# Einfache while-Schleife
ZAEHLER=0
while [ "$ZAEHLER" -lt 5 ]; do
    echo "Zaehler: $ZAEHLER"
    ZAEHLER=$((ZAEHLER + 1))
done

# Datei Zeile für Zeile lesen
while IFS= read -r ZEILE; do
    echo "Zeile: $ZEILE"
done < /etc/hosts

# Mit Pipeline
find /var/log -name "*.log" | while IFS= read -r LOG; do
    echo "Log: $LOG ($(wc -l < "$LOG") Zeilen)"
done

# Endlosschleife mit break
while true; do
    if systemctl is-active --quiet nginx; then
        echo "nginx ist aktiv"
        break
    fi
    echo "Warte auf nginx..."
    sleep 5
done

# Retry-Logik
MAX=3
VERSUCHE=0
while [ "$VERSUCHE" -lt "$MAX" ]; do
    if curl -sf https://api.example.com/health; then
        echo "API erreichbar"
        break
    fi
    VERSUCHE=$((VERSUCHE + 1))
    echo "Versuch $VERSUCHE/$MAX fehlgeschlagen. Warte 5s..."
    sleep 5
done

Funktionen

#!/bin/bash

# Funktion definieren
sagHallo() {
    local NAME="$1"    # local: Variable nur in Funktion
    echo "Hallo, $NAME!"
}

# Alternativ (ältere Syntax)
function sagHallo {
    echo "Hallo!"
}

# Funktion aufrufen
sagHallo "Andre"

# Rückgabewert (Exit-Code, 0-255)
istGueltigeIP() {
    local IP="$1"
    if [[ "$IP" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
        return 0   # Erfolg
    else
        return 1   # Fehler
    fi
}

if istGueltigeIP "192.168.1.1"; then
    echo "Gültige IP"
else
    echo "Ungültige IP"
fi

# Rückgabewert als String (via echo + Command Substitution)
getHostname() {
    echo "$(hostname -f)"
}

FULL_HOSTNAME=$(getHostname)
echo "Hostname: $FULL_HOSTNAME"

Nützliche Standard-Funktionen

#!/bin/bash

# Logging-Funktion
log() {
    local LEVEL="$1"
    shift
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] [$LEVEL] $*" | tee -a /var/log/mein-skript.log
}

log "INFO" "Skript gestartet"
log "WARN" "Warnung aufgetreten"
log "ERROR" "Fehler aufgetreten"

# Fehler-Exit-Funktion
error_exit() {
    echo "Fehler: $1" >&2
    exit "${2:-1}"
}

# Verwendung:
[ -f "/etc/config.conf" ] || error_exit "/etc/config.conf nicht gefunden" 2

# Bestätigungsdialog
bestaetigen() {
    read -r -p "$1 [y/N] " ANTWORT
    case "$ANTWORT" in
        [yY][eE][sS]|[yY]) return 0 ;;
        *) return 1 ;;
    esac
}

if bestaetigen "Wirklich löschen?"; then
    rm -rf /tmp/test/
fi

Fehlerbehandlung und set-Optionen

Empfohlene set-Optionen

#!/bin/bash

# Am Anfang jedes Skripts (Best Practice):
set -euo pipefail

# set -e: Skript bei Fehler (Exit-Code != 0) sofort beenden
# set -u: Fehler bei undefinierten Variablen
# set -o pipefail: Pipe schlägt fehl wenn irgendein Befehl fehlschlägt
# Beispiel: Ohne set -e
rm /nicht-existent   # Fehler
echo "Hier geht es weiter"  # wird trotzdem ausgeführt!

# Mit set -e
set -e
rm /nicht-existent   # Fehler → Skript beendet sich hier
echo "Hier geht es NICHT mehr hin"

Trap: Aufräumen bei Fehler oder Abbruch

#!/bin/bash
set -euo pipefail

# Temp-Datei
TMPFILE=$(mktemp)

# Trap: wird bei Beendigung (auch bei Fehler) ausgeführt
cleanup() {
    echo "Räume auf..."
    rm -f "$TMPFILE"
}
trap cleanup EXIT

# Trap auf SIGINT (Ctrl+C) und SIGTERM
trap 'echo "Signal empfangen"; cleanup; exit 130' INT TERM

# Trap auf Fehler
trap 'echo "Fehler in Zeile $LINENO"; cleanup; exit 1' ERR

# Skript-Logik
echo "Arbeite mit $TMPFILE"
# ...

Exit-Codes verstehen

# Exit-Codes
exit 0    # Erfolg
exit 1    # Allgemeiner Fehler
exit 2    # Falsche Verwendung / falsche Argumente
exit 126  # Datei nicht ausführbar
exit 127  # Befehl nicht gefunden
exit 128  # Ungültiger Exit-Argument
exit 130  # Abgebrochen mit Ctrl+C (128+2)

# Letzten Exit-Code abfragen
echo $?

# Exit-Code testen
if ! befehl; then
    echo "befehl ist fehlgeschlagen mit $?"
fi

# Befehl erlaubt fehlzuschlagen (trotz set -e)
rm -f /tmp/nicht-existent || true   # Fehler ignorieren

Strings und Text verarbeiten

#!/bin/bash

# String-Länge
NAME="Andre"
echo "$"     # 5

# Substring
TEXT="Linux ist toll"
echo "${TEXT:6}"        # ist toll (ab Position 6)
echo "${TEXT:6:3}"      # ist (3 Zeichen ab Position 6)
echo "${TEXT: -4}"      # toll (von hinten)

# Groß-/Kleinschreibung
echo "${NAME,,}"    # klein: andre
echo "${NAME^^}"    # groß: ANDRE
echo "${NAME^}"     # erster Buchstabe groß: Andre

# Suchen und Ersetzen
PFAD="/var/log/nginx/error.log"
echo "${PFAD/nginx/apache}"    # erstes Vorkommen
echo "${PFAD//o/0}"            # alle Vorkommen

# split (IFS)
CSV="eins,zwei,drei"
IFS=',' read -ra TEILE <<< "$CSV"
for TEIL in "${TEILE[@]}"; do
    echo "$TEIL"
done

# trim (Leerzeichen entfernen)
TEXT="  Hallo Welt  "
echo "${TEXT#"${TEXT%%[! ]*}"}"  # vorne trimmen
echo "${TEXT%"${TEXT##*[! ]}"}"  # hinten trimmen
# Einfacher mit sed:
echo "$TEXT" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//'

Praktische Beispiele

Deployment-Skript

#!/bin/bash
set -euo pipefail

APP_DIR="/var/www/meine-app"
BACKUP_DIR="/var/backups/meine-app"
BRANCH="${1:-main}"

log() { echo "[$(date '+%H:%M:%S')] $*"; }
error_exit() { echo "FEHLER: $1" >&2; exit 1; }

log "Deployment von Branch: $BRANCH"

# Backup erstellen
mkdir -p "$BACKUP_DIR"
log "Backup erstellen..."
rsync -a --delete "$APP_DIR/" "$BACKUP_DIR/$(date +%Y%m%d_%H%M%S)/"

# Code aktualisieren
log "Code aktualisieren..."
cd "$APP_DIR"
git fetch origin
git checkout "$BRANCH"
git pull origin "$BRANCH"

# Abhängigkeiten
log "Abhängigkeiten installieren..."
composer install --no-dev --optimize-autoloader

# Migrationen
log "Datenbank-Migrationen..."
php artisan migrate --force

# Cache leeren
log "Cache leeren..."
php artisan cache:clear
php artisan config:cache
php artisan route:cache

# Service neu starten
log "php-fpm neu starten..."
sudo systemctl reload php8.3-fpm

log "Deployment abgeschlossen!"

System-Info-Skript

#!/bin/bash

# Farben
ROT='\033[0;31m'
GRUEN='\033[0;32m'
GELB='\033[1;33m'
NC='\033[0m'  # No Color

echo "=== System-Überblick ==="
echo "Hostname: $(hostname -f)"
echo "OS: $(lsb_release -ds 2>/dev/null || cat /etc/os-release | grep PRETTY_NAME | cut -d'"' -f2)"
echo "Kernel: $(uname -r)"
echo "Laufzeit: $(uptime -p)"
echo ""

# CPU-Auslastung
CPU=$(top -bn1 | grep "Cpu(s)" | awk '{print $2}' | cut -d. -f1)
echo -n "CPU-Last: ${CPU}% "
[ "$CPU" -gt 80 ] && echo -e "${ROT}(HOCH!)${NC}" || echo -e "${GRUEN}(OK)${NC}"

# RAM
RAM_TOTAL=$(free -m | awk '/Mem:/ {print $2}')
RAM_USED=$(free -m | awk '/Mem:/ {print $3}')
RAM_PCT=$((RAM_USED * 100 / RAM_TOTAL))
echo -n "RAM: ${RAM_USED}MB / ${RAM_TOTAL}MB (${RAM_PCT}%) "
[ "$RAM_PCT" -gt 90 ] && echo -e "${ROT}(HOCH!)${NC}" || echo -e "${GRUEN}(OK)${NC}"

# Disk
df -h / | awk 'NR==2 {
    used=$3; total=$2; pct=$5
    printf "Disk: %s / %s (%s)\n", used, total, pct
}'

echo ""
echo "=== Laufende Services ==="
for SERVICE in nginx php8.3-fpm mysql redis; do
    if systemctl is-active --quiet "$SERVICE"; then
        echo -e "${GRUEN}✓${NC} $SERVICE"
    else
        echo -e "${ROT}✗${NC} $SERVICE"
    fi
done

Best Practices

#!/bin/bash

# 1. Immer Shebang und set-Optionen
set -euo pipefail

# 2. Variablen in Anführungszeichen (Leerzeichen-sicher)
echo "$VARIABLE"          # richtig
echo $VARIABLE            # falsch bei Leerzeichen

# 3. Lokale Variablen in Funktionen
funktion() {
    local VAR="lokal"   # nicht global
}

# 4. Fehler-Checks bei wichtigen Operationen
mkdir -p /tmp/test || { echo "mkdir fehlgeschlagen"; exit 1; }

# 5. Sinnvolle Fehlermeldungen auf stderr
echo "Fehler: Datei nicht gefunden" >&2

# 6. Temporäre Dateien sicher erstellen
TMPFILE=$(mktemp)
trap "rm -f $TMPFILE" EXIT

# 7. Pfade nicht als Hart-Code (konfigurierbar)
BACKUP_DIR="${BACKUP_DIR:-/var/backups}"  # Standard, überschreibbar

# 8. Benutzerrechte prüfen
if [ "$EUID" -ne 0 ]; then
    echo "Fehler: Muss als root ausgeführt werden"
    exit 1
fi

# 9. Sicherheit: Eingaben prüfen
FILENAME="$1"
# Verhindere Verzeichnis-Traversal
if [[ "$FILENAME" =~ \.\. ]]; then
    echo "Ungültiger Dateiname" >&2
    exit 1
fi

# 10. Skripte mit shellcheck testen
# sudo apt install shellcheck
# shellcheck mein-skript.sh

Fazit

Shell-Skripting ist eine der produktivsten Fähigkeiten für jeden Linux-Nutzer. Mit den Grundlagen aus diesem Guide kannst du:

  • Repetitive Tasks automatisieren
  • Deployment-Prozesse standardisieren
  • System-Monitoring einrichten
  • Fehlerbehandlung implementieren

Die wichtigsten Punkte: set -euo pipefail immer setzen, Variablen in Anführungszeichen, trap für Aufräumen, und shellcheck zum Testen der Skripte.

War dieser Artikel hilfreich?