EMZETT.
Login

Kurz: Ein Skript, das einen Ordner packt, mit Zeitstempel benennt und nur die letzten n Sicherungen behält:

Teil des Kurses Bash

Kapitel 12 von 13 im Kurs Bash (Abschnitt „Projekte“). Mit Fortschritt, Quiz und Zertifikat auf der Lernseite.

Projekt 1: Backup mit Rotation

Ein Skript, das einen Ordner packt, mit Zeitstempel benennt und nur die letzten n Sicherungen behält:

set -euo pipefail
 
backup() {
    local quelle=$1 ziel=$2 behalten=${3:-3}
    mkdir -p "$ziel"
    local name
    name="backup-$(basename "$quelle")-$4.tar.gz"
    tar -czf "$ziel/$name" -C "$(dirname "$quelle")" "$(basename "$quelle")"
    # alte Sicherungen löschen: nach Namen sortieren, nur die neuesten $behalten behalten
    ls -1 "$ziel" | sort | head -n -"$behalten" | while read -r alt; do
        rm -- "$ziel/$alt"
        echo "gelöscht: $alt"
    done
    echo "erstellt: $name"
}
 
arbeit=$(mktemp -d)
mkdir "$arbeit/daten"
echo "wichtig" > "$arbeit/daten/a.txt"
for tag in 01 02 03 04 05; do
    backup "$arbeit/daten" "$arbeit/sicherungen" 3 "2025-03-$tag"
done
echo "Übrig:"
ls "$arbeit/sicherungen"

Ausgabe:

erstellt: backup-daten-2025-03-01.tar.gz
erstellt: backup-daten-2025-03-02.tar.gz
erstellt: backup-daten-2025-03-03.tar.gz
gelöscht: backup-daten-2025-03-01.tar.gz
erstellt: backup-daten-2025-03-04.tar.gz
gelöscht: backup-daten-2025-03-02.tar.gz
erstellt: backup-daten-2025-03-05.tar.gz
Übrig:
backup-daten-2025-03-03.tar.gz
backup-daten-2025-03-04.tar.gz
backup-daten-2025-03-05.tar.gz

Der Zeitstempel kommt hier als Argument (in der Praxis $(date +%F)), damit die Ausgabe reproduzierbar bleibt.

Projekt 2: Logdatei-Report

cat > access.log <<'EOF'
10.0.0.1 - - [01/Mar/2025:10:00:01] "GET /index.html HTTP/1.1" 200 512
10.0.0.2 - - [01/Mar/2025:10:00:05] "GET /api/users HTTP/1.1" 200 1024
10.0.0.1 - - [01/Mar/2025:10:01:10] "POST /api/login HTTP/1.1" 401 64
10.0.0.3 - - [01/Mar/2025:10:02:00] "GET /missing HTTP/1.1" 404 128
10.0.0.1 - - [01/Mar/2025:10:03:44] "GET /index.html HTTP/1.1" 200 512
10.0.0.2 - - [01/Mar/2025:10:04:00] "GET /api/users HTTP/1.1" 500 32
EOF
 
echo "== Anfragen gesamt: $(wc -l < access.log)"
echo "== Häufigste IPs"
awk '{ print $1 }' access.log | sort | uniq -c | sort -rn | head -3
echo "== Statuscodes"
awk '{ print $9 }' access.log | sort | uniq -c | sort -k2
echo "== Fehlerquote (4xx/5xx)"
awk '$9 >= 400 { f++ } END { printf "%d von %d (%.1f%%)\n", f, NR, f / NR * 100 }' access.log
echo "== Übertragene Bytes"
awk '{ summe += $10 } END { print summe " Bytes" }' access.log
echo "== Beliebteste Pfade"
awk '{ print $7 }' access.log | sort | uniq -c | sort -rn | head -2

Ausgabe:

== Anfragen gesamt: 6
== Häufigste IPs
      3 10.0.0.1
      2 10.0.0.2
      1 10.0.0.3
== Statuscodes
      1 1024
      1 128
      1 32
      2 512
      1 64
== Fehlerquote (4xx/5xx)
3 von 6 (50.0%)
== Übertragene Bytes
0 Bytes
== Beliebteste Pfade
      6 HTTP/1.1"

Projekt 3: Stapelumbenennung

cd "$(mktemp -d)"
touch "Urlaub 1.JPG" "Urlaub 2.JPG" "bild  3.jpg" notiz.txt
 
i=1
for datei in *.[jJ][pP][gG]; do
    neu=$(printf "foto-%03d.jpg" "$i")
    mv -- "$datei" "$neu"
    echo "$datei -> $neu"
    ((i++))
done
ls

Ausgabe:

Urlaub 1.JPG -> foto-001.jpg
Urlaub 2.JPG -> foto-002.jpg
bild  3.jpg -> foto-003.jpg
foto-001.jpg
foto-002.jpg
foto-003.jpg
notiz.txt

Projekt 4: Menü und Eingaben

menue() {
    local wahl=$1
    case "$wahl" in
        1) echo "Datum anzeigen (hier nicht ausgeführt)" ;;
        2) echo "Dateien zählen: $(ls -1 / | wc -l | awk '{ print ($1 > 0) ? "mehr als 0" : "0" }')" ;;
        q) echo "Auf Wiedersehen" ;;
        *) echo "Ungültige Eingabe: $wahl" ;;
    esac
}
for eingabe in 1 2 x q; do menue "$eingabe"; done
# Interaktive Variante:
#   while true; do read -rp "Wahl (1/2/q): " w; menue "$w"; [[ $w == q ]] && break; done

Ausgabe:

Datum anzeigen (hier nicht ausgeführt)
Dateien zählen: mehr als 0
Ungültige Eingabe: x
Auf Wiedersehen

Projekt 5: Zahlenraten (nicht interaktiv simuliert)

ziel=42
raten() {
    local zahl=$1
    if (( zahl < ziel )); then echo "$zahl: zu klein"
    elif (( zahl > ziel )); then echo "$zahl: zu groß"
    else echo "$zahl: richtig!"; return 0
    fi
    return 1
}
versuche=0
for tipp in 50 25 37 43 42; do
    ((versuche++))
    raten "$tipp" && break
done
echo "Geschafft in $versuche Versuchen"

Ausgabe:

50: zu groß
25: zu klein
37: zu klein
43: zu groß
42: richtig!
Geschafft in 5 Versuchen

Für das echte Spiel: ziel=$((RANDOM % 100 + 1)) und read -rp "Tipp: " tipp in einer while-Schleife.

Projekt 6: Eigenes Kommandozeilenwerkzeug

Ein kleines todo-Programm, das Aufgaben in einer Textdatei verwaltet:

datei=$(mktemp)
 
todo() {
    local befehl=${1:-hilfe}; shift || true
    case "$befehl" in
        add)  echo "$*" >> "$datei" ;;
        list) nl -w2 -s'. ' "$datei" ;;
        done) sed -i "${1}d" "$datei" ;;
        *)    echo "Aufruf: todo add TEXT | list | done NUMMER" ;;
    esac
}
 
todo add "Milch kaufen"
todo add "Bash lernen"
todo add "Sport"
todo list
todo done 2
echo "--"
todo list
todo
rm -f "$datei"

Ausgabe:

1. Milch kaufen
 2. Bash lernen
 3. Sport
--
 1. Milch kaufen
 2. Sport
Aufruf: todo add TEXT | list | done NUMMER

Merke

  • Aus kleinen Bausteinen (tar, find, awk, sort, sed) entstehen schnell nützliche Werkzeuge
  • Funktionen, Argumente und Strict Mode machen Skripte wiederverwendbar
  • Zeitstempel und Zufall der Reproduzierbarkeit wegen als Parameter übergeben
  • Ab etwa 100 Zeilen oder komplexer Logik lohnt Python

Übungsaufgabe

Schreibe ein Skript, das alle .log-Dateien eines Ordners älter als 7 Tage komprimiert (find -mtime +7).

Quiz zur Selbstkontrolle

Weiter im Kurs

Zurück: Prozesse, Cron, SSH und Pakete

Weiter: Referenz und Spickzettel

Alle Kapitel: Bash im Überblick