Zum Inhalt springen

Shell-Scripting-Grundlagen für moderne Entwickler

Praktische Shell-Scripting-Muster zur Automatisierung von Entwicklungs-Workflows – von grundlegenden Bash-Konstrukten bis hin zu robusten Produktionsskripten.

4 Min. Lesezeit
Terminalfenster mit einem gut strukturierten Shell-Skript und Syntaxhervorhebung

Shell-Scripting befindet sich in einer unbequemen Zwischenposition. Es ist zu wichtig, um es zu ignorieren – jede Deployment-Pipeline, jedes Entwicklungssetup und jeder Automatisierungs-Workflow kommt damit in Berührung. Aber es steckt auch voller Fallstricke, die dazu führen, dass „einfache" Skripte in der Produktion unbemerkt kaputtgehen.

Der Unterschied zwischen einem Skript, das nur auf deinem Rechner funktioniert, und einem, das überall funktioniert, liegt in ein paar grundlegenden Praktiken, die die meisten Entwickler auslassen.

Jedes Skript richtig starten

Die ersten drei Zeilen eines Bash-Skripts entscheiden darüber, ob es laut fehlschlägt oder unbemerkt Daten beschädigt.

shbash
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'

Was jede Zeile bewirkt:

  • set -e — Beendet das Skript sofort bei einem fehlgeschlagenen Befehl (statt mit einem inkonsistenten Zustand weiterzumachen)
  • set -u — Behandelt nicht gesetzte Variablen als Fehler (statt sie stillschweigend zu einem leeren String zu expandieren)
  • set -o pipefail — Eine Pipeline schlägt fehl, wenn ein beliebiger Befehl darin fehlschlägt (statt nur den letzten zu prüfen)
  • IFS=$'\n\t' — Verhindert Wortaufteilung bei Leerzeichen in Dateinamen
shbash
# ❌ Without set -euo pipefail — silently uses wrong directory
cd /tmp/deploy
rm -rf *
# If cd fails, this runs rm -rf in whatever directory you're in
 
# ✅ With set -euo pipefail — stops immediately
set -euo pipefail
cd /tmp/deploy
rm -rf ./*
# If cd fails, the script stops. Crisis averted.

Genau dieses cd-Fehlerszenario hat schon echte Produktionsausfälle verursacht. Das set -e-Flag ist nicht optional – es ist der Sicherheitsgurt.

Variablen und Anführungszeichen

Nicht in Anführungszeichen gesetzte Variablen sind die häufigste Fehlerquelle in Shell-Skripten. Eine Variable mit Leerzeichen oder Globbing-Zeichen wird sich anders verhalten, als du erwartest.

shbash
# ❌ Unquoted variable — breaks on spaces and special chars
file_path=/tmp/my project/data.csv
cp $file_path /backup/
# Actually runs: cp /tmp/my project/data.csv /backup/
# Shell sees three arguments: /tmp/my, project/data.csv, /backup/
 
# ✅ Always quote variables
file_path="/tmp/my project/data.csv"
cp "$file_path" /backup/

Verwende "${variable}" für String-Interpolation und "$@", um Argumente an andere Befehle weiterzugeben:

shbash
# Pass all script arguments to another command
run_tests() {
  local test_dir="${1:-.}"
  local flags=("${@:2}")
 
  echo "Running tests in ${test_dir}..."
  pytest "$test_dir" "${flags[@]}"
}
 
run_tests "$@"

Das Schlüsselwort local begrenzt die Variable auf den Gültigkeitsbereich der Funktion. Ohne es ist jede Variable global – ein Rezept für versehentliches Überschreiben in längeren Skripten.

Funktionen und Fehlerbehandlung

Strukturiere Skripte mit Funktionen, einem zentralen main-Einstiegspunkt und expliziter Fehlerbehandlung. Dieses Muster skaliert von 10-zeiligen Hilfsskripten bis zu 500-zeiligen Deployment-Skripten.

shbash
#!/usr/bin/env bash
set -euo pipefail
 
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly LOG_FILE="/tmp/deploy-$(date +%Y%m%d-%H%M%S).log"
 
log() {
  local level="$1"
  shift
  echo "[$(date +'%H:%M:%S')] [${level}] $*" | tee -a "$LOG_FILE"
}
 
cleanup() {
  local exit_code=$?
  if [[ $exit_code -ne 0 ]]; then
    log "ERROR" "Script failed with exit code ${exit_code}"
    log "ERROR" "Check log: ${LOG_FILE}"
  fi
  # Remove temp files, restore state, etc.
  rm -f /tmp/deploy-lock
}
 
check_prerequisites() {
  local missing=()
  for cmd in docker kubectl jq; do
    if ! command -v "$cmd" &>/dev/null; then
      missing+=("$cmd")
    fi
  done
 
  if [[ ${#missing[@]} -gt 0 ]]; then
    log "ERROR" "Missing required tools: ${missing[*]}"
    exit 1
  fi
}
 
main() {
  trap cleanup EXIT
  log "INFO" "Starting deployment..."
 
  check_prerequisites
  # ... rest of deployment logic
  log "INFO" "Deployment complete"
}
 
main "$@"

Das trap cleanup EXIT sorgt dafür, dass die Aufräumroutine läuft, egal ob das Skript erfolgreich ist oder fehlschlägt. Das Schlüsselwort readonly verhindert die versehentliche Neuzuweisung von Konstanten.

Bedingte Logik und Vergleiche

Bash kennt zwei Vergleichssyntaxen: [ ] (POSIX) und [[ ]] (erweitertes Bash). Verwende [[ ]] – es kommt mit Leerzeichen, Musterabgleich und logischen Operatoren ohne Überraschungen zurecht.

shbash
# ❌ Single brackets — breaks on empty variables, needs escaping
if [ $status = "active" -a $count -gt 0 ]; then
 
# ✅ Double brackets — safe with empty vars, supports && ||
if [[ "$status" == "active" && "$count" -gt 0 ]]; then

Gängige bedingte Muster:

shbash
# File checks
[[ -f "$file" ]]     # File exists and is a regular file
[[ -d "$dir" ]]      # Directory exists
[[ -x "$script" ]]   # File is executable
[[ -s "$file" ]]     # File exists and is not empty
 
# String checks
[[ -z "$var" ]]      # String is empty
[[ -n "$var" ]]      # String is not empty
[[ "$var" == *.log ]] # Glob pattern match
 
# Default values
name="${1:-anonymous}"          # Default if unset
db_host="${DB_HOST:?'DB_HOST must be set'}"  # Error if unset

Die :?-Syntax ist für erforderliche Umgebungsvariablen unverzichtbar. Statt stillschweigend einen leeren String zu verwenden, bricht das Skript mit einer klaren Fehlermeldung ab.

Daten sicher verarbeiten

Vermeide es, die Ausgabe von ls zu parsen oder dich beim Durchlaufen von Dateien auf Wortaufteilung zu verlassen. Verwende Globs und find mit korrekter Behandlung des Null-Trennzeichens.

shbash
# ❌ Parsing ls — breaks on spaces, special characters, symlinks
for file in $(ls /data/*.csv); do
  process "$file"
done
 
# ✅ Glob pattern — handles all filenames correctly
for file in /data/*.csv; do
  [[ -f "$file" ]] || continue
  process "$file"
done
 
# ✅ find with null delimiter — recursive, handles everything
while IFS= read -r -d '' file; do
  process "$file"
done < <(find /data -name '*.csv' -type f -print0)

Verwende für die JSON-Verarbeitung jq. Für strukturierten Text verwende awk. Verzichte auf verkettete grep | sed | cut-Aufrufe für alles, was ein einzelner jq- oder awk-Befehl erledigen könnte.

shbash
# ❌ Fragile pipeline — breaks if JSON format changes
curl -s "$API_URL" | grep '"name"' | sed 's/.*: "//;s/".*//'
 
# ✅ Structured JSON parsing with jq
curl -s "$API_URL" | jq -r '.items[].name'

Portable Skriptmuster

Wenn ein Skript auf macOS, Linux und in CI-Umgebungen laufen muss, vermeide plattformspezifische Annahmen.

shbash
# ❌ GNU-specific flags — fails on macOS
date -d "2020-01-01" +%s
sed -i 's/old/new/g' file.txt
 
# ✅ Cross-platform alternatives
# Date parsing — use Python for portability
python3 -c "from datetime import datetime; print(int(datetime(2020,1,1).timestamp()))"
 
# In-place sed — macOS requires backup extension
if [[ "$(uname)" == "Darwin" ]]; then
  sed -i '' 's/old/new/g' file.txt
else
  sed -i 's/old/new/g' file.txt
fi

Wenn die plattformübergreifende Kompatibilität zu mühsam wird, ist das ein Signal, auf Python oder Node.js umzusteigen. Shell-Scripting glänzt bei der Orchestrierung anderer Befehle. Es tut sich schwer mit Stringverarbeitung, Datenstrukturen und komplexer Logik.

Die wichtigsten Erkenntnisse

  1. Verwende immer set -euo pipefail — es verhindert stille Fehler, die echte Ausfälle verursachen
  2. Setze jede Variable in Anführungszeichen — "$var" ist richtig, $var ist ein Fehler, der nur darauf wartet zu passieren
  3. Verwende [[ ]], nicht [ ] — doppelte Klammern decken Grenzfälle ab, die einfache nicht abdecken
  4. Strukturiere mit Funktionen und main — selbst kleine Skripte profitieren von klarer Organisation
  5. Verwende jq für JSON und Globs für Dateien — vermeide brüchige grep | sed | cut-Pipelines
  6. Wisse, wann Schluss ist — wenn das Skript Datenstrukturen oder Fehlerbehandlung jenseits von try/catch braucht, wechsle zu einer echten Programmiersprache
Wilfredo Rujel

Wilfredo Rujel

Full-Stack-Softwareentwickler

Diesen Beitrag teilenX