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.