Zum Inhalt springen

Shell-Scripting für Webentwickler – Bash & ZSH Automation auf macOS (2025)

· von

Shell-Scripts automatisieren alles, was du mehr als dreimal machst. Von simplen Backup-Scripts bis zu komplexen Build-Pipelines – hier lernst du Shell-Scripting von Grund auf, mit praktischen Beispielen für Webentwickler.

Shell-Scripting auf macOS – Übersicht

~15 Min. Lesezeit · Veröffentlicht am

🎯 Was du hier lernst:

  • Shell-Scripts schreiben und ausführen
  • Build-Prozesse automatisieren
  • Git-Workflows scripten
  • Backup & Deploy automatisieren
  • Fehler behandeln und debuggen

Erste Schritte

🟢 Einsteiger-Bereich: Dein erstes Shell-Script

Erstes Script erstellen

#!/bin/bash
# Mein erstes Script: hello.sh

echo "Hello World!"
echo "Aktuelles Datum: $(date)"
echo "Du bist: $USER"
echo "Arbeitsverzeichnis: $PWD"

Script ausführbar machen und starten

# Ausführbar machen
chmod +x hello.sh

# Ausführen
./hello.sh

# Oder direkt mit bash/zsh
bash hello.sh
zsh hello.sh

Bash vs. ZSH auf macOS

macOS nutzt seit Catalina standardmäßig ZSH. Die meisten Bash-Scripts laufen aber problemlos:

# Welche Shell ist aktiv?
echo $SHELL  # /bin/zsh

# Verfügbare Shells
cat /etc/shells

# Bash-Version (macOS hat oft alte Version 3.2)
bash --version

# ZSH-Version
zsh --version

Shebang & Permissions

Shebang-Varianten

#!/bin/bash           # Bash explizit
#!/bin/zsh           # ZSH explizit
#!/bin/sh            # POSIX-kompatible Shell
#!/usr/bin/env bash  # Bash über PATH (portabler)
#!/usr/bin/env zsh   # ZSH über PATH
#!/usr/bin/env node  # Für Node.js Scripts
#!/usr/bin/env python3 # Für Python Scripts

Permissions verstehen

# Permissions anzeigen
ls -l script.sh
# -rw-r--r--  = 644 (nur Owner kann schreiben)
# -rwxr-xr-x = 755 (alle können ausführen)
# -rwx------ = 700 (nur Owner kann alles)

# Ausführbar für Owner
chmod u+x script.sh

# Ausführbar für alle
chmod +x script.sh
chmod 755 script.sh

# Nur Owner (sicherer)
chmod 700 script.sh

Variablen & Parameter

Variablen definieren und nutzen

#!/bin/bash

# Variablen setzen (KEINE Leerzeichen um =)
NAME="Sven"
AGE=30
IS_DEV=true

# Variablen nutzen
echo "Hallo $NAME"
echo "Du bist $AGE Jahre alt"
echo "Entwickler: $IS_DEV"

# Geschützte Variablen (bei Sonderzeichen)
FILE="data.json"
echo "${FILE}_backup"  # data.json_backup
echo "$FILE_backup"    # Sucht nach Variable FILE_backup

# Command Substitution
CURRENT_DATE=$(date +%Y-%m-%d)
FILE_COUNT=$(ls -1 | wc -l)
GIT_BRANCH=$(git branch --show-current)

# Arithmetik
COUNT=5
COUNT=$((COUNT + 1))  # 6
DOUBLE=$((COUNT * 2)) # 12

Script-Parameter

#!/bin/bash
# script.sh param1 param2

echo "Script Name: $0"
echo "Erster Parameter: $1"
echo "Zweiter Parameter: $2"
echo "Alle Parameter: $@"
echo "Anzahl Parameter: $#"
echo "Exit Code letzter Befehl: $?"
echo "Prozess ID: $$"

# Mit Default-Werten
NAME=${1:-"Standardname"}
PORT=${2:-3000}

# Shift - Parameter verschieben
shift  # $2 wird zu $1, $3 zu $2, etc.
echo "Nach shift: $1"

Umgebungsvariablen

#!/bin/bash

# System-Variablen nutzen
echo "Home: $HOME"
echo "User: $USER"
echo "Path: $PATH"
echo "Shell: $SHELL"
echo "Editor: $EDITOR"

# Eigene exportieren
export API_KEY="secret123"
export NODE_ENV="production"

# Nur für einen Befehl
NODE_ENV=development npm start

# .env Datei laden
if [ -f .env ]; then
  export $(cat .env | xargs)
fi

Conditionals & Tests

If-Else Statements

#!/bin/bash

# Basis If-Else
if [ "$USER" = "sven" ]; then
  echo "Hallo Sven!"
else
  echo "Hallo Fremder!"
fi

# Elif
if [ "$1" = "start" ]; then
  echo "Starte Server..."
elif [ "$1" = "stop" ]; then
  echo "Stoppe Server..."
elif [ "$1" = "restart" ]; then
  echo "Restart..."
else
  echo "Usage: $0 {start|stop|restart}"
  exit 1
fi

# Einzeiler
[ -f "config.json" ] && echo "Config exists" || echo "No config"

# Moderne [[ ]] Syntax (Bash/ZSH)
if [[ $NAME == "Sven" ]]; then
  echo "Match!"
fi

Test-Operatoren

# Datei-Tests
[ -f file.txt ]    # Datei existiert
[ -d folder ]      # Verzeichnis existiert
[ -e item ]        # Existiert (Datei oder Verzeichnis)
[ -s file.txt ]    # Datei existiert und nicht leer
[ -r file.txt ]    # Lesbar
[ -w file.txt ]    # Schreibbar
[ -x script.sh ]   # Ausführbar
[ -L link ]        # Symbolischer Link

# String-Vergleiche
[ "$A" = "$B" ]    # Gleich
[ "$A" != "$B" ]   # Ungleich
[ -z "$A" ]        # Leer
[ -n "$A" ]        # Nicht leer

# Zahlen-Vergleiche
[ $A -eq $B ]      # Equal
[ $A -ne $B ]      # Not equal
[ $A -gt $B ]      # Greater than
[ $A -ge $B ]      # Greater or equal
[ $A -lt $B ]      # Less than
[ $A -le $B ]      # Less or equal

# Logische Operatoren
[ $A -eq 1 ] && [ $B -eq 2 ]  # AND
[ $A -eq 1 ] || [ $B -eq 2 ]  # OR
[ ! -f file.txt ]              # NOT

Loops & Iteration

For Loops

#!/bin/bash

# Über Liste iterieren
for file in *.js; do
  echo "Processing: $file"
done

# Über Array
FILES=(app.js index.js test.js)
for file in "${FILES[@]}"; do
  echo "File: $file"
done

# Über Bereich (Bash/ZSH)
for i in {1..5}; do
  echo "Nummer: $i"
done

# C-Style (Bash)
for ((i=0; i<10; i++)); do
  echo "Index: $i"
done

# Über Kommando-Output
for branch in $(git branch -r); do
  echo "Remote branch: $branch"
done

While & Until

#!/bin/bash

# While Loop
COUNTER=0
while [ $COUNTER -lt 10 ]; do
  echo "Count: $COUNTER"
  COUNTER=$((COUNTER + 1))
done

# Datei zeilenweise lesen
while IFS= read -r line; do
  echo "Zeile: $line"
done < input.txt

# Until Loop (läuft bis Bedingung true)
until [ -f "ready.txt" ]; do
  echo "Warte auf ready.txt..."
  sleep 1
done

# Endlos-Loop mit Break
while true; do
  echo "Running... (Ctrl+C to stop)"
  # Mache etwas
  [ -f "stop.txt" ] && break
  sleep 1
done

Funktionen

Funktionen definieren und nutzen

#!/bin/bash

# Einfache Funktion
function greet() {
  echo "Hallo $1!"
}

# Alternative Syntax
say_goodbye() {
  echo "Tschüss $1!"
}

# Aufruf
greet "Sven"
say_goodbye "Welt"

# Mit Return Value
is_number() {
  if [[ $1 =~ ^[0-9]+$ ]]; then
    return 0  # true/success
  else
    return 1  # false/failure
  fi
}

# Nutzen
if is_number "42"; then
  echo "Ist eine Zahl!"
fi

# Mit Output
get_timestamp() {
  echo $(date +%Y%m%d_%H%M%S)
}

# Output in Variable speichern
TIMESTAMP=$(get_timestamp)
echo "Backup_$TIMESTAMP.tar.gz"

# Lokale Variablen
calculate() {
  local num1=$1
  local num2=$2
  local result=$((num1 + num2))
  echo $result
}

RESULT=$(calculate 5 3)  # 8

🔥 Fortgeschrittene Features: Hier wird's richtig interessant!

Input & Output

User Input

#!/bin/bash

# Einfacher Input
echo "Wie heißt du?"
read NAME
echo "Hallo $NAME!"

# Mit Prompt
read -p "Bitte Port eingeben: " PORT
echo "Nutze Port: $PORT"

# Silent (für Passwörter)
read -s -p "Passwort: " PASSWORD
echo  # Neue Zeile nach silent input

# Mit Timeout
read -t 5 -p "Schnell! (5 Sekunden): " ANSWER

# Mit Default
read -p "Environment [development]: " ENV
ENV=${ENV:-development}

# Auswahl-Menü
echo "Wähle eine Option:"
select option in "Start" "Stop" "Restart" "Quit"; do
  case $option in
    Start) echo "Starting..."; break;;
    Stop) echo "Stopping..."; break;;
    Restart) echo "Restarting..."; break;;
    Quit) exit;;
    *) echo "Ungültige Option";;
  esac
done

Output Redirection

#!/bin/bash

# Stdout in Datei
echo "Log entry" > log.txt    # Überschreiben
echo "Another entry" >> log.txt # Anhängen

# Stderr umleiten
command 2> errors.txt          # Nur Fehler
command > output.txt 2>&1      # Alles in eine Datei
command &> all.txt             # Kurzform für alles

# Stdout und Stderr trennen
command 1> stdout.txt 2> stderr.txt

# Tee - Output in Datei UND Terminal
echo "Important" | tee log.txt
echo "More" | tee -a log.txt  # Append

# Here Document
cat << EOF > config.json
{
  "name": "$APP_NAME",
  "port": $PORT,
  "env": "$NODE_ENV"
}
EOF

# Null Device (Output verwerfen)
command > /dev/null 2>&1

# Nur bei Erfolg speichern
command && echo "Success" > success.log

Error Handling

Exit Codes & Error Checking

#!/bin/bash

# Exit Codes setzen
exit 0  # Success
exit 1  # General error
exit 2  # Misuse of shell command
exit 127  # Command not found

# Exit Code prüfen
npm build
if [ $? -eq 0 ]; then
  echo "Build erfolgreich!"
else
  echo "Build fehlgeschlagen!"
  exit 1
fi

# Kurzform mit &&/||
npm build && echo "Success" || echo "Failed"

# Set -e: Bei Fehler sofort beenden
set -e
npm install  # Script stoppt hier bei Fehler
npm build
npm test

# Set -u: Undefined variables als Fehler
set -u
echo $UNDEFINED_VAR  # Fehler!

# Set -o pipefail: Pipe-Fehler beachten
set -eo pipefail
cat missing.txt | grep something  # Stoppt bei Fehler

# Trap - Cleanup bei Exit
cleanup() {
  echo "Cleaning up..."
  rm -f /tmp/tempfile
}
trap cleanup EXIT

# Error Handler
error_handler() {
  echo "Error on line $1"
  exit 1
}
trap 'error_handler $LINENO' ERR

Logging

#!/bin/bash

# Log-Funktion
log() {
  echo "[$(date +'%Y-%m-%d %H:%M:%S')] $@" | tee -a app.log
}

log "Script started"
log "Processing files..."
log "ERROR: File not found" >&2

# Mit Log-Levels
LOG_LEVEL=${LOG_LEVEL:-INFO}

log_debug() {
  [ "$LOG_LEVEL" = "DEBUG" ] && echo "[DEBUG] $@" >&2
}

log_info() {
  echo "[INFO] $@"
}

log_error() {
  echo "[ERROR] $@" >&2
}

# Verwendung
log_debug "Variable X = $X"
log_info "Starting process..."
log_error "Failed to connect!"

Praktische Beispiele

Backup Script

#!/bin/bash
# backup.sh - Erstellt timestamped Backups

set -e

# Config
SOURCE_DIR="${1:-$PWD}"
BACKUP_DIR="${2:-$HOME/Backups}"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_NAME="backup_${TIMESTAMP}.tar.gz"

# Backup-Verzeichnis erstellen
mkdir -p "$BACKUP_DIR"

# Backup erstellen
echo "Creating backup of $SOURCE_DIR..."
tar -czf "$BACKUP_DIR/$BACKUP_NAME" \
  --exclude='node_modules' \
  --exclude='.git' \
  --exclude='dist' \
  "$SOURCE_DIR"

# Alte Backups löschen (älter als 30 Tage)
echo "Cleaning old backups..."
find "$BACKUP_DIR" -name "backup_*.tar.gz" -mtime +30 -delete

# Erfolg
echo "✅ Backup created: $BACKUP_NAME"
echo "📁 Location: $BACKUP_DIR"
echo "📊 Size: $(du -h "$BACKUP_DIR/$BACKUP_NAME" | cut -f1)"

# Liste der Backups
echo -e "\nCurrent backups:"
ls -lh "$BACKUP_DIR"/backup_*.tar.gz 2>/dev/null | tail -5

File Watcher

#!/bin/bash
# watch.sh - Überwacht Dateien und führt Befehle aus

# Nutzt fswatch (brew install fswatch)
if ! command -v fswatch &> /dev/null; then
  echo "fswatch nicht installiert. Installiere mit: brew install fswatch"
  exit 1
fi

# Config
WATCH_DIR="${1:-.}"
WATCH_PATTERN="${2:-*.js}"
COMMAND="${3:-npm run build}"

echo "👀 Watching $WATCH_DIR for changes in $WATCH_PATTERN"
echo "🚀 Will execute: $COMMAND"
echo "Press Ctrl+C to stop"

# Initial build
eval "$COMMAND"

# Watch for changes
fswatch -o "$WATCH_DIR" | while read f; do
  clear
  echo "📝 Change detected at $(date +%H:%M:%S)"
  echo "⚡ Running: $COMMAND"
  echo "---"
  
  if eval "$COMMAND"; then
    echo "✅ Success!"
  else
    echo "❌ Failed with exit code $?"
  fi
done

Project Setup Script

#!/bin/bash
# setup-project.sh - Neues Webprojekt initialisieren

set -e

# Projekt-Name abfragen
read -p "Projekt Name: " PROJECT_NAME
if [ -z "$PROJECT_NAME" ]; then
  echo "❌ Projekt Name erforderlich!"
  exit 1
fi

# Optionen
read -p "TypeScript verwenden? (y/n): " USE_TS
read -p "Tailwind CSS? (y/n): " USE_TAILWIND
read -p "Git initialisieren? (y/n): " USE_GIT

# Projekt-Verzeichnis erstellen
mkdir -p "$PROJECT_NAME"
cd "$PROJECT_NAME"

# package.json erstellen
cat > package.json << EOF
{
  "name": "$PROJECT_NAME",
  "version": "1.0.0",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  }
}
EOF

# Dependencies installieren
echo "📦 Installing dependencies..."
npm install -D vite

[ "$USE_TS" = "y" ] && npm install -D typescript @types/node
[ "$USE_TAILWIND" = "y" ] && npm install -D tailwindcss postcss autoprefixer

# Basis-Struktur
mkdir -p src public
cat > src/main.js << 'EOF'
console.log('Hello from Vite!');
EOF

cat > index.html << EOF



  
  
  $PROJECT_NAME


  
EOF # Git if [ "$USE_GIT" = "y" ]; then git init echo "node_modules/" > .gitignore echo "dist/" >> .gitignore git add . git commit -m "Initial commit" fi echo "✅ Projekt '$PROJECT_NAME' erstellt!" echo "📁 cd $PROJECT_NAME" echo "🚀 npm run dev"

Build & Deploy Automation

Build Pipeline

#!/bin/bash
# build-pipeline.sh - Komplette Build-Pipeline

set -eo pipefail

# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color

# Functions
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }

# Timer
START_TIME=$(date +%s)

# Clean
log_info "Cleaning dist folder..."
rm -rf dist
mkdir -p dist

# Install dependencies
if [ ! -d "node_modules" ] || [ package.json -nt node_modules ]; then
  log_info "Installing dependencies..."
  npm ci
fi

# Lint
log_info "Running linter..."
npm run lint || {
  log_warn "Linting failed, continuing..."
}

# Tests
if [ "$SKIP_TESTS" != "true" ]; then
  log_info "Running tests..."
  npm test
else
  log_warn "Skipping tests (SKIP_TESTS=true)"
fi

# Build
log_info "Building application..."
npm run build

# Post-processing
log_info "Optimizing assets..."
find dist -name "*.js" -exec gzip -k {} \;
find dist -name "*.css" -exec gzip -k {} \;

# Generate build info
cat > dist/build-info.json << EOF
{
  "version": "$(git describe --tags --always)",
  "branch": "$(git branch --show-current)",
  "commit": "$(git rev-parse HEAD)",
  "buildDate": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
  "builder": "$USER"
}
EOF

# Time calculation
END_TIME=$(date +%s)
DURATION=$((END_TIME - START_TIME))

log_info "✅ Build completed in ${DURATION}s"
log_info "📁 Output: ./dist"
log_info "📊 Size: $(du -sh dist | cut -f1)"

Deploy Script

#!/bin/bash
# deploy.sh - Deploy to server via rsync/ssh

set -e

# Config from environment or defaults
SERVER=${DEPLOY_SERVER:-"user@server.com"}
REMOTE_PATH=${DEPLOY_PATH:-"/var/www/html"}
LOCAL_PATH=${LOCAL_PATH:-"./dist"}
SSH_KEY=${SSH_KEY:-"$HOME/.ssh/id_rsa"}

# Verify build exists
if [ ! -d "$LOCAL_PATH" ]; then
  echo "❌ Build folder not found. Run build first!"
  exit 1
fi

# Dry run option
if [ "$1" = "--dry-run" ]; then
  echo "🔍 Dry run mode..."
  DRY_RUN="--dry-run"
fi

# Pre-deploy checks
echo "🔍 Running pre-deploy checks..."
ssh -i "$SSH_KEY" "$SERVER" "df -h $REMOTE_PATH" || {
  echo "❌ Cannot connect to server!"
  exit 1
}

# Backup on server
echo "📦 Creating backup on server..."
ssh -i "$SSH_KEY" "$SERVER" \
  "cd $REMOTE_PATH && tar -czf ../backup_$(date +%Y%m%d_%H%M%S).tar.gz ."

# Deploy
echo "🚀 Deploying to $SERVER..."
rsync -avz --delete \
  $DRY_RUN \
  --exclude '.DS_Store' \
  --exclude '*.map' \
  -e "ssh -i $SSH_KEY" \
  "$LOCAL_PATH/" \
  "$SERVER:$REMOTE_PATH/"

# Post-deploy
if [ -z "$DRY_RUN" ]; then
  echo "🔄 Running post-deploy commands..."
  ssh -i "$SSH_KEY" "$SERVER" << 'ENDSSH'
    # Clear cache
    redis-cli FLUSHDB
    # Restart services
    sudo systemctl reload nginx
    # Verify
    curl -s -o /dev/null -w "%{http_code}" http://localhost
ENDSSH
fi

echo "✅ Deploy complete!"

Git Automation

Git Workflow Automation

#!/bin/bash
# git-flow.sh - Automatisierter Git Workflow

set -e

# Parse command
COMMAND=${1:-help}
BRANCH_NAME=${2:-}

# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
BLUE='\033[0;34m'
NC='\033[0m'

# Current branch
CURRENT_BRANCH=$(git branch --show-current)

case "$COMMAND" in
  feature)
    if [ -z "$BRANCH_NAME" ]; then
      echo "Usage: $0 feature "
      exit 1
    fi
    
    echo -e "${BLUE}Creating feature branch...${NC}"
    git checkout main
    git pull origin main
    git checkout -b "feature/$BRANCH_NAME"
    echo -e "${GREEN}✅ Feature branch 'feature/$BRANCH_NAME' created${NC}"
    ;;
    
  finish)
    if [[ ! "$CURRENT_BRANCH" =~ ^feature/ ]]; then
      echo -e "${RED}Not on a feature branch!${NC}"
      exit 1
    fi
    
    echo -e "${BLUE}Finishing feature...${NC}"
    
    # Stash uncommitted changes
    git stash
    
    # Update main
    git checkout main
    git pull origin main
    
    # Merge feature
    git merge --no-ff "$CURRENT_BRANCH" \
      -m "Merge $CURRENT_BRANCH into main"
    
    # Push
    git push origin main
    
    # Delete feature branch
    git branch -d "$CURRENT_BRANCH"
    git push origin --delete "$CURRENT_BRANCH" 2>/dev/null || true
    
    echo -e "${GREEN}✅ Feature merged and cleaned up${NC}"
    ;;
    
  sync)
    echo -e "${BLUE}Syncing with remote...${NC}"
    
    # Fetch all
    git fetch --all --prune
    
    # Update current branch
    git pull --rebase
    
    # Clean merged branches
    git branch --merged main | grep -v main | xargs -n 1 git branch -d 2>/dev/null || true
    
    echo -e "${GREEN}✅ Synced with remote${NC}"
    ;;
    
  status)
    echo -e "${BLUE}=== Git Status Overview ===${NC}"
    echo "Branch: $CURRENT_BRANCH"
    echo "Remote: $(git remote get-url origin)"
    echo ""
    
    # Status
    git status -s
    
    # Recent commits
    echo -e "\n${BLUE}Recent commits:${NC}"
    git log --oneline -5
    
    # Branches
    echo -e "\n${BLUE}Local branches:${NC}"
    git branch -vv
    ;;
    
  *)
    echo "Git Workflow Helper"
    echo "Usage: $0 {feature|finish|sync|status} [options]"
    echo ""
    echo "Commands:"
    echo "  feature   - Create new feature branch"
    echo "  finish         - Merge current feature to main"
    echo "  sync           - Sync with remote and cleanup"
    echo "  status         - Show comprehensive status"
    ;;
esac

Commit Message Helper

#!/bin/bash
# commit-helper.sh - Strukturierte Commit Messages

set -e

# Commit types
echo "Select commit type:"
select TYPE in "feat" "fix" "docs" "style" "refactor" "test" "chore" "perf"; do
  [ -n "$TYPE" ] && break
done

# Scope (optional)
read -p "Scope (optional, e.g., api, ui): " SCOPE
[ -n "$SCOPE" ] && SCOPE="($SCOPE)"

# Description
read -p "Short description: " DESCRIPTION
if [ -z "$DESCRIPTION" ]; then
  echo "❌ Description required!"
  exit 1
fi

# Body (optional)
echo "Long description (optional, Ctrl+D when done):"
BODY=$(cat)

# Breaking change?
read -p "Breaking change? (y/n): " BREAKING
if [ "$BREAKING" = "y" ]; then
  read -p "Describe breaking change: " BREAKING_DESC
  FOOTER="BREAKING CHANGE: $BREAKING_DESC"
fi

# Build commit message
COMMIT_MSG="$TYPE$SCOPE: $DESCRIPTION"
[ -n "$BODY" ] && COMMIT_MSG="$COMMIT_MSG\n\n$BODY"
[ -n "$FOOTER" ] && COMMIT_MSG="$COMMIT_MSG\n\n$FOOTER"

# Show preview
echo -e "\n📝 Commit message preview:"
echo -e "---"
echo -e "$COMMIT_MSG"
echo -e "---"

# Confirm
read -p "Commit with this message? (y/n): " CONFIRM
if [ "$CONFIRM" = "y" ]; then
  git add -A
  echo -e "$COMMIT_MSG" | git commit -F -
  echo "✅ Committed!"
else
  echo "❌ Aborted"
fi

Debugging

Debug-Techniken

#!/bin/bash

# Debug-Modus aktivieren
set -x  # Zeigt jeden Befehl vor Ausführung
set -v  # Zeigt Script-Zeilen

# Oder beim Aufruf
bash -x script.sh
bash -v script.sh

# Selektives Debugging
DEBUG=${DEBUG:-false}
if [ "$DEBUG" = "true" ]; then
  set -x
fi

# Debug-Funktion
debug() {
  [ "$DEBUG" = "true" ] && echo "[DEBUG] $@" >&2
}

debug "Variable X = $X"
debug "Entering function foo()"

# Trace-Funktion
trace() {
  echo "[TRACE:${BASH_SOURCE[1]}:${BASH_LINENO[0]}:${FUNCNAME[1]}] $@" >&2
}

# Assertions
assert() {
  if [ "$1" != "$2" ]; then
    echo "Assertion failed: '$1' != '$2'" >&2
    echo "Location: ${BASH_SOURCE[1]}:${BASH_LINENO[0]}" >&2
    exit 1
  fi
}

# Verwendung
RESULT=$(calculate 2 3)
assert "$RESULT" "5"

# Variable Dump
dump_vars() {
  echo "=== Variable Dump ==="
  echo "PWD: $PWD"
  echo "USER: $USER"
  echo "PATH: $PATH"
  echo "All vars:"
  set | grep -E "^[A-Z_]+="
}

ShellCheck - Linting für Shell Scripts

# Installation
brew install shellcheck

# Verwendung
shellcheck script.sh

# In VS Code
# Extension: ShellCheck

# Inline-Direktiven
# shellcheck disable=SC2086
echo $VARIABLE_WITHOUT_QUOTES  # Absichtlich

# Global ignorieren
# shellcheck disable=SC2086,SC2046

# Strikt prüfen
shellcheck -S error script.sh   # Nur Fehler
shellcheck -S warning script.sh # Mit Warnings
shellcheck -S info script.sh    # Alles

# Format für CI/CD
shellcheck -f json script.sh

Best Practices

Sicheres Scripting

#!/bin/bash
# Best Practice Template

# Strict Mode
set -euo pipefail
IFS=$'\n\t'

# Error handling
error_exit() {
  echo "ERROR: $1" >&2
  exit "${2:-1}"
}

# Cleanup on exit
cleanup() {
  rm -f "$TEMP_FILE"
}
trap cleanup EXIT

# Check dependencies
check_deps() {
  local deps=(git node npm)
  for cmd in "${deps[@]}"; do
    if ! command -v "$cmd" &> /dev/null; then
      error_exit "$cmd is not installed"
    fi
  done
}

# Validate input
validate_input() {
  if [ $# -eq 0 ]; then
    error_exit "No arguments provided"
  fi
  
  if [ ! -f "$1" ]; then
    error_exit "File not found: $1"
  fi
}

# Main function
main() {
  check_deps
  validate_input "$@"
  
  # Your code here
  echo "Processing..."
}

# Only run main if script is executed (not sourced)
if [ "${BASH_SOURCE[0]}" = "${0}" ]; then
  main "$@"
fi

Performance Tips

#!/bin/bash

# SCHLECHT: Mehrere externe Befehle
cat file.txt | grep pattern | wc -l

# BESSER: Ein Befehl
grep -c pattern file.txt

# SCHLECHT: Schleife mit externem Befehl
for file in *.txt; do
  cat "$file" | wc -l
done

# BESSER: Einen Befehl für alle
wc -l *.txt

# SCHLECHT: Backticks
files=`ls *.txt`

# BESSER: $() Syntax
files=$(ls *.txt)

# NOCH BESSER: Glob direkt
files=(*.txt)

# Parallel Processing
# SCHLECHT: Sequenziell
for file in *.jpg; do
  convert "$file" "thumb_$file"
done

# BESSER: Parallel mit xargs
ls *.jpg | xargs -P 4 -I {} convert {} thumb_{}

# ODER: GNU Parallel
parallel convert {} thumb_{} ::: *.jpg

# Arrays statt String-Splitting
# SCHLECHT
FILES="file1.txt file2.txt file3.txt"
for f in $FILES; do
  echo "$f"
done

# BESSER
FILES=("file1.txt" "file2.txt" "file3.txt")
for f in "${FILES[@]}"; do
  echo "$f"
done

Großes Cheatsheet

📋 Shell Scripting Cheatsheet

Variablen & Substitution

# Variable Assignment
VAR="value"                  # String
NUM=42                       # Number
ARRAY=(one two three)        # Array
declare -r CONST="fixed"     # Readonly
declare -i INT=5            # Integer
declare -a ARR              # Array
declare -A HASH             # Associative array

# Variable Expansion
${VAR}                      # Value
${VAR:-default}            # Default if unset
${VAR:=default}           # Set default if unset
${VAR:?error}             # Error if unset
${VAR:+alt}               # Alt value if set
${#VAR}                   # Length
${VAR:2:5}               # Substring (pos:len)
${VAR#pattern}           # Remove from start
${VAR%pattern}          # Remove from end
${VAR/old/new}          # Replace first
${VAR//old/new}         # Replace all
${VAR^}                 # Uppercase first
${VAR^^}                # Uppercase all
${VAR,}                 # Lowercase first
${VAR,,}                # Lowercase all

Arrays

# Arrays (Bash 4+)
arr=(a b c)                 # Create
arr[0]="first"             # Set element
${arr[0]}                  # Get element
${arr[@]}                  # All elements
${!arr[@]}                 # All indices
${#arr[@]}                 # Length
arr+=(d e)                 # Append
unset arr[1]               # Remove element

# Associative Arrays
declare -A hash
hash[key]="value"
${hash[key]}               # Get value
${!hash[@]}                # All keys
${hash[@]}                 # All values

Conditionals

# File Tests
-e file    # Exists
-f file    # Regular file
-d dir     # Directory
-s file    # Not empty
-r file    # Readable
-w file    # Writable
-x file    # Executable
-L link    # Symlink
-S file    # Socket
-p file    # Named pipe
-b file    # Block device
-c file    # Character device
-t fd      # Terminal

# File Comparisons
file1 -nt file2  # Newer than
file1 -ot file2  # Older than
file1 -ef file2  # Same file

# String Tests
-z str     # Empty
-n str     # Not empty
s1 = s2    # Equal
s1 != s2   # Not equal
s1 < s2    # Less than
s1 > s2    # Greater than

# Numeric Tests
n1 -eq n2  # Equal
n1 -ne n2  # Not equal
n1 -lt n2  # Less than
n1 -le n2  # Less or equal
n1 -gt n2  # Greater than
n1 -ge n2  # Greater or equal

Loop Patterns

# For Loops
for i in {1..10}; do ...; done
for i in {1..10..2}; do ...; done    # Step 2
for ((i=0; i<10; i++)); do ...; done
for file in *.txt; do ...; done
for arg in "$@"; do ...; done

# While/Until
while read line; do ...; done < file
while true; do ...; break; done
until [ condition ]; do ...; done

# Process substitution
while read line; do
  echo "$line"
done < <(command)

# Parallel execution
for i in {1..10}; do
  command &
done
wait  # Wait for all

Functions

# Function definition
func() {
  local var=$1
  echo "Result"
  return 0
}

# Call function
result=$(func arg1 arg2)

# Check return value
if func; then
  echo "Success"
fi

# Pass arrays
func() {
  local -n arr=$1  # Name reference
  echo "${arr[@]}"
}
myarr=(1 2 3)
func myarr

🛠️ Useful One-Liners

File Operations

# Find and replace in files
find . -name "*.txt" -exec sed -i '' 's/old/new/g' {} \;

# Delete empty directories
find . -type d -empty -delete

# Find large files
find . -type f -size +100M

# Count lines of code
find . -name "*.js" -exec wc -l {} \; | awk '{sum+=$1} END {print sum}'

# Backup with timestamp
tar czf backup_$(date +%Y%m%d_%H%M%S).tar.gz folder/

# Batch rename
for f in *.jpeg; do mv "$f" "${f%.jpeg}.jpg"; done

# Create multiple directories
mkdir -p project/{src,dist,test}/{js,css,img}

# Watch file changes
while true; do clear; ls -la; sleep 1; done

Text Processing

# Extract column
cut -d',' -f2 file.csv

# Sort unique
sort -u file.txt

# Count occurrences
sort file.txt | uniq -c | sort -rn

# JSON pretty print
cat file.json | python -m json.tool

# Extract between patterns
sed -n '/START/,/END/p' file.txt

# Remove empty lines
sed '/^$/d' file.txt

# Replace tabs with spaces
expand -t 4 file.txt

# Add line numbers
nl file.txt
cat -n file.txt

Network & System

# Port in use?
lsof -i :3000
netstat -an | grep 3000

# Kill process on port
kill -9 $(lsof -t -i:3000)

# Monitor file
tail -f log.txt

# System info
uname -a           # All info
sw_vers           # macOS version
system_profiler   # Detailed info

# Disk usage
du -sh *          # Summary
df -h             # Free space
ncdu              # Interactive

# Process tree
pstree
ps aux | grep node

# Network test
ping -c 5 google.com
curl -I https://example.com
traceroute google.com

🎯 Git Aliases for Shell

# Add to ~/.gitconfig or ~/.zshrc

# Quick status
alias gs='git status -sb'

# Pretty log
alias gl='git log --graph --pretty=format:"%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset" --abbrev-commit'

# Quick commit
alias gc='git commit -m'
alias gca='git commit -am'

# Branch management
alias gb='git branch'
alias gco='git checkout'
alias gcb='git checkout -b'

# Stash helpers
alias gst='git stash'
alias gstp='git stash pop'
alias gstl='git stash list'

# Undo helpers
alias gundo='git reset HEAD~1 --soft'
alias greset='git reset --hard HEAD'

# Diff helpers
alias gd='git diff'
alias gds='git diff --staged'

# Remote
alias gp='git push'
alias gpu='git push -u origin $(git branch --show-current)'
alias gpl='git pull --rebase'

# Cleanup
alias gclean='git branch --merged | grep -v "\*\|main\|master" | xargs -n 1 git branch -d'

⚡ ZSH Specific Features

# ZSH Arrays (1-indexed!)
arr=(a b c)
echo $arr[1]  # 'a' (nicht 0!)

# Globbing
**/*.js       # Recursive
*.{js,ts}     # Multiple extensions
*~*.bak       # Exclude pattern
*(.)          # Regular files only
*(/)          # Directories only
*(*)          # Executables only
*(@)          # Symlinks only

# Modifiers
$file:t       # Basename
$file:h       # Directory
$file:e       # Extension
$file:r       # Remove extension
$file:u       # Uppercase
$file:l       # Lowercase

# History expansion
!!            # Last command
!$            # Last argument
!^            # First argument
!*            # All arguments
!-2           # Two commands ago

# Parameter expansion
${+VAR}       # 1 if set, 0 if not
${VAR:#pattern} # Remove matching
${(j:,:)array}  # Join with comma
${(s:,:)string} # Split by comma
${(U)VAR}     # Uppercase
${(L)VAR}     # Lowercase

📚 Script Template Collection

Minimal Script

#!/usr/bin/env bash
set -euo pipefail
# Your code here

Full Featured Script

#!/usr/bin/env bash
#
# script-name.sh - Description
# Usage: script-name.sh [options] arguments
#
# Author: Your Name
# Version: 1.0.0

set -euo pipefail
IFS=$'\n\t'

# --- Constants ---
readonly SCRIPT_NAME=$(basename "$0")
readonly SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
readonly VERSION="1.0.0"

# --- Options ---
DEBUG=${DEBUG:-false}
VERBOSE=${VERBOSE:-false}
DRY_RUN=${DRY_RUN:-false}

# --- Colors ---
if [[ -t 1 ]]; then
  RED='\033[0;31m'
  GREEN='\033[0;32m'
  YELLOW='\033[1;33m'
  BLUE='\033[0;34m'
  NC='\033[0m'
else
  RED=''
  GREEN=''
  YELLOW=''
  BLUE=''
  NC=''
fi

# --- Functions ---
usage() {
  cat << EOF
Usage: $SCRIPT_NAME [OPTIONS] ARGUMENTS

Description of what this script does.

OPTIONS:
  -h, --help      Show this help message
  -v, --version   Show version
  -d, --debug     Enable debug mode
  -n, --dry-run   Dry run mode
  -V, --verbose   Verbose output

EXAMPLES:
  $SCRIPT_NAME file.txt
  $SCRIPT_NAME --debug --verbose file.txt

EOF
  exit 0
}

log() {
  echo -e "${GREEN}[INFO]${NC} $*"
}

warn() {
  echo -e "${YELLOW}[WARN]${NC} $*" >&2
}

error() {
  echo -e "${RED}[ERROR]${NC} $*" >&2
  exit 1
}

debug() {
  if [[ "$DEBUG" == "true" ]]; then
    echo -e "${BLUE}[DEBUG]${NC} $*" >&2
  fi
}

cleanup() {
  debug "Cleaning up..."
  # Cleanup code here
}

# --- Main ---
main() {
  # Parse options
  while [[ $# -gt 0 ]]; do
    case $1 in
      -h|--help)
        usage
        ;;
      -v|--version)
        echo "$SCRIPT_NAME version $VERSION"
        exit 0
        ;;
      -d|--debug)
        DEBUG=true
        set -x
        shift
        ;;
      -n|--dry-run)
        DRY_RUN=true
        shift
        ;;
      -V|--verbose)
        VERBOSE=true
        shift
        ;;
      -*)
        error "Unknown option: $1"
        ;;
      *)
        break
        ;;
    esac
  done

  # Validate arguments
  if [[ $# -eq 0 ]]; then
    error "No arguments provided. Use -h for help."
  fi

  # Setup trap
  trap cleanup EXIT INT TERM

  # Main logic
  log "Starting $SCRIPT_NAME..."
  
  # Your code here
  
  log "Done!"
}

# Run main only if executed directly
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
  main "$@"
fi