Skip to content

Bash Spickzettel

Unix-Shell und Befehlssprache für Skripting und Automatisierung.

01

Variablen & Strings

Variablen & Zuweisung

Bash-Variablen sind standardmäßig untypisierte Strings; Arithmetik funktioniert, wenn der Wert numerisch ist. Setzen Sie NIEMALS Leerzeichen um = in Zuweisungen — 'name = Alice' versucht, einen Befehl namens name auszuführen. Verwenden Sie $(...) für Kommando-Substitution (bevorzugt gegenüber Backticks). export macht eine Variable für Kind-Prozesse verfügbar; readonly macht sie unveränderlich. Umgebungsvariablen (exportiert) werden von Skripten und Programmen geerbt, die Sie starten.

bash
# no spaces around = in assignments
name="Alice"
age=30
PI=3.14
active=true

# use variables with $ prefix
echo "Hello, $name!"
echo "Age: $age"

# command substitution: capture command output
today=$(date +%Y-%m-%d)
files=$(ls | wc -l)
echo "Today is $today, $files files"

# readonly and environment variables
readonly MAX=100
export PATH="$PATH:/opt/bin"
env | grep PATH

String-Parameter-Expansion

Parameter-Expansion ist eines der mächtigsten Bash-Features, eingeschlossen in ${...}. ${#var} gibt die Länge; ${var:offset:length} extrahiert Teilstrings (negativer Offset zählt vom Ende — beachten Sie das Leerzeichen vor -). ,, und ^^ konvertieren Groß-/Kleinschreibung (Bash 4+). :- bietet Standardwerte, ohne die Variable zu setzen; := setzt sie als Seiteneffekt; :? bricht mit einem Fehler ab — großartig für Pflichtargument-Prüfungen in Skripten.

bash
s="Hello World"
echo ${#s}                # length: 11
echo ${s:0:5}             # substring: Hello
echo ${s:6}               # from index 6: World
echo ${s: -5}             # last 5 chars: World

# case conversion (Bash 4+)
echo ${s,,}               # lowercase: hello world
echo ${s^^}               # uppercase: HELLO WORLD
echo ${s^l}               # capitalize first if 'l': Hello World

# default values
echo ${name:-Guest}       # use Guest if name unset
echo ${name:=Guest}       # assign Guest if unset, then use
echo ${name:?required}    # error and exit if unset

String-Suche & -Ersetzung

Das /-Muster substituiert; // substituiert alle; # stimmt am Anfang überein; % stimmt am Ende überein. ## und %% sind gierig (längste Übereinstimmung), während # und % nicht-gierig sind. Diese sind unerlässlich für Pfad-Manipulation: ${path##*/} extrahiert den Basename (wie der basename-Befehl), und ${path%/*} extrahiert das Verzeichnis (wie dirname). Das Beherrschen dieser vermeidet viele Aufrufe externer Befehle wie sed und cut.

bash
s="Hello World Hello"

# replace first occurrence
echo ${s/Hello/Hi}        # Hi World Hello

# replace all occurrences
echo ${s//Hello/Hi}       # Hi World Hi

# replace at beginning / end
echo ${s/#Hello/Hi}       # Hi World Hello (prefix)
echo ${s/%Hello/Hi}       # Hello World Hello (no suffix match)

# delete patterns (replace with empty)
echo ${s//Hello/}         #  World 
path="/usr/local/bin/app"
echo ${path##*/}          # app (longest prefix)
echo ${path%/*}           # /usr/local/bin (shortest suffix)

# extract filename and extension
file="report.tar.gz"
echo ${file%.*}           # report.tar (remove ext)
echo ${file##*.}          # gz (keep ext)

Quoting & Sonderzeichen

Einfache Anführungszeichen erhalten alles wörtlich — keine Variablen-Expansion, keine Escape-Verarbeitung (Sie können nicht einmal ein einfaches Anführungszeichen innerhalb einfacher Anführungszeichen einschließen). Doppelte Anführungszeichen erlauben $, Backtick und \-Expansion, während sie Leerzeichen und Sonderzeichen erhalten — fast immer doppelte Anführungszeichen um Variablen bevorzugen, um Word-Splitting- und Globbing-Bugs zu verhindern. Verwenden Sie $'...' für ANSI-C-Escapes wie \n und \t.

bash
# single quotes: literal, no expansion
echo 'Hello $USER'         # Hello $USER

# double quotes: expansion happens
echo "Hello $USER"         # Hello alice

# escape special chars with backslash
echo "Price: \\$5.00"      # Price: $5.00
echo 'It\'s a test'       # It's a test (escape inside single)

# concatenate strings
a="Hello"
b="World"
c="$a, $b!"
echo $c                    # Hello, World!

# multi-line string
msg="Line 1
Line 2
Line 3"
echo "$msg"

Read & Benutzereingabe

read erfasst Benutzereingabe. -p zeigt einen Prompt, -t setzt ein Timeout (gibt non-zero zurück), -a liest in ein Array. Beim Lesen von Dateien Zeile für Zeile verwenden Sie immer 'IFS= read -r' — IFS= erhält führende/nachfolgende Whitespace, und -r deaktiviert Backslash-Escaping, sodass Backslashes wörtlich erhalten bleiben. Dies ist das kanonische sichere Muster zum Verarbeiten von Dateien Zeile für Zeile.

bash
# read a single value
echo -n "Enter your name: "
read name
echo "Hi, $name!"

# read multiple values
read -p "First and last: " first last

# read with timeout (seconds)
read -t 5 -p "Quick! " answer || echo "too slow"

# read into an array
echo "Enter colors:"
read -a colors
echo "First: ${colors[0]}"

# read line by line from a file
while IFS= read -r line; do
    echo ">>> $line"
done < file.txt
02

Arrays

Indizierte Arrays

Bash-Arrays sind null-indiziert. @ und * expandieren zu allen Elementen — zitieren Sie sie immer ("${arr[@]}"), um Elemente mit Leerzeichen korrekt zu behandeln. ${#arr[@]} gibt die Anzahl. += hängt an. unset hinterlässt eine Lücke in den Indizes; zum Neu-Indizieren verwenden Sie arr=("${arr[@]}"). Slicing mit ${arr[@]:start:count} gibt eine Teilmenge zurück.

bash
# create and populate
fruits=("apple" "banana" "cherry")
fruits[3]="date"

# access elements
echo ${fruits[0]}         # apple
echo ${fruits[@]}         # all: apple banana cherry date
echo ${fruits[*]}         # same as @ in most contexts

# count and slice
echo ${#fruits[@]}        # count: 4
echo ${fruits[@]:1:2}     # elements 1-2: banana cherry

# append
fruits+=("elderberry")

# iterate
for f in "${fruits[@]}"; do
    echo "- $f"
done

# delete an element
unset fruits[1]            # removes "banana" (gap remains)

Assoziative Arrays (Maps)

Assoziative Arrays (declare -A) bilden String-Keys auf Werte ab, wie Dictionaries in anderen Sprachen — verfügbar in Bash 4+. Verwenden Sie ${!arr[@]} für Keys und ${arr[@]} für Werte. Der -v-Test prüft, ob ein Key existiert. Im Gegensatz zu indizierten Arrays sind Keys beliebige Strings, daher ist das Quoten von Keys mit Leerzeichen unerlässlich. Diese erfordern Bash 4+ (macOS liefert standardmäßig Bash 3).

bash
# must declare before use (Bash 4+)
declare -A ages

# key-value assignment
ages[alice]=30
ages[bob]=25
ages["carol smith"]=28

# access by key
echo ${ages[alice]}       # 30
echo ${ages[bob]}         # 25

# all keys and values
echo ${!ages[@]}          # keys: alice bob carol smith
echo ${ages[@]}           # values: 30 25 28
echo ${#ages[@]}          # count: 3

# iterate keys
for name in "${!ages[@]}"; do
    echo "$name is ${ages[$name]}"
done

# check if key exists
if [[ -v ages[alice] ]]; then
    echo "alice exists"
fi

Array-Iterationsmuster

Es gibt drei Haupt-Iterationsstile: nach Wert (for x in "${arr[@]}"), nach Index (for i in "${!arr[@]}") und C-Stil. Die nach-Index-Form ist nützlich, wenn Sie die Position benötigen. Zitieren Sie immer "${arr[@]}", sodass Elemente mit Leerzeichen als einzelne Items erhalten bleiben. Das Aufbauen von Arrays mit += in einer Schleife ist die idiomatische Methode, Ergebnisse zu akkumulieren.

bash
arr=(one two three four five)

# iterate by value
for item in "${arr[@]}"; do
    echo "$item"
done

# iterate by index
for i in "${!arr[@]}"; do
    echo "[$i] = ${arr[$i]}"
done

# C-style indexed loop
for ((i=0; i<${#arr[@]}; i++)); do
    echo "${arr[$i]}"
done

# map/transform: build a new array
upper=()
for w in "${arr[@]}"; do
    upper+=("$(echo $w | tr a-z A-Z)")
done
echo "${upper[@]}"

Array aus Kommando-Ausgabe

mapfile/readarray (Bash 4+) liest Zeilen in einem Schritt in ein Array — viel schneller als eine while-Schleife für große Dateien. Um einen begrenzten String zu splitten, setzen Sie IFS und verwenden read -ra. Für Dateinamen (die Leerzeichen oder Zeilenumbrüche enthalten können), verwenden Sie immer find -print0 mit read -d '' zum Splitten an Null-Bytes — das einzige sichere Trennzeichen für Dateinamen. Die < <(...)-Prozess-Substitution speist die Schleife ohne Subshell.

bash
# split a string into an array
csv="a,b,c,d"
IFS=',' read -ra parts <<< "$csv"
echo ${parts[2]}          # c

# read lines of a file into an array
mapfile -t lines < file.txt
echo "${lines[0]}"        # first line
echo "${#lines[@]}"       # line count

# alternative: readarray (same as mapfile)
readarray -t lines2 < file.txt

# from command output (words split on IFS)
files=($(ls *.txt))
echo "${files[@]}"

# safer: read null-delimited output
while IFS= read -r -d '' f; do
    files+=("$f")
done < <(find . -name "*.txt" -print0)

Spezielle Variablen

Diese speziellen Variablen sind unerlässlich für Skripting. $0 ist der Skript-Pfad; $1-$9 sind positionale Argumente (verwenden Sie ${10} für zwei Ziffern). Zitieren Sie immer "$@", um Argumente mit Leerzeichen zu erhalten — "$*" verbindet sie zu einem String. $? gibt den Exit-Status (0 = Erfolg) und ist die Grundlage aller Fehlerprüfung. $$ und $! sind nützlich für PID-Dateien und Prozess-Management. shift verwirft $1 und verschiebt den Rest nach unten.

bash
# positional parameters
echo $0       # script name
echo $1       # first argument
echo $#       # argument count
echo $@       # all arguments (separate words)
echo "$@"     # all arguments (preserves quoting)
echo $*       # all arguments (single string)

# process and shell info
echo $$       # current shell PID
echo $!       # PID of last background command
echo $?       # exit status of last command (0 = success)
echo $-       # current shell option flags
echo $_       # last argument of previous command

# shift arguments
echo "$1 $2"  # a b
shift
echo "$1 $2"  # b c

# iterate all args safely
for arg in "$@"; do
    echo "arg: $arg"
done
03

Kontrollfluss & Tests

If / Elif / Else

[ ] ist der POSIX-Test-Befehl (portabel, aber begrenzt); [[ ]] ist Bashes erweiterte Version mit Pattern-Matching (== mit Wildcards), Regex (=~), && / || Operatoren und keinem Need, Variablen zu quoten. Bevorzugen Sie [[ ]] in Bash-Skripten. Quoten Sie immer Variablen in [ ], um Syntaxfehler mit leeren oder Leerzeichen enthaltenden Werten zu vermeiden. Die -f, -r, -d, -e-Tests prüfen Datei-Existenz und -Attribute.

bash
# basic if
if [ "$age" -ge 18 ]; then
    echo "adult"
elif [ "$age" -ge 13 ]; then
    echo "teen"
else
    echo "child"
fi

# modern [[ ]] test (Bash-specific, safer)
if [[ $name == "Alice" ]]; then
    echo "hi Alice"
fi

# pattern matching in [[ ]]
if [[ $file == *.txt ]]; then
    echo "text file"
fi

# regex matching
if [[ $email =~ ^[a-z]+@[a-z]+.[a-z]+$ ]]; then
    echo "valid email"
fi

# multiple conditions
if [[ -f file.txt && -r file.txt ]]; then
    echo "readable file"
fi

Test-Operatoren

Datei-Tests (-f, -d, -r usw.) prüfen Dateisystem-Attribute. String-Tests verwenden = und != (oder == in [[ ]]); -z prüft auf leer, -n auf nicht-leer. Integer-Vergleiche verwenden -eq, -ne, -lt, -gt, -le, -ge — verwenden Sie NICHT < > für Integer in [ ] (sie leiten um!). In [[ ]] und (( )) können Sie die vertrauten < > <= >= Operatoren verwenden. Das Auswendiglernen dieser Operatoren ist unerlässlich, um korrekte Bedingungsanweisungen zu schreiben.

bash
# file tests
[ -f file ]       # exists and is regular file
[ -d dir ]        # exists and is directory
[ -e path ]       # exists (any type)
[ -r file ]       # readable
[ -w file ]       # writable
[ -x file ]       # executable
[ -s file ]       # exists and is non-empty
[ file1 -nt file2 ]  # newer than
[ file1 -ot file2 ]  # older than

# string tests
[ -z "$s" ]       # empty string
[ -n "$s" ]       # non-empty string
[ "$a" = "$b" ]   # equal (use == in [[ ]])
[ "$a" != "$b" ]  # not equal

# integer tests
[ $n -eq 5 ]      # equal
[ $n -ne 5 ]      # not equal
[ $n -lt 5 ]      # less than
[ $n -gt 5 ]      # greater than
[ $n -le 5 ]      # less or equal
[ $n -ge 5 ]      # greater or equal

Case-Anweisung

case ist das Bash-Äquivalent von switch — es gleicht einen Wert gegen Glob-Patterns ab. | trennt Alternativen. ;; beendet einen Branch (wie break). Das *) -Pattern ist der Default. Patterns unterstützen Wildcards (*, ?, [abc]), aber keinen vollständigen Regex. case ist sauberer als lange if-elif-Ketten zum Dispositionieren auf einen einzelnen Wert und ist die Standardmethode, um Kommandozeilen-Subcommands zu erstellen (start/stop/restart).

bash
case $1 in
    start)
        echo "Starting service..."
        systemctl start app
        ;;
    stop)
        echo "Stopping service..."
        systemctl stop app
        ;;
    restart)
        $0 stop
        $0 start
        ;;
    status)
        systemctl status app
        ;;
    --help|-h)
        echo "Usage: $0 {start|stop|restart|status}"
        ;;
    *)
        echo "Unknown command: $1" >&2
        exit 1
        ;;
esac

# pattern matching in case
case $file in
    *.jpg|*.png)    echo "image" ;;
    *.mp3|*.wav)    echo "audio" ;;
    *.txt)          echo "text" ;;
    *)              echo "other" ;;
esac

Arithmetik & (( ))

$(( )) wertet arithmetische Ausdrücke aus und gibt das Ergebnis zurück. Innerhalb (( )) benötigen Variablennamen kein $ — verwenden Sie einfach den Namen. (( )) als Befehl gibt Exit-Status 0 (true) zurück, wenn das Ergebnis non-zero ist, was es ideal für numerische Bedingungen macht. Bash macht nur Integer-Arithmetik; für Gleitkomma verwenden Sie bc oder awk. Operatoren: + - * / % ** und C-Stil-Vergleichs- und Bit-Operatoren funktionieren alle.

bash
# arithmetic expansion
x=5
y=$((x + 3))
echo $y              # 8
echo $((x * 2))      # 10
echo $((2 ** 10))    # 1024 (exponent)
echo $((17 % 5))     # 2 (modulo)
echo $((17 / 5))     # 3 (integer division)

# (( )) condition: no $ needed, returns exit status
x=10
if (( x > 5 && x < 20 )); then
    echo "in range"
fi

# increment / decrement
((x++))             # post-increment
((x--))             # post-decrement
((++x))             # pre-increment

# bitwise operators
echo $((5 & 3))     # 1 (AND)
echo $((5 | 2))     # 7 (OR)
echo $((5 << 1))    # 10 (left shift)

Short-Circuit & Ternär

&& und || sind Short-Circuit-Operatoren, die als präziser Kontrollfluss doppelt fungieren: 'cmd1 && cmd2' führt cmd2 nur aus, wenn cmd1 erfolgreich ist; 'cmd1 || cmd2' führt cmd2 nur aus, wenn cmd1 fehlschlägt. Dies ersetzt einfaches if-else in einer Zeile. Das Muster 'A && B || C' imitiert einen ternären Operator, ist aber subtil fehlerhaft, wenn B fehlschlagen kann — für robusten Code verwenden Sie eine echte if-Anweisung. Der Doppelpunkt (:) ist ein No-Op-Befehl, nützlich für leere Branches.

bash
# && and || as short-circuit control flow
[[ -f file.txt ]] && echo "exists"
[[ -f file.txt ]] || echo "missing"

# combined: do this OR fail
mkdir -p build && cd build && make || exit 1

# ternary-like (Bash doesn't have a real ternary)
result=$([[ $x -gt 0 ]] && echo "positive" || echo "non-positive")

# default value via &&
echo ${name:-"anonymous"}

# run command, fallback on failure
ping -c1 host && echo "up" || echo "down"

# null command as no-op
if condition; then
    :    # do nothing
fi
04

Schleifen & Iteration

For-Schleifen

Die for-Schleife iteriert über eine Liste von Wörtern. Brace-Expansion {1..5} generiert Sequenzen (mit optionalem Schritt {start..end..step}). Die C-Stil-for ((init; cond; update)) ist am besten für numerische Zähler. Beim Iterieren von Dateien mit *.txt expandieren unquoted Globs sicher, aber wenn keine Dateien übereinstimmen, erhalten Sie das Literal '*.txt' — aktivieren Sie shopt -s nullglob, um stattdessen eine leere Liste zu erhalten.

bash
# iterate a list
for fruit in apple banana cherry; do
    echo "$fruit"
done

# iterate an array
colors=("red" "green" "blue")
for c in "${colors[@]}"; do
    echo "Color: $c"
done

# range with brace expansion
for i in {1..5}; do
    echo "Number: $i"
done

# range with step
for i in {0..20..2}; do echo $i; done    # 0 2 4 ... 20

# C-style for loop
for ((i=0; i<5; i++)); do
    echo "Iteration $i"
done

# iterate command output
for file in *.txt; do
    echo "Processing $file"
done

While & Until-Schleifen

while läuft, solange die Bedingung erfolgreich ist (Exit 0); until läuft, bis sie erfolgreich ist — sie sind Gegensätze. Das 'while read'-Pattern ist die kanonische Methode, Dateien Zeile für Zeile zu verarbeiten; verwenden Sie immer 'IFS= read -r' für Sicherheit. Beachten Sie, dass das Pipen in eine while-Schleife es in einer Subshell ausführt, sodass Variablen-Änderungen nicht persistieren — verwenden Sie Prozess-Substitution '< <(cmd)' stattdessen, wenn Sie Variablen behalten müssen.

bash
# while: runs while condition is true
count=0
while [ $count -lt 5 ]; do
    echo "Count: $count"
    ((count++))
done

# read lines from a file
while IFS= read -r line; do
    echo "Line: $line"
done < input.txt

# infinite loop with break
while true; do
    read -p "Quit? (y/n) " ans
    [[ $ans == "y" ]] && break
done

# until: runs until condition is true (opposite of while)
n=0
until [ $n -ge 3 ]; do
    echo "n=$n"
    ((n++))
done

# while reading from a pipeline
seq 1 5 | while read n; do
    echo "got $n"
done

Break, Continue & Select

break verlässt die Schleife (break N verlässt N verschachtelte Schleifen); continue springt zur nächsten Iteration. select erstellt ein interaktives nummeriertes Menü aus einer Liste — es schleift, bis es gebrochen wird. Diese Steueranweisungen funktionieren in for-, while- und until-Schleifen. Die break N-Form ist unerlässlich zum Verlassen tief verschachtelter Schleifen, sollte aber sparsam verwendet werden, da sie Code schwerer lesbar machen kann.

bash
# break exits the loop
for i in {1..10}; do
    [[ $i -eq 5 ]] && break
    echo $i       # prints 1 2 3 4
done

# continue skips to next iteration
for i in {1..6}; do
    (( i % 2 == 0 )) && continue
    echo $i       # prints 1 3 5 (odd only)
done

# break out of nested loops
for i in 1 2 3; do
    for j in a b c; do
        [[ $j == "b" ]] && break 2   # break both loops
        echo "$i$j"
    done
done

# select menu
select opt in "Start" "Stop" "Quit"; do
    case $opt in
        Start) echo "starting" ;;
        Stop)  echo "stopping" ;;
        Quit)  break ;;
        *)     echo "invalid" ;;
    esac
done

Sicheres Iterieren über Dateien

Das Parsen von ls-Ausgabe ist ein klassischer Bug — Dateinamen mit Leerzeichen, Zeilenumbrüchen oder Sonderzeichen brechen es. Verwenden Sie Shell-Globs (*.txt) direkt in for-Schleifen und quoten Sie die Variable. Für rekursive oder komplexe Suchen verwenden Sie find -print0 gepiped zu 'read -d ''' — Null-Bytes sind das einzige sichere Trennzeichen für Dateinamen. Aktivieren Sie nullglob, sodass nicht übereinstimmende Globs eine leere Liste statt des literalen Patterns erzeugen.

bash
# BAD: breaks on filenames with spaces
for f in $(ls *.txt); do
    cat "$f"
done

# GOOD: glob expands safely
for f in *.txt; do
    [[ -f "$f" ]] || continue
    cat "$f"
done

# BEST: handle spaces, newlines, and no matches
shopt -s nullglob dotglob
for f in *.txt; do
    echo "Processing: $f"
done

# find with -print0 and while read -d ''
while IFS= read -r -d '' f; do
    echo "Found: $f"
done < <(find . -type f -name "*.txt" -print0)

# process files modified today
for f in $(find . -mtime -1 -print); do
    echo "Recent: $f"
done

Bereiche & Sequenz-Generierung

Brace-Expansion {a..b} generiert Sequenzen zur Parse-Zeit — schnell und eingebaut. Sie unterstützt Buchstaben, Zahlen, Zero-Padding und Schritte ({start..end..step}). seq ist ein externer Befehl mit mehr Formatierungsoptionen (-f für printf-Stil). Brace-Expansion erstellt auch mehrere Argumente: echo file{1..3}.{txt,log} generiert 6 Dateinamen. Bevorzugen Sie Brace-Expansion für einfache Bereiche; verwenden Sie seq, wenn Sie benutzerdefinierte Formatierung benötigen.

bash
# brace expansion (not a loop, but generates lists)
echo {1..10}                 # 1 2 3 4 5 6 7 8 9 10
echo {a..e}                  # a b c d e
echo {01..10}                # zero-padded: 01 02 ... 10
echo file{1..3}.txt          # file1.txt file2.txt file3.txt

# seq command
seq 1 5                      # 1 2 3 4 5
seq 1 2 10                   # 1 3 5 7 9 (step 2)
seq -f "%03d" 1 5            # 001 002 003 004 005

# use seq in a for loop
for i in $(seq 1 5); do
    echo $i
done

# generate a list of dates
for d in $(seq -f "2024-01-%02g" 1 31); do
    echo "$d"
done
05

Funktionen

Funktionen definieren & aufrufen

Funktionen in Bash werden mit name() oder function name definiert. Rufen Sie sie beim Namen mit leerzeichengetrennten Argumenten auf (keine Klammern). Verwenden Sie IMMER 'local' für Variablen innerhalb von Funktionen, um den globalen Scope nicht zu verschmutzen — ohne es lecken Zuweisungen heraus. Bash-Funktionen können nicht direkt Werte zurückgeben (return ist ein Exit-Status 0-255); um einen String zurückzugeben, echoen Sie ihn und erfassen Sie ihn mit $().

bash
# two equivalent syntaxes
greet() {
    echo "Hello, $1!"
}
function greet2() {
    echo "Hi, $1!"
}

# call with arguments (no parens)
greet "Alice"               # Hello, Alice!
greet2 "Bob"                # Hi, Bob!

# local variables (function-scoped)
counter() {
    local count=0
    ((count++))
    echo $count
}

# return a value via echo + command substitution
add() {
    echo $(($1 + $2))
}
result=$(add 3 4)           # 7

Argumente & Parameter

Innerhalb einer Funktion referenzieren $1, $@, $# die Argumente der Funktion, nicht die des Skripts — sie shadowen die Skript-Level-Parameter. $FUNCNAME hält den Namen der Funktion (nützlich zum Debuggen). shift innerhalb einer Funktion betrifft nur die Parameter der Funktion. Um die Argumente des Skripts an eine Funktion zu übergeben, verwenden Sie func "$@". Dieses Shadowing ist der Grund, warum Funktionen wiederverwendbare Bausteine sind.

bash
# all positional params work inside functions
show_args() {
    echo "Function name: $FUNCNAME"
    echo "First arg:  $1"
    echo "Second arg: $2"
    echo "All args:   $@"
    echo "Arg count:  $#"
}

show_args a b c
# Function name: show_args
# First arg:  a
# Second arg: b
# All args:   a b c
# Arg count:  3

# pass all script args to a function
process "$@"

# shift within a function (local to it)
parse() {
    while (($#)); do
        echo "arg: $1"
        shift
    done
}

Rückgabewerte & Exit-Status

Bashes 'return' setzt nur einen Exit-Status (0-255), sodass Funktionen als Boolean-Tests doppelt fungieren. Um einen String zurückzugeben, echoen Sie ihn und erfassen Sie ihn mit $(). Für mehrere Werte verwenden Sie eine Nameref (local -n) — Bash 4.3+ — die es der Funktion ermöglicht, per Name in die Variable des Aufrufers zu schreiben. Dies ist die sauberste Methode, komplexe Daten zurückzugeben. Vermeiden Sie globale Variablen für Rückgabewerte, da sie Funktionen nicht reentrant machen.

bash
# return sets exit status (0-255), not a value
is_even() {
    if (($1 % 2 == 0)); then
        return 0    # true/success
    else
        return 1    # false/failure
    fi
}

# use as a condition
if is_even 4; then
    echo "4 is even"
fi

# chain with && and ||
is_even 4 && echo "yes" || echo "no"

# return a string via echo (capture with $())
to_upper() {
    echo "${1^^}"
}
UP=$(to_upper hello)        # HELLO

# return multiple values via global or nameref
set_pair() {
    local -n _r=$1
    _r=(10 20)
}
set_pair result
echo "${result[@]}"       # 10 20

Variablen-Scope

Variablen in Bash sind standardmäßig global — selbst wenn sie innerhalb einer Funktion zugewiesen werden! Dies ist eine häufige Fehlerquelle. Verwenden Sie immer 'local' für funktionsinterne Variablen. 'declare -g' erstellt explizit eine Globale aus einer Funktion heraus. Rekursion funktioniert, ist aber langsam in Bash (jeder Aufruf forked eine Subshell für $()); verwenden Sie sie sparsam. Das Factorial-Beispiel zeigt das Muster: lokale Variablen + rekursiver Aufruf + Arithmetik.

bash
# global by default
x=1
modify() {
    x=2          # changes the GLOBAL x!
    local y=3    # local to function
    echo "inside: x=$x y=$y"
}

modify
echo "outside: x=$x"       # x=2 (modified!)
echo "outside: y=$y"       # empty (y is local)

# declare -g for global inside a function
set_global() {
    declare -g NEW_VAR=42
}
set_global
echo $NEW_VAR              # 42

# recursion with local
factorial() {
    local n=$1
    if (( n <= 1 )); then
        echo 1
    else
        local prev=$(factorial $((n - 1)))
        echo $((n * prev))
    fi
}
echo $(factorial 5)        # 120

Bibliotheken & Sourcing

source (oder .) führt eine Datei in der aktuellen Shell aus, sodass ihre Funktionen und Variablen verfügbar werden — so bauen Sie wiederverwendbare Bibliotheken. Ein häufiges Muster ist eine utils.sh mit Hilfsfunktionen, die Skripte sourcen. Anders als das Ausführen eines Skripts (das in einer Subshell läuft), kann gesourceter Code die Umgebung des Aufrufers modifizieren. So funktionieren auch .bashrc und .bash_profile — sie werden beim Shell-Start gesourced.

bash
# utils.sh — a reusable library
#!/bin/bash
log() {
    echo "[$(date +%H:%M:%S)] $*" >&2
}

die() {
    log "ERROR: $*"
    exit 1
}

confirm() {
    read -p "$1 [y/N] " ans
    [[ $ans =~ ^[Yy]$ ]]
}

# main.sh — use the library
source ./utils.sh        # or . ./utils.sh

log "Starting script"
confirm "Continue?" || die "aborted"
log "Done"

# source vs execute
# source: runs in current shell (shares variables)
# ./script.sh: runs in a subshell (isolated)
06

Text-Verarbeitung

grep — Pattern-Suche

grep findet Zeilen, die einem Pattern entsprechen. -i ignoriert Groß-/Kleinschreibung, -v invertiert, -n zeigt Zeilennummern, -r rekursiert, -E verwendet erweiterten Regex (wie +, |, {}). -A/-B/-C zeigen Kontextzeilen um Treffer — unschätzbar zum Verstehen von Fehlern. Verwenden Sie --include, um Dateitypen während der rekursiven Suche zu filtern. grep gibt Exit-Status 0 zurück, wenn ein Treffer gefunden wird, was es in Bedingungen nützlich macht: if grep -q pattern file; then...

bash
# basic search
grep "error" app.log
grep -i "error" app.log          # case-insensitive
grep -v "debug" app.log          # invert (non-matching lines)
grep -c "error" app.log          # count matches
grep -n "error" app.log          # line numbers
grep -w "error" app.log          # whole word only

# recursive search
grep -rn "TODO" src/             # recursive + line numbers
grep -rl "TODO" src/             # files with matches only

# extended regex (or use egrep)
grep -E "error|warn|fatal" log
grep -E "^[0-9]{4}-" log         # lines starting with date

# context lines
grep -A 2 "error" log            # 2 lines after
grep -B 2 "error" log            # 2 lines before
grep -C 2 "error" log            # 2 lines before and after

# search multiple files
grep "function" *.js --include="*.js" -r .

sed — Stream-Editor

sed bearbeitet Text-Streams nicht-interaktiv. Der s-Befehl substituiert; g macht es global. -i bearbeitet in Place (immer zuerst ohne -i testen!). Das Trennzeichen kann ein beliebiges Zeichen sein — verwenden Sie | oder # für Pfade, um Slashes zu escapen. sed verarbeitet Zeile für Zeile, sodass Multi-Zeilen-Operationen den N-Befehl oder Hold-Space benötigen. Für komplexe Bearbeitungen können awk oder perl klarer sein. Quoten Sie sed-Skripte immer, um Shell-Expansion zu verhindern.

bash
# substitute (replace)
sed 's/old/new/' file.txt           # first occurrence per line
sed 's/old/new/g' file.txt          # all occurrences (global)
sed 's/old/new/3' file.txt          # 3rd occurrence only
sed 's/old/new/gi' file.txt         # global + case-insensitive

# in-place editing
sed -i 's/foo/bar/g' file.txt       # edit file in place
sed -i.bak 's/foo/bar/g' file.txt   # keep a backup

# delete lines
sed '/^#/d' file.txt                # delete comment lines
sed '/^$/d' file.txt                # delete blank lines
sed '5d' file.txt                   # delete line 5
sed '5,10d' file.txt                # delete lines 5-10

# print specific lines
sed -n '10,20p' file.txt            # print lines 10-20
sed -n '/pattern/p' file.txt        # print matching lines

# use a different delimiter for paths
sed 's|/usr/local|/opt|g' paths.txt

# multiple commands
sed -e 's/a/A/g' -e 's/b/B/g' file.txt

awk — Spalten-Verarbeitung

awk ist eine Mini-Programmiersprache für spaltenorientierte Daten. $1, $2... sind Felder; $0 ist die ganze Zeile; $NF ist das letzte Feld. -F setzt den Eingabe-Separator; OFS setzt die Ausgabe. BEGIN läuft vor der Verarbeitung, END danach. NR ist die Datensatz-(Zeilen-)Nummer; NF ist die Feld-Anzahl. awk ist ideal für CSV/TSV-Verarbeitung, Log-Analyse und Report-Generierung — viel mächtiger als cut für alles jenseits einfacher Extraktion.

bash
# print columns (default separator: whitespace)
awk '{print $1}' file.txt           # first column
awk '{print $1, $3}' file.txt       # columns 1 and 3
awk '{print $NF}' file.txt          # last column
awk -F: '{print $1}' /etc/passwd    # split on :

# filter and print
awk '$3 > 100' file.txt             # lines where col 3 > 100
awk '$1 == "ERROR" {print $2}' log  # col 2 of ERROR lines
awk 'NR == 5' file.txt              # 5th line only
awk 'NR >= 10 && NR <= 20' file.txt # lines 10-20

# BEGIN and END blocks
awk 'BEGIN {print "Start"} {sum += $1} END {print sum}' nums.txt

# field separator and output
awk -F, 'BEGIN {OFS="|"} {print $1, $2}' csv.txt

# count lines matching a pattern
awk '/error/' file.txt              # like grep
awk '/error/ {count++} END {print count}' log

# built-in variables: NR (row), NF (fields), FS, OFS
awk '{print NR, NF, $0}' file.txt

cut, tr, sort & uniq

cut ist einfache Feld/Zeichen-Extraktion — schnell, aber begrenzt (kein Quoting-Support). tr übersetzt Zeichen (großartig für Groß-/Kleinschreibungs-Konvertierung oder Trennzeichen-Wechsel) und -d löscht. sort ordnet Zeilen (-n numerisch, -r umgekehrt, -k Feld). uniq entfernt nur ANGRENDENDE Duplikate, daher immer zuerst durch sort pipen. uniq -c mit sort -rn ist das klassische Muster für Häufigkeitsanalyse: 'sort | uniq -c | sort -rn' zeigt die häufigsten Zeilen.

bash
# cut: extract fields or characters
cut -d: -f1 /etc/passwd            # field 1, delimiter :
cut -c1-5 file.txt                 # characters 1-5
cut -d, -f2,3 data.csv             # fields 2 and 3

# tr: translate or delete characters
echo "Hello" | tr 'a-z' 'A-Z'      # HELLO
echo "a,b,c" | tr ',' ' '          # a b c
echo "hello" | tr -d 'l'           # heo (delete)
tr -s ' ' < file.txt               # squeeze repeated spaces

# sort lines
sort file.txt                      # alphabetical
sort -n nums.txt                   # numeric
sort -rn nums.txt                  # reverse numeric
sort -t: -k3 -n /etc/passwd        # by 3rd field, numeric
sort -u file.txt                   # sort + unique

# uniq: filter or count adjacent duplicates
sort file.txt | uniq               # unique lines
sort file.txt | uniq -c            # count occurrences
sort file.txt | uniq -d            # only duplicates
sort file.txt | uniq -u            # only unique lines

Pipes & Umleitung

Pipes verbinden stdout mit stdin und bauen mächtige Pipelines. > leitet stdout um (>> hängt an), 2> leitet stderr um, &> erfasst beide. Here-Docs (<<EOF) speisen Multi-Zeilen-Strings; das Quoten des Trennzeichens ('EOF') deaktiviert Variablen-Expansion. Prozess-Substitution <(cmd) behandelt die Ausgabe eines Befehls als temporäre Datei — unerlässlich zum Speisen von while-Schleifen ohne Subshell und für Befehle wie diff, die Dateien erwarten.

bash
# pipe: chain commands (stdout of one -> stdin of next)
cat file | grep "x" | sort | uniq -c | sort -rn | head

# redirect stdout
echo "hello" > file.txt            # overwrite
echo "world" >> file.txt           # append

# redirect stderr
command 2> error.log               # stderr to file
command 2>&1                       # stderr to stdout
command > all.log 2>&1             # both to same file
command &> all.log                 # both (Bash shortcut)

# stdin from file
command < input.txt

# here-string
grep "x" <<< "some text with x"

# here-document (multi-line input)
cat << EOF
Line 1
Line 2 with $VAR expansion
EOF

# quoted delimiter disables expansion
cat << 'EOF'
Literal $VAR no expansion
EOF

# process substitution
diff <(ls dir1) <(ls dir2)         # compare outputs
while read line; do echo "$line"; done < <(grep x file)
07

Datei- & Verzeichnis-Operationen

find — Datei-Suche

find ist das mächtigste Datei-Such-Tool. -name stimmt mit Globs überein (verwenden Sie -iname für Groß-/Kleinschreibung-insensitiv); -type filtert nach f/d/l; -mtime/-mmin filtern nach Änderungszeit (- = innerhalb, + = älter als); -size filtert nach Größe. -exec führt einen Befehl auf jedem Ergebnis aus; {} ist der Dateiname, \; führt pro Datei aus, + batcht sie. -delete ist sicherer als -exec rm zum Löschen. Testen Sie find immer zuerst ohne -delete!

bash
# search by name
find . -name "*.js"
find . -iname "*.JS"               # case-insensitive
find / -name "*.conf" 2>/dev/null  # suppress permission errors

# search by type
find . -type f -name "*.txt"       # files only
find . -type d -name src           # directories only
find . -type l                     # symlinks

# search by time and size
find . -mtime -7                   # modified < 7 days ago
find . -mtime +30                  # modified > 30 days ago
find . -mmin -60                   # modified < 60 min ago
find . -size +10M                  # larger than 10MB
find . -size -1k                   # smaller than 1KB
find . -empty                      # empty files/dirs

# act on results (-exec)
find . -name "*.log" -exec rm {} \;
find . -name "*.js" -exec wc -l {} +
find . -type f -exec chmod 644 {} \;

# safe deletion with -delete
find /tmp -type f -mtime +7 -delete

Berechtigungen & Eigentümerschaft

Berechtigungen sind drei Tripletts: Eigentümer, Gruppe, Andere. Jede Ziffer ist r(4)+w(2)+x(1), also 755 = rwxr-xr-x. chmod ändert Berechtigungen; chown ändert Eigentümer/Gruppe. Symbolische Notation (u+x, g-w) ist klarer für inkrementelle Änderungen. umask setzt Standard-Berechtigungen für neue Dateien (von 666 für Dateien, 777 für Verzeichnisse abgezogen). Für Webserver sind 755 für Verzeichnisse und 644 für Dateien Standard; verwenden Sie nie 777 in Produktion.

bash
# view permissions
ls -l file.txt
# -rw-r--r-- 1 user group 1024 Jan 1 10:00 file.txt
#  ^^^ ^^^ ^^^
#  owner group others

# chmod: change permissions
chmod 755 script.sh        # rwxr-xr-x
chmod 644 file.txt         # rw-r--r--
chmod +x script.sh         # add execute for all
chmod u+x,g-w file.txt     # symbolic: user +x, group -w
chmod -R 755 directory/    # recursive

# chown: change owner
chown alice file.txt
chown alice:developers file.txt
chown -R alice:developers project/

# umask: default permissions for new files
umask                      # show current (e.g. 022)
umask 077                  # new files: 600, dirs: 700

# numeric permission reference:
# 7=rwx 6=rw 5=rx 4=r 3=wx 2=w 1=x 0=none

Kopieren, Verschieben, Entfernen & Verlinken

cp -r kopiert Verzeichnisse rekursiv; -i fragt vor Überschreiben (sicherer); -p erhält Zeitstempel und Berechtigungen. mv ist sowohl Verschieben als auch Umbenennen. rm -rf ist gefährlich — es löscht rekursiv ohne Nachfrage; prüfen Sie immer den Pfad. Hardlinks zeigen auf dasselbe Inode (dieselbe Datei, kann nicht Dateisysteme übergreifen oder Verzeichnisse verlinken); Symlinks sind Pfad-Referenzen (können alles verlinken, brechen aber, wenn das Ziel verschoben wird). Verwenden Sie ln -s für Symlinks.

bash
# copy
cp file.txt backup.txt
cp -r src/ dest/            # recursive (directories)
cp -i file.txt dest/        # interactive (prompt before overwrite)
cp -u file.txt dest/        # update (only if newer)
cp -p file.txt dest/        # preserve attributes

# move / rename
mv old.txt new.txt
mv file.txt /other/dir/
mv -i file.txt dest/        # prompt before overwrite

# remove
rm file.txt
rm -r directory/            # recursive
rm -f file.txt              # force (no error if missing)
rm -rf node_modules/        # force + recursive (DANGER)

# links
ln target hardlink          # hard link (same inode)
ln -s target symlink        # symbolic link (path reference)
ln -sf target symlink       # force recreate symlink

# view link target
readlink symlink
readlink -f symlink         # canonical absolute path

Archive & Kompression

tar bündelt Dateien in ein Archiv; gzip/bzip2/xz komprimieren es. Die Flags: c=create, x=extract, t=list, f=file, z=gzip, j=bzip2, J=xz, v=verbose. .tar.gz ist der Unix-Standard; .zip ist häufig auf Windows. bzip2 komprimiert besser als gzip, aber langsamer; xz ist am besten, aber am langsamsten. Verwenden Sie -C zum Extrahieren in ein spezifisches Verzeichnis. Das -k-Flag erhält das Original beim Komprimieren.

bash
# tar + gzip (most common)
tar -czf archive.tar.gz dir/      # create
tar -xzf archive.tar.gz           # extract
tar -xzf archive.tar.gz -C /opt/  # extract to /opt
tar -tzf archive.tar.gz           # list contents

# tar + bzip2 (better compression)
tar -cjf archive.tar.bz2 dir/
tar -xjf archive.tar.bz2

# zip / unzip
zip -r archive.zip dir/
unzip archive.zip
unzip archive.zip -d /target

# gzip / gunzip (single files)
gzip file.txt           # -> file.txt.gz
gunzip file.txt.gz      # -> file.txt
gzip -k file.txt        # keep original

# xz (best compression)
tar -cJf archive.tar.xz dir/
tar -xJf archive.tar.xz

# mnemonic: eXtract, Create, List, File, gZip, bZip2, xz

Datei-Inhalt & Vergleich

cat gibt die ganze Datei aus; head/tail zeigen Enden; tail -f streamt Live-Updates (unerlässlich für Logs). less ist ein interaktiver Pager mit Suche (/) und Navigation. diff vergleicht Dateien; -u produziert das Unified-Format, das von patch verwendet wird. comm vergleicht zwei sortierte Dateien und zeigt Zeilen, die nur in jeder oder in beiden gemeinsam sind — nützlich zum Vergleichen von Listen. Sortieren Sie immer Eingaben vor comm, da es sortierte Dateien erfordert.

bash
# view file contents
cat file.txt               # entire file
head -n 20 file.txt        # first 20 lines
tail -n 20 file.txt        # last 20 lines
tail -f app.log            # follow (live updates)
less file.txt              # pager (q to quit, / to search)

# line/word/char count
wc file.txt                # lines words bytes
wc -l file.txt             # lines only
wc -w file.txt             # words only

# compare files
diff file1.txt file2.txt
diff -u file1.txt file2.txt    # unified format (for patches)
diff -r dir1/ dir2/            # recursive

# patch
diff -u old new > change.patch
patch < change.patch
patch -R < change.patch        # reverse

# find duplicate lines across files
comm -12 <(sort a) <(sort b)   # lines in both
comm -23 <(sort a) <(sort b)   # only in a
comm -13 <(sort a) <(sort b)   # only in b
08

Prozess-Management & Signale

Hintergrund-Jobs & Job-Kontrolle

Ein angehängtes & führt einen Befehl im Hintergrund aus und kehrt sofort zurück. jobs listet aktive Jobs; fg/bg bewegen sie zwischen Vordergrund und Hintergrund. Ctrl+Z suspendiert den Vordergrund-Job (sendet SIGTSTP). wait blockiert, bis Hintergrund-Jobs fertig sind — unerlässlich für Skripte, die parallele Arbeit starten. disown entfernt einen Job aus der Job-Tabelle der Shell, sodass er nach dem Ausloggen weiterläuft (im Gegensatz zu nohup funktioniert es auf bereits laufenden Jobs).

bash
# run in background (append &)
sleep 100 &
# [1] 12345   (job 1, PID 12345)

# list jobs
jobs
jobs -l            # with PIDs

# bring to foreground / send to background
fg                 # foreground the current job
fg %1              # foreground job 1
bg %2              # background job 2 (continue running)

# suspend a running foreground job: Ctrl+Z
# then resume in background:
bg

# wait for background jobs
sleep 5 &
wait                # wait for all background jobs
wait $!             # wait for the last background job
wait 12345          # wait for specific PID

# disown: detach from shell (survives logout)
long_task &
disown %1

kill & Signale

kill sendet Signale an Prozesse. SIGTERM (Standard) bittet den Prozess, elegant zu beenden — er kann aufräumen. SIGKILL (-9) ist fordernd und sofort; der Prozess kann es nicht fangen oder ignorieren, daher kann er Ressourcen in einem schlechten Zustand hinterlassen. Versuchen Sie immer zuerst SIGTERM, warten Sie, dann SIGKILL nur wenn nötig. killall/pkill killen nach Namen. pkill -f stimmt mit der vollständigen Kommandozeile überein (flexibler). Verwenden Sie kill -l, um alle Signal-Namen aufzulisten.

bash
# send signals to processes
kill 12345              # SIGTERM (graceful, default)
kill -15 12345          # SIGTERM explicitly
kill -9 12345           # SIGKILL (force, cannot be caught)
kill -HUP 12345         # SIGHUP (reload config)

# kill by name
killall nginx           # kill all nginx processes
pkill -f "python app.py"  # match full command line

# common signals
# SIGTERM (15) - graceful termination (default)
# SIGKILL (9)  - force kill (cannot be caught/ignored)
# SIGINT (2)   - interrupt (Ctrl+C)
# SIGHUP (1)   - hangup (often reload config)
# SIGSTOP (19) - pause (cannot be caught)
# SIGCONT (18) - resume
# SIGTSTP (20) - terminal stop (Ctrl+Z)

# list all signals
kill -l

# priority: try SIGTERM first, SIGKILL as last resort
kill 12345; sleep 2; kill -9 12345 2>/dev/null

trap — Signal-Behandlung

trap registriert Befehle, die ausgeführt werden, wenn das Skript ein Signal empfängt — unerlässlich für Cleanup. Das Pseudo-Signal EXIT feuert bei jedem Exit (normal, Fehler oder gekillt), was es perfekt zum Entfernen temporärer Dateien macht. Setzen Sie immer Traps, bevor Sie die Ressourcen erstellen, die sie bereinigen. Häufiges Muster: trap cleanup EXIT INT TERM. trap '' SIGNAL ignoriert es; trap - SIGNAL stellt den Default wieder her. So stellen robuste Skripte sicher, dass selbst bei Unterbrechung aufgeräumt wird.

bash
# clean up on exit (EXIT signal)
trap 'rm -f $TMPFILE' EXIT
TMPFILE=$(mktemp)
echo "working with $TMPFILE"
# when script exits (normally or via error), TMPFILE is removed

# handle Ctrl+C (SIGINT)
trap 'echo "Caught Ctrl+C, exiting"; exit 130' INT

# handle multiple signals
cleanup() {
    echo "Cleaning up..."
    rm -rf "$WORKDIR"
    exit
}
trap cleanup INT TERM EXIT

# ignore a signal
trap '' INT              # ignore Ctrl+C
trap - INT               # restore default behavior

# reload on SIGHUP
trap 'echo "reloading config"; source config.sh' HUP

# list current traps
trap -p

Prozess-Inspektion

ps zeigt einen Snapshot von Prozessen; top/htop zeigen Echtzeit. pstree zeigt die Eltern-Kind-Hierarchie. pgrep findet PIDs nach Namen (sicherer als ps|grep). lsof -i :PORT findet, welcher Prozess einen Port verwendet — unerlässlich zum Debuggen von 'Port in use'-Fehlern. ss (Socket-Statistiken) ist der moderne Ersatz für netstat. Das ps aux | grep-Muster ist allgegenwärtig, aber pgrep ist sauberer. Verwenden Sie --sort, um Ressourcenfresser zu finden.

bash
# list processes
ps aux                   # all processes (BSD style)
ps -ef                   # all processes (System V style)
ps -ef | grep nginx      # find nginx processes

# interactive viewer (like Task Manager)
top                      # real-time, all processes
htop                     # nicer interactive viewer (install separately)

# process tree
pstree                   # tree view
pstree -p                # with PIDs

# find process by name/port
pgrep -f "node app"      # print PIDs matching
pgrep -fl "node"         # full command line

# who's listening on a port
lsof -i :8080            # process using port 8080
ss -tlnp                 # all listening TCP ports
netstat -tlnp            # legacy alternative

# process resource usage
top -b -n 1 | head       # snapshot
ps aux --sort=-%mem | head  # top memory users

nohup, disown & tmux

nohup lässt einen Prozess SIGHUP ignorieren, sodass er das Ausloggen überlebt — Output geht an nohup.out. disown erreicht dasselbe für einen bereits laufenden Hintergrund-Job. setsid startet einen Prozess in einer neuen Session und koppelt ihn vollständig ab. Für langlaufende interaktive Arbeit sind tmux oder screen besser: Sie halten eine vollständige Terminal-Session am Leben, an die Sie sich später wieder anhängen können, sodass selbst Texteditoren Verbindungsabbrüche überleben. So verwalten Sysadmins Remote-Server.

bash
# nohup: run immune to hangups (survives logout)
nohup ./long_script.sh &
# output goes to nohup.out by default
nohup ./server.sh > server.log 2>&1 &

# disown: detach an already-running job
./long_task &
disown %1

# setsid: start in a new session
setsid ./daemon.sh &

# tmux: terminal multiplexer (persistent sessions)
tmux                    # start new session
tmux new -s work        # named session
tmux ls                 # list sessions
tmux attach -t work     # reattach
# inside tmux: Ctrl+B then D to detach
# processes keep running after detach

# screen: alternative to tmux
screen -S work
screen -ls
screen -r work

# check if a process is running
pgrep -x nginx && echo "running" || echo "stopped"
09

Skripting & fortgeschrittene Themen

Shebang & Skript-Struktur

Der Shebang (#!) teilt dem Kernel mit, welchen Interpreter er verwenden soll. #!/usr/bin/env bash ist am portabelsten. Ein gut strukturiertes Skript beginnt mit 'set -euo pipefail' für Sicherheit, definiert usage() und parst Argumente mit getopts (für kurze Flags) oder einer manuellen Schleife (für lange Flags). OPTIND trackt das nächste Argument; shift past geparste Optionen, sodass $1 das erste positionale Argument ist. Diese Struktur macht Skripte robust und benutzerfreundlich.

bash
#!/usr/bin/env bash
# ^ portable shebang (finds bash via PATH)
# #!/bin/bash          # absolute path (common)
# #!/usr/bin/bash       # on some systems
# #!/bin/sh             # POSIX sh (most portable)

set -euo pipefail        # strict mode (see below)

# script metadata
readonly PROGNAME=$(basename "$0")
readonly VERSION="1.0.0"

usage() {
    cat << EOF
Usage: $PROGNAME [OPTIONS] FILE
Options:
  -h, --help     Show this help
  -v, --version  Show version
  -o OUTPUT      Output file
EOF
}

# parse options with getopts
while getopts "hvo:" opt; do
    case $opt in
        h) usage; exit 0 ;;
        v) echo $VERSION; exit 0 ;;
        o) output=$OPTARG ;;
        *) usage; exit 1 ;;
    esac
done
shift $((OPTIND - 1))

Strict Mode (set -euo pipefail)

set -e beendet sofort, wenn ein Befehl fehlschlägt (non-zero zurückgibt) — fängt Fehler früh. set -u behandelt Referenzen auf ungesetzte Variablen als Fehler. pipefail lässt eine Pipeline non-zero zurückgeben, wenn IRGENDEIN Befehl darin fehlschlägt (standardmäßig zählt nur der Status des letzten Befehls). Zusammen fangen sie die meisten Bugs. Verwenden Sie 'cmd || true', um erwartete Fehler zu erlauben, und 'if ! cmd' für explizite Prüfungen. Einige Befehle (grep, test) geben legitim non-zero zurück, daher wrappen Sie sie.

bash
# the 'strict mode' trio
set -e             # exit on error (any non-zero status)
set -u             # error on unset variable
set -o pipefail    # pipeline fails if any command fails
# combined: set -euo pipefail

# example: without -e, errors are silently ignored
mkdir /root/nope   # fails (permission denied)
echo "still running"  # this runs anyway!

# with set -e, the script exits at the failure
set -e
mkdir /root/nope   # script exits here
echo "never reached"

# handle expected failures
set -e
if ! grep -q "x" file; then
    echo "not found"   # grep returning 1 won't kill script
fi

# temporarily disable -e
set +e
risky_command
set -e

# || true to ignore a command's failure
mkdir -p dir || true

Debugging-Techniken

set -x (xtrace) gibt jeden Befehl vor der Ausführung aus — das #1-Debugging-Tool. PS4 passt den Prompt an (Zeilennummern hinzuzufügen hilft, Probleme zu lokalisieren). bash -n prüft Syntax ohne Ausführung. trap ERR führt einen Befehl aus, wenn ein Fehler auftritt, perfekt zum Loggen, wo Dinge schiefgingen. Für komplexe Skripte wrappen Sie verdächtige Funktionen mit set -x/+x, um nur diesen Teil zu tracen. Kombinieren Sie mit set -e, um beim ersten Fehler zu stoppen und zu inspizieren.

bash
# trace execution: print each command before running
set -x              # turn on
set +x              # turn off
bash -x script.sh   # run with tracing from the start

# verbose: print input lines as read
set -v

# combine: -xv for full detail
bash -xv script.sh

# print commands with line numbers
export PS4='+ ${BASH_SOURCE}:${LINENO}: '
set -x

# check syntax without executing
bash -n script.sh   # syntax check only

# step through interactively
bash -x script.sh 2>&1 | less

# debug a specific function
debug_func() {
    set -x
    actual_func "$@"
    set +x
}

# use trap ERR to catch errors
trap 'echo "Error on line $LINENO" >&2' ERR

Arithmetik & Mathematik

Bash macht nur Integer-Arithmetik mit $(( )). Für Gleitkomma pipen Sie Ausdrücke an bc (scale=N setzt Dezimalstellen) oder verwenden awk. bc -l lädt die Mathe-Bibliothek (sqrt, sin, cos usw.). $RANDOM gibt eine Pseudo-Zufalls-Integer 0-32767; für kryptografische Zufälligkeit verwenden Sie /dev/urandom. Der (( ))-Befehl unterstützt +=, -=, *=, /=, %= und ++/-- wie C. Für ernsthafte Mathe sind Python oder awk bessere Wahl.

bash
# integer arithmetic
echo $((5 + 3))         # 8
echo $((10 / 3))        # 3 (integer division)
echo $((10 % 3))        # 1 (modulo)
echo $((2 ** 8))        # 256 (exponent)

# floating point with bc
echo "scale=2; 10/3" | bc       # 3.33
echo "3.14 * 2" | bc            # 6.28
result=$(echo "scale=4; $a / $b" | bc)

# floating point with awk
awk 'BEGIN {printf "%.2f\n", 10/3}'   # 3.33
awk "BEGIN {print $a * $b}"

# math functions with bc -l
echo "scale=4; s(0)" | bc -l    # 0 (sine, radians)
echo "scale=4; sqrt(16)" | bc -l # 4.0000

# random numbers
echo $RANDOM              # 0-32767
echo $((RANDOM % 100))    # 0-99
head -c 4 /dev/urandom | od -An -tu4   # 32-bit random

# increment/decrement
n=5
((n++)); echo $n          # 6
((n+=10)); echo $n        # 16

Regex & Pattern-Matching

Bash hat drei Pattern-Systeme: Globs (*.txt, ?, [abc]) für Dateinamen, erweiterte Globs (extglob: !(), @(), *()) für komplexeres Matching und ERE-Regex (=~ in [[ ]]). =~ erfasst Gruppen in BASH_REMATCH (Index 0 = vollständige Übereinstimmung, 1+ = Gruppen). Regex verwendet ERE-Syntax (wie grep -E). Aktivieren Sie extglob für mächtige Negation und Alternation in Dateinamen-Patterns. Regex ist eines der nützlichsten Bash-Features für Eingabe-Validierung.

bash
# glob patterns (filename matching)
*.txt          # any .txt file
file?.txt      # single char: file1.txt, fileA.txt
file[0-9].txt  # digit: file0.txt ... file9.txt
file[!0-9].txt # NOT a digit

# extended globs (shopt -s extglob)
shopt -s extglob
echo !(*.bak)       # everything except .bak files
echo @(*.jpg|*.png) # jpg or png
echo *(foo)         # zero or more "foo"

# regex with =~ in [[ ]]
if [[ $email =~ ^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$ ]]; then
    echo "valid email"
fi

# capture groups with BASH_REMATCH
if [[ $date =~ ^([0-9]{4})-([0-9]{2})-([0-9]{2})$ ]]; then
    echo "Year: ${BASH_REMATCH[1]}"
    echo "Month: ${BASH_REMATCH[2]}"
    echo "Day: ${BASH_REMATCH[3]}"
fi

# case-insensitive regex
shopt -s nocasematch
if [[ $s =~ ^hello ]]; then echo "starts with hello"; fi
10

grep Deep Dive

Basis-grep-Patterns

grep durchsucht Text mit Patterns. -i ignoriert Groß-/Kleinschreibung; -w stimmt mit ganzen Wörtern überein (verhindert das Matchen von 'errors' bei der Suche nach 'error'); -v invertiert (nicht übereinstimmende Zeilen); -c zählt; -n zeigt Zeilennummern; -l listet nur Dateinamen mit Treffern; -h unterdrückt das Dateinamen-Präfix beim Suchen mehrerer Dateien. Standardmäßig verwendet grep Basic Regular Expressions (BRE), wo Metazeichen Backslash-Escaping benötigen. Verwenden Sie -E für Extended Regex (sauberere Syntax) oder -F für feste Strings (kein Regex, schneller für literale Suchen).

bash
# Basic search (literal string)
grep "error" logfile.txt

# Case-insensitive
grep -i "error" logfile.txt

# Whole word match
grep -w "error" logfile.txt

# Invert match (lines NOT matching)
grep -v "debug" logfile.txt

# Count matches
grep -c "error" logfile.txt

# Show line numbers
grep -n "error" logfile.txt

# Multiple files with filename prefix
grep "error" *.log

# Suppress filename prefix (single file behavior)
grep -h "error" *.log

# Only show filenames with matches
grep -l "error" *.log

grep mit Regex

grep -E (oder egrep) verwendet Extended Regular Expressions mit saubererer Syntax: +, ?, |, () funktionieren ohne Escaping. ^ und $ ankern am Zeilenanfang/-ende. [] definiert Zeichenklassen; {} spezifiziert Wiederholung. -P aktiviert Perl-kompatiblen Regex (PCRE) mit Features wie \d, \w, Lookaheads — aber dies ist GNU-spezifisch und nicht portabel. Für komplexen Regex ziehen Sie ripgrep (rg) in Betracht, der schneller ist und standardmäßig PCRE-ähnliche Syntax verwendet. Quoten Sie Patterns immer, um Shell-Interpretation von Sonderzeichen wie $ und * zu verhindern.

bash
# Extended regex (-E or egrep)
grep -E "error|warning|fatal" logfile.txt

# Anchors: ^ start, $ end
grep -E "^ERROR:" logfile.txt      # lines starting with ERROR:
grep -E "completed$" logfile.txt   # lines ending with completed

# Character classes
grep -E "[0-9]{4}-[0-9]{2}-[0-9]{2}" dates.txt  # date format
grep -E "[A-Z][a-z]+" names.txt                  # Capitalized words

# Quantifiers
grep -E "ab+c" file.txt    # one or more b's
grep -E "ab*c" file.txt    # zero or more b's
grep -E "ab?c" file.txt    # zero or one b

# Alternation and grouping
grep -E "(cat|dog) food" file.txt

# PCRE (-P, GNU grep only)
grep -P "\d{3}-\d{4}" phones.txt  # \d for digits

grep-Kontext & Output-Kontrolle

Kontext-Flags (-B, -A, -C) zeigen umgebende Zeilen, unerlässlich zum Verstehen von Log-Einträgen. -o gibt nur den übereinstimmenden Teil aus (nützlich zum Extrahieren von URLs, Zahlen usw.). --color hebt Treffer im Terminal hervor. -r sucht rekursiv (schließt Symlinks aus); -R folgt Symlinks. --include/--exclude/--exclude-dir filtern, welche Dateien durchsucht werden — unschätzbar für große Codebasen (immer node_modules, .git, vendor ausschließen). -a zwingt Binärdateien, als Text behandelt zu werden. Für Code-Suche ist ripgrep (rg) eine moderne, schnellere Alternative mit sinnvollen Defaults.

bash
# Show context: lines before/after match
grep -B 2 "error" log.txt    # 2 lines Before
grep -A 3 "error" log.txt    # 3 lines After
grep -C 2 "error" log.txt    # 2 lines Context (both)

# Only show matched part (not full line)
grep -o "https?://[^ ]+" urls.txt

# Color highlight matches
grep --color=auto "error" log.txt

# Recursive search
grep -r "TODO" ./src/
grep -R "TODO" ./src/        # follows symlinks

# Include/exclude file patterns
grep -r --include="*.py" "import" .
grep -r --exclude="*.test.js" "TODO" .
grep -r --exclude-dir=node_modules "TODO" .

# Binary files
grep -a "pattern" binary_file  # treat as text

grep mit stdin & Pipelines

grep ist in Pipelines am mächtigsten. Der [n]ginx-Trick verhindert, dass grep seinen eigenen Prozess matcht: die Klammer lässt das Pattern nicht den literalen 'grep'-String in der Prozessliste matchen. zgrep durchsucht komprimierte Dateien ohne manuelle Dekomprimierung. pgrep ist ein spezialisierter Prozess-Finder (besser als ps | grep). grep -rl listet Dateien, die ein Pattern enthalten; das Pipen an xargs grep durchsucht diese Dateien nach einem anderen Pattern — eine häufige Code-Archäologie-Technik. Für interaktive Code-Suche verwenden Sie ripgrep oder ack, die für Quellcode entwickelt wurden.

bash
# Search command output
ps aux | grep nginx
ps aux | grep "[n]ginx"  # trick: prevents matching grep itself

# Search compressed logs
zcat log.gz | grep "error"
zgrep "error" log.gz     # direct

# Chain multiple greps
cat log.txt | grep "error" | grep -v "timeout" | grep -c

# Extract and filter
grep -oE "[0-9.]+" response.txt | sort -n | uniq

# Find processes excluding grep
pgrep -f "node server.js"  # better than ps | grep

# Search command history
history | grep "git rebase"

# Find files containing pattern, then search more
grep -rl "config" . | xargs grep "database"

grep Exit-Status & Skripting

greps Exit-Status macht es ideal für Skripting: 0 (Treffer gefunden), 1 (kein Treffer), 2 (Fehler). -q (quiet) unterdrückt Output für reine Bedingungsprüfungen. In set -e-Skripten würde grep-Rückgabe 1 (kein Treffer) das Skript beenden — verwenden Sie '|| true', um dies zu verhindern. Das while-read-Muster verarbeitet übereinstimmende Zeilen einzeln. grep -c gibt eine Anzahl (0 bei keinen Treffern), die Sie numerisch vergleichen können. Diese Skripting-Fähigkeit macht grep zu einem Baustein für Log-Monitoring, Validierungs-Skripte und CI/CD-Checks.

bash
# grep returns exit codes for scripting
# 0 = match found, 1 = no match, 2 = error

if grep -q "error" /var/log/syslog; then
    echo "Errors found!"
    # send alert, etc.
fi

# Silent check (-q), no output
grep -q "^$" file.txt && echo "has blank lines"

# Use in while loop
grep "pattern" file.txt | while read -r line; do
    echo "Found: $line"
done

# Count and branch
errors=$(grep -c "ERROR" log.txt)
if [ "$errors" -gt 10 ]; then
    echo "Too many errors: $errors"
fi

# grep with set -e (exits on non-zero)
set -e
grep "missing" file.txt || true  # prevent exit
11

sed Deep Dive

sed-Substitution

sed (Stream-Editor) transformiert Text Zeile für Zeile. Der s-Befehl substituiert Text: s/pattern/replacement/flags. g (global) ersetzt alle Vorkommen pro Zeile; ohne es wird nur die erste Übereinstimmung ersetzt. Sie können Operationen auf spezifische Zeilen beschränken (nach Nummer, Bereich oder Pattern). -i bearbeitet Dateien in Place (gefährlich — immer zuerst ohne -i testen, oder verwenden Sie -i.bak für ein Backup). sed verarbeitet jede Zeile unabhängig. Das Trennzeichen muss nicht / sein — verwenden Sie s|old|new|g, wenn Patterns Slashes enthalten (z.B. Dateipfade).

bash
# Basic substitution (first occurrence per line)
sed 's/old/new/' file.txt

# Replace all occurrences
sed 's/old/new/g' file.txt

# Case-insensitive (GNU sed)
sed 's/old/new/gi' file.txt

# Replace Nth occurrence
sed 's/old/new/2' file.txt  # 2nd occurrence only

# Only on lines matching pattern
sed '/error/s/old/new/' file.txt

# Only on specific line numbers
sed '5s/old/new/' file.txt      # line 5 only
sed '5,10s/old/new/g' file.txt  # lines 5-10
sed '$s/old/new/' file.txt      # last line

# In-place edit (modifies file)
sed -i 's/old/new/g' file.txt
sed -i.bak 's/old/new/g' file.txt  # keeps backup

sed mit Regex & Capture-Gruppen

sed -E (oder -r) verwendet Extended Regular Expressions mit saubererer Syntax (kein Backslash vor Klammern/Geschweifklammern). Capture-Gruppen werden als \1, \2 usw. in der Ersetzung referenziert. & repräsentiert die vollständige Übereinstimmung. Das Datum-Umformatierungs-Beispiel zeigt die Macht: Jahr, Monat, Tag separat erfassen und neu anordnen. Mehrere Befehle können mit Semikolons verkettet werden (s/.../.../;s/.../.../). Escapen Sie immer Sonderzeichen in Patterns (., *, [, usw.) und escapen Sie / in Ersetzungen (oder verwenden Sie ein anderes Trennzeichen).

bash
# Extended regex (-E or -r)
sed -E 's/[0-9]+/NUMBER/g' file.txt

# Capture groups with \1, \2 (BRE)
sed 's/\(foo\)\(bar\)/\2\1/' file.txt  # swap: foobar -> barfoo

# Capture groups with () (ERE, cleaner)
sed -E 's/(foo)(bar)/\2\1/' file.txt

# Reformat date: 2025-01-15 -> 15/01/2025
sed -E 's/([0-9]{4})-([0-9]{2})-([0-9]{2})/\3\/\2\/\1/' file.txt

# Add prefix to lines
sed 's/^/>> /' file.txt

# Add suffix
sed 's/$/ <<</' file.txt

# Remove leading/trailing whitespace
sed 's/^[ \t]*//;s/[ \t]*$//' file.txt

# Use & for full match
sed 's/[0-9]\{3\}/[&]/g' file.txt  # wrap 3 digits in brackets

sed Delete & Print

Der d-Befehl löscht Zeilen; p druckt Zeilen. Mit -n (kein Auto-Print) produzieren nur explizite p-Befehle Output — dies macht sed zu einem selektiven Drucker (wie grep). Das Löschen leerer Zeilen (sed '/^$/d') ist ein häufiges Cleanup. sed -n '5,10p' ist äquivalent zu sed -n '5,10p' oder head/tail-Kombinationen. Die ~-Syntax (0~3p) druckt jede 3. Zeile (GNU-Erweiterung). Merken Sie: ohne -n druckt sed jede Zeile (möglicherweise modifiziert); mit -n druckt es nichts, es sei denn, Sie verwenden p. Diese Dualität macht sed sowohl zum Editor als auch zum Filter.

bash
# Delete lines
sed '5d' file.txt              # delete line 5
sed '5,10d' file.txt           # delete lines 5-10
sed '/pattern/d' file.txt      # delete matching lines
sed '/^$/d' file.txt           # delete blank lines
sed '/^[ \t]*$/d' file.txt    # delete whitespace-only lines
sed '$d' file.txt              # delete last line

# Print only specific lines (like head/tail)
sed -n '5p' file.txt           # print line 5 only
sed -n '5,10p' file.txt        # print lines 5-10
sed -n '/pattern/p' file.txt   # print matching lines (like grep)
sed -n '5,${/pattern/p}' file.txt  # from line 5 to end

# Print with line numbers
sed '=' file.txt | sed 'N;s/\n/\t/'

# Print every Nth line
sed -n '0~3p' file.txt  # every 3rd line (GNU)

sed Multi-Zeile & Hold-Space

seds Hold-Space ermöglicht Multi-Zeilen-Operationen — der Pattern-Space (aktuelle Zeile) kann zum Hold-Space gespeichert und später abgerufen werden. h/H kopiert/hängt an Hold an; g/G kopiert/hängt von Hold an; x tauscht sie. Der Zeilen-Verbindungs-Trick (:a;N;$!ba;s/\n/ /g) liest die gesamte Datei in den Pattern-Space und ersetzt dann Zeilenumbrüche. N hängt die nächste Zeile an den Pattern-Space an. Diese fortgeschrittenen Features machen sed Turing-vollständig, aber auch sehr kryptisch. Für komplexe Multi-Zeilen-Transformationen sind awk oder Perl lesbarer. Verwenden Sie sed für einfache zeilenbasierte Bearbeitungen; verwenden Sie awk für feldbasierte oder Multi-Zeilen-Logik.

bash
# Join lines (replace newline with space)
sed ':a;N;$!ba;s/\n/ /g' file.txt

# Join every 2 lines
sed 'N;s/\n/ /' file.txt

# Print paragraph (blocks separated by blank lines)
sed '/./{H;d;};x;s/\n/ /g' file.txt

# Reverse line order (tac alternative)
sed '1!G;h;$!d' file.txt

# Double-space a file
sed 'G' file.txt

# Remove last line of each paragraph
sed -n '/^$/{p;h;};/./{H;};/^$/{x;s/\n.*//;p;}' file.txt

# Hold space (h,H,g,G,x) stores lines between cycles
# h: copy pattern space to hold
# H: append pattern space to hold
# g: copy hold to pattern space
# G: append hold to pattern space
# x: exchange pattern and hold

sed in Pipelines & Skripten

sed glänzt in Pipelines zur Text-Transformation. Beim Substituieren von Variablen escapen Sie Sonderzeichen (& und \), um Interpretation zu verhindern. Mehrere -e-Flags wenden mehrere Befehle in Sequenz an. -f liest Befehle aus einer Datei (nützlich für komplexe Skripte). Die CSV-zu-TSV-Konvertierung zeigt eine praktische Anwendung. Für Config-Datei-Bearbeitung in Skripten validieren Sie immer die Änderung (grep nach sed) und ziehen Sie dedizierte Tools in Betracht (jq für JSON, yq für YAML). seds Stärke sind einfache, schnelle, zeilenbasierte Bearbeitungen — es ist ein Grundpfeiler der Unix-Text-Verarbeitung neben grep und awk.

bash
# Clean up output in pipeline
cat messy.txt | sed 's/[ \t]*$//' | sed '/^$/d' | sort

# Extract and transform
echo "name=John;age=30" | sed 's/;/\n/g' | sed 's/=/: /'

# Modify config files in scripts
sed -i "s/PORT=8080/PORT=$NEW_PORT/" config.env

# Escape special chars for safe substitution
ESCAPED=$(printf '%s\n' "$URL" | sed 's/[&\]/\\&/g')
sed "s|URL|$ESCAPED|g" template.txt

# Multiple substitutions
sed -e 's/foo/bar/g' -e 's/baz/qux/g' file.txt

# Read commands from file
sed -f script.sed input.txt

# Convert CSV to TSV
sed 's/,/\t/g' data.csv
12

awk Deep Dive

awk-Grundlagen & Felder

awk splittet automatisch jede Zeile in Felder ($1, $2, ..., $NF). -F setzt den Eingabe-Feld-Separator; OFS setzt den Ausgabe-Separator. $0 ist die gesamte Zeile. NR (Number of Records) ist die Zeilennummer; NF (Number of Fields) ist die Feld-Anzahl für die aktuelle Zeile. awk verarbeitet jede Zeile durch Pattern-Aktion-Paare: pattern { action }. Ohne Pattern läuft die Aktion für jede Zeile. Ohne Aktion wird die Zeile gedruckt. Dies macht awk ideal für spaltenbasierte Datenextraktion aus CSV, TSV, /etc/passwd und Log-Dateien.

bash
# Print specific fields (default delimiter: whitespace)
awk '{print $1}' file.txt        # first field
awk '{print $1, $3}' file.txt    # first and third
awk '{print $NF}' file.txt       # last field
awk '{print $(NF-1)}' file.txt   # second-to-last

# Custom field separator
awk -F: '{print $1, $7}' /etc/passwd   # colon-separated
awk -F',' '{print $2}' data.csv        # CSV
awk -F'\t' '{print $1}' data.tsv      # TSV

# Print entire line ($0)
awk '{print}' file.txt
awk '{print NR, $0}' file.txt  # with line number

# NR = record (line) number
# NF = number of fields in current record
# NR and NF are built-in variables

# Change output separator
awk 'BEGIN{OFS="|"} {print $1, $2, $3}' file.txt

awk-Patterns & Bedingungen

awk-Patterns können Regex (/pattern/), Vergleiche ($3 > 100), Zeilennummern (NR == 5) oder Bereiche (/start/,/end/) sein. ~ und !~ wenden Regex auf spezifische Felder an. Bedingungen können mit &&, || und ! kombiniert werden. Dies macht awk zu einem mächtigen Filter — ausdrucksstärker als grep, weil Sie Feld-Werte numerisch oder als Strings vergleichen können. Das Bereich-Pattern (/start/,/end/) druckt alle Zeilen zwischen zwei Markern (inklusiv). awk wertet Bedingungen pro Zeile aus; wenn true, läuft die Aktion. Ohne Aktion ist der Default {print}.

bash
# Pattern: only process matching lines
awk '/error/ {print}' log.txt          # like grep
awk '$3 > 100' data.txt                # 3rd field > 100
awk 'NR == 5' file.txt                 # line 5 only
awk 'NR >= 10 && NR <= 20' file.txt    # lines 10-20
awk 'NF == 0' file.txt                 # empty lines
awk '$1 == "ERROR"' log.txt            # first field equals ERROR

# Range pattern: from start to end
awk '/start/,/end/' file.txt

# Combine conditions
awk '$3 > 50 && $3 < 100 {print $1, $3}' data.txt
awk '$1 == "GET" || $1 == "POST" {print}' access.log

# Negation
awk '!/debug/ {print}' log.txt  # lines NOT containing debug

# Field comparison with regex
awk '$2 ~ /^[0-9]+$/' file.txt   # 2nd field is numeric
awk '$2 !~ /pattern/' file.txt   # 2nd field doesn't match

awk BEGIN/END & Variablen

BEGIN läuft, bevor irgendeine Eingabe gelesen wird (ideal für Initialisierung, Header, Variablen-Setup). END läuft, nachdem alle Eingabe verarbeitet wurde (ideal für Zusammenfassungen, Totale). Benutzer-Variablen benötigen keine Deklaration — sie defaulten zu 0 (numerisch) oder leer (String). -v übergibt externe Variablen an awk. Dies macht awk zu einer Mini-Programmiersprache für Daten-Verarbeitung: Summe, Durchschnitt, Min, Max, Anzahl — alles in einem Durchgang. Das Pattern 'NR == 1 {max = $1}' initialisiert max aus der ersten Zeile, dann aktualisieren nachfolgende Zeilen es. Dies ist viel effizienter als mehrere grep/sort/cut-Pipelines.

bash
# BEGIN: runs before processing (setup)
# END: runs after processing (summary)
awk 'BEGIN {print "Total:"} {sum += $1} END {print sum}' nums.txt

# Count lines
awk 'END {print NR " lines"}' file.txt

# Sum a column
awk '{sum += $2} END {print "Total:", sum}' sales.csv

# Average
awk '{sum += $2; count++} END {print sum/count}' data.txt

# Find max value
awk 'NR == 1 {max = $1} $1 > max {max = $1} END {print max}' nums.txt

# User-defined variables (no declaration needed)
awk '{total += $1; if ($1 > max) max = $1}
     END {print "Sum:", total, "Max:", max, "Avg:", total/NR}'

# Pass variables with -v
awk -v threshold=50 '$1 > threshold {print}' data.txt
awk -v date="$(date +%Y-%m-%d)" '{print date, $0}' file.txt

awk-Kontrollfluss

awk hat vollständigen Kontrollfluss: if/else, for, while und assoziative Arrays (Key-Value). Dies macht es zu einer vollständigen Daten-Verarbeitungs-Sprache. Das Word-Count-Beispiel (words[$1]++) ist der klassische awk-Anwendungsfall — Vorkommen jedes Wertes in einer Spalte zählen, dann nach Häufigkeit sortieren. Arrays in awk sind assoziativ (wie Dictionaries), indiziert nach String oder Zahl. for (key in array) iteriert Keys (ungeordnet — durch sort pipen). awk kann gesamte Pipelines aus cut, sort, uniq und wc mit einem einzigen, effizienten Durchgang ersetzen. Für komplexe Daten-Verarbeitung ist awk oft klarer als verkettete Unix-Befehle.

bash
# if-else
awk '{
  if ($3 >= 90) grade = "A"
  else if ($3 >= 80) grade = "B"
  else if ($3 >= 70) grade = "C"
  else grade = "F"
  print $1, grade
}' students.txt

# for loop
awk 'BEGIN {
  for (i = 1; i <= 5; i++) print i, i*i
}'

# for-in (iterate array)
awk '{count[$1]++} END {
  for (key in count) print key, count[key]
}' access.log | sort -rnk2

# while loop
awk '{
  i = 1
  while (i <= NF) {
    print i, $i
    i++
  }
}' file.txt

# Arrays (associative)
awk '{words[$1]++} END {
  for (w in words) print words[w], w
}' file.txt | sort -rn | head

awk praktische Beispiele

Diese praktischen Beispiele zeigen awks reale Macht. Die IP-Häufigkeits-Zählung ist unerlässlich für Log-Analyse. Die Dateigrößen-nach-Erweiterung-Zusammenfassung verwendet split(), um Erweiterungen zu extrahieren. CSV-Filterung nach Spaltenwert ersetzt komplexe grep/sed-Pipelines. Perzentil-Berechnung demonstriert awks mathematische Fähigkeiten (asorti sortiert Array-Indizes). Spalten-Umformatierung (Trennzeichen ändern und Felder auswählen) ist eine häufige ETL-Aufgabe. awk ist das Go-To-Tool für strukturierte Text-Verarbeitung — wenn Daten Spalten/Felder haben, ist awk fast immer die richtige Wahl. Für JSON verwenden Sie jq; für CSV awk oder csvkit.

bash
# Top 10 IP addresses from access log
awk '{print $1}' access.log | sort | uniq -c | sort -rn | head
# Or entirely in awk:
awk '{ip[$1]++} END {for (i in ip) print ip[i], i}' access.log | sort -rn | head

# Sum file sizes by extension
ls -l | awk '{split($NF, a, "."); ext=a[2]; size[ext]+=$5}
  END {for (e in size) print e, size[e]}' | sort -rnk2

# Filter CSV by column value
awk -F',' '$3 == "active" {print $1, $2}' users.csv

# Calculate response time percentiles from log
awk '{times[NR] = $2} END {
  n = asorti(times, sorted)
  print "p50:", sorted[int(n*0.5)]
  print "p95:", sorted[int(n*0.95)]
  print "p99:", sorted[int(n*0.99)]
}' response_times.txt

# Reformat columns
awk -F',' 'BEGIN{OFS="|"} {print $1, $2, $4}' data.csv
13

find & xargs

find nach Name & Typ

find durchsucht das Dateisystem nach verschiedenen Kriterien. -name stimmt mit dem Dateinamen überein (Groß-/Kleinschreibung-sensitiv); -iname ist Groß-/Kleinschreibung-insensitiv. -type f/d/l filtert nach Dateityp. -path stimmt mit dem vollständigen Pfad überein; -regex verwendet Full-Path-Regex. -o (oder) kombiniert Bedingungen; verwenden Sie \( \) für Gruppierung. -maxdepth limitiert die Rekursionstiefe (wichtig für Performance auf großen Dateisystemen). find gibt Pfade aus; kombinieren Sie mit -exec oder xargs, um auf Ergebnisse zu agieren. Quoten Sie Patterns immer, um Shell-Glob-Expansion zu verhindern. Für Code-Suche ist ripgrep (rg) schneller, aber find ist flexibler für Dateisystem-Operationen.

bash
# Find by name (exact)
find . -name "config.txt"

# Case-insensitive name
find . -iname "config.txt"

# Wildcard pattern
find . -name "*.py"
find . -name "test_*"

# Find by type
find . -type f -name "*.js"   # files only
find . -type d -name "node_modules"  # directories
find . -type l                # symlinks

# Find by path (full path match)
find . -path "*/src/*.test.js"

# Find by regex
find . -regex ".*\.\(py\|js\)$"

# Multiple name patterns
find . \( -name "*.py" -o -name "*.js" \)

# Max depth (don't recurse too deep)
find . -maxdepth 2 -name "*.txt"

find nach Zeit & Größe

finds zeitbasierte Suche ist unerlässlich für Cleanup und Auditing. -mtime (Modifikation), -atime (Zugriff), -ctime (Metadaten-Änderung) verwenden Tage; -mmin/-amin/-cmin verwenden Minuten. - (weniger als) und + (mehr als) präfixen den Wert. Größe verwendet Suffixe: c (Bytes), k (KB), M (MB), G (GB). -empty findet Null-Länge-Dateien oder leere Verzeichnisse. -perm prüft Berechtigungen: exakte Übereinstimmung (644), alle Bits gesetzt (-u+x), oder irgendein Bit gesetzt (/4000). SUID-Dateien finden (/4000) ist eine Security-Audit-Technik. Diese Kriterien können mit -a (und, Standard) und -o (oder) kombiniert werden.

bash
# Find by modification time
find . -mtime -1    # modified in last 24 hours
find . -mtime +7    # modified more than 7 days ago
find . -mtime 7     # modified exactly 7 days ago

# Access time (-atime) and change time (-ctime)
find . -atime -1    # accessed in last 24h
find . -ctime -1    # status changed in last 24h

# Minutes instead of days
find . -mmin -30    # modified in last 30 minutes

# Find by size
find . -size +100M   # larger than 100MB
find . -size -1k     # smaller than 1KB
find . -size 10M     # exactly 10MB

# Find empty files/directories
find . -empty -type f
find . -empty -type d

# Find by permissions
find . -perm 644     # exactly 644
find . -perm -u+x    # has execute for user
find / -perm /4000   # SUID files (security audit)

find -exec & -delete

-exec führt einen Befehl auf jedem Ergebnis aus. {} ist der Platzhalter für den Dateinamen; \; beendet den Befehl (läuft einmal pro Datei); + batcht Dateien (läuft einmal mit allen Dateien — effizienter). -delete entfernt übereinstimmende Dateien (schneller als -exec rm, aber zuerst mit -print testen). Das chmod-Muster (Verzeichnisse 755, Dateien 644) ist ein häufiges Webserver-Setup. -exec mit + wird für Befehle bevorzugt, die mehrere Dateien akzeptieren (grep, ls, wc). Für Löschung immer zuerst mit -print ausführen, um zu verifizieren, was gelöscht wird, dann durch -delete ersetzen. -ok statt -exec fragt pro Datei nach Bestätigung.

bash
# Execute command on each result
find . -name "*.log" -exec ls -lh {} \;

# Execute with + (batch, more efficient)
find . -name "*.log" -exec ls -lh {} +

# Delete files (DANGEROUS - test first!)
find /tmp -name "*.tmp" -type f -delete
# Safer: use -exec rm
find /tmp -name "*.tmp" -type f -exec rm -i {} \;

# Execute a complex command
find . -name "*.py" -exec wc -l {} \; | sort -rn | head

# Change permissions
find . -type d -exec chmod 755 {} \;  # dirs to 755
find . -type f -exec chmod 644 {} \;  # files to 644

# Find and grep
find . -name "*.py" -exec grep "TODO" {} +

# Print before deleting (confirmation)
find . -name "*.bak" -print -delete

xargs-Grundlagen

xargs konvertiert stdin in Kommando-Argumente. Es ist die Brücke zwischen find-Output und Befehlen, die stdin nicht lesen. -n limitiert Argumente pro Befehl; -I {} definiert einen Platzhalter für benutzerdefinierte Positionierung. -0 (mit find -print0) behandelt Dateinamen mit Leerzeichen/Zeilenumbrüchen korrekt — verwenden Sie immer dieses Paar für Sicherheit. -P aktiviert parallele Ausführung (großartig für CPU-intensive Aufgaben wie Bildkonvertierung). -t (trace) zeigt Befehle vor der Ausführung; -p fragt nach Bestätigung. xargs ist unerlässlich, wenn -exec zu langsam ist (ein Prozess pro Datei) — xargs batcht Argumente effizient.

bash
# Basic: take stdin and pass as arguments
echo "file1 file2 file3" | xargs rm

# One argument per line
find . -name "*.bak" | xargs rm

# Control arguments per command (-n)
echo "1 2 3 4 5" | xargs -n 2 echo  # echo 1 2; echo 3 4; echo 5

# Show commands before running (-t, verbose)
find . -name "*.log" | xargs -t rm

# Prompt before each command (-p)
find . -name "*.tmp" | xargs -p rm

# Handle filenames with spaces (-0 with find -print0)
find . -name "*.txt" -print0 | xargs -0 grep "pattern"

# Replace string (-I)
find . -name "*.py" | xargs -I {} cp {} /backup/

# Parallel execution (-P)
find . -name "*.png" | xargs -P 4 -I {} convert {} {}.thumb.png

# Limit with -L (lines per command)
cat urls.txt | xargs -L 1 curl -O

find + xargs-Patterns

find + xargs ist das klassische Unix-Muster für Batch-Datei-Operationen. -print0 | xargs -0 ist die sichere Kombination für Dateinamen mit Leerzeichen oder Sonderzeichen. Das Bulk-grep-Muster ist viel schneller als -exec grep (ein grep-Prozess vs. viele). Paralleles xargs (-P N) beschleunigt CPU-intensive Aufgaben wie Audio/Video-Konvertierung drastisch. Das Archiv-Muster (alte Logs finden, taren) ist eine häufige Log-Rotation-Technik. Verwenden Sie immer -print0/-0 für Robustheit — ohne es brechen Dateinamen mit Leerzeichen, Anführungszeichen oder Zeilenumbrüchen die Pipeline. Diese Kombination ist fundamental für Unix-Systemadministration.

bash
# Find and bulk grep (efficient)
find . -name "*.py" -print0 | xargs -0 grep -l "import django"

# Find large files and show sizes
find . -type f -size +100M -print0 | xargs -0 ls -lhS

# Find and archive
find . -name "*.log" -mtime +30 -print0 |
  xargs -0 tar -czf old_logs.tar.gz

# Find and batch rename
find . -name "*.JPG" | xargs -I {} mv {} {}.bak
# Or with rename command:
find . -name "*.JPG" -exec rename 's/\.JPG$/.jpg/' {} +

# Find empty directories and remove
find . -type d -empty -print0 | xargs -0 rmdir

# Find recently modified files and copy
find . -name "*.py" -mtime -1 -print0 |
  xargs -0 -I {} cp {} /tmp/recent/

# Parallel processing
find . -name "*.wav" -print0 |
  xargs -0 -P 8 -I {} ffmpeg -i {} {}.mp3
14

Fortgeschrittene Bash-Techniken

Here Document & Here String

Here-Dokumente (<< DELIMITER) speisen Multi-Zeilen-Text als stdin an einen Befehl — nützlich zum Generieren von Config-Dateien, SQL-Queries oder jeglicher Multi-Zeilen-Eingabe. Unquoted Delimiter erlaubt Variablen-Expansion; gequoteter ('EOF') behandelt Inhalt wörtlich. Here-Strings (<<<) speisen einen einzelnen String als stdin — sauberer als echo | Befehl für einfache Fälle. Der Delimiter kann ein beliebiges Wort sein (EOF, END, DONE); Konvention ist Großschreibung. Here-Docs sind unerlässlich für Skripte, die Dateien generieren oder mit interaktiven Programmen interagieren (mysql, psql, ssh). Eingerückte Delimiter (<<-) strippen führende Tabs für Lesbarkeit.

bash
# Here document: multi-line input to a command
cat << EOF
Line 1
Line 2 with $VARIABLE expansion
Line 3
EOF

# Quoted delimiter: no variable expansion
cat << 'EOF'
This $won't expand
\n stays literal
EOF

# Write to file
cat > config.txt << EOF
host=localhost
port=8080
EOF

# Here string: single string as stdin
grep "pattern" <<< "search this text"
read -r line <<< "hello world"

# With variables
name="Alice"
greeting=$(cat <<< "Hello, $name!")

# Pipe multi-line to command
mysql << EOF
USE mydb;
SELECT * FROM users;
EOF

Parameter-Expansion

Parameter-Expansion ist Bashes eingebaute String-Manipulation — kein Need für sed/awk/cut für einfache Operationen. :- bietet Standardwerte; # und ## strippen Präfixe; % und %% strippen Suffixe. Die Datei-Erweiterungs-Beispiele sind extrem häufig: ${file##*.} bekommt die Erweiterung, ${file%.*} bekommt den Basename ohne Erweiterung. / ersetzt die erste Übereinstimmung; // ersetzt alle. ^^ und ,, konvertieren Groß-/Kleinschreibung (Bash 4+). Diese Operationen sind schneller als das Spawnen externer Befehle. Beherrschen Sie Parameter-Expansion, um effiziente, lesbare Bash-Skripte ohne unnötige Subshells zu schreiben.

bash
# Default values
echo ${name:-"default"}    # use "default" if name unset/empty
echo ${name:="default"}    # set name to "default" if unset/empty

# String length
echo ${#var}

# Substring extraction
var="Hello World"
echo ${var:0:5}    # Hello (start:length)
echo ${var:6}      # World (start to end)

# Remove from beginning (# shortest, ## longest)
file="archive.tar.gz"
echo ${file#*.}     # tar.gz (remove up to first .)
echo ${file##*.}    # gz (remove up to last .)

# Remove from end (% shortest, %% longest)
echo ${file%.*}     # archive.tar (remove from last .)
echo ${file%%.*}    # archive (remove from first .)

# Replace
echo ${var/World/Bash}    # Hello Bash (first match)
echo ${var//l/L}          # HeLLo WorLd (all matches)

# Case conversion (Bash 4+)
echo ${var^^}   # HELLO WORLD (uppercase)
echo ${var,,}   # hello world (lowercase)

trap & Signal-Behandlung

trap registriert Handler für Signale und Events. EXIT feuert bei Skript-Terminierung (normal, Fehler oder gekillt) — ideal für Cleanup (temporäre Dateien, Locks). INT (Ctrl-C), TERM (kill), HUP (Terminal geschlossen) sind häufige Signale. Das Temp-File-Muster (trap 'rm -f' EXIT) stellt Cleanup sicher, selbst wenn das Skript fehlschlägt. DEBUG feuert vor jedem Befehl — nützlich zum Tracen. Quoten Sie den Trap-Befehl immer (einfache Anführungszeichen verhindern sofortige Expansion). trap ist unerlässlich für robuste Skripte: ohne es akkumulieren sich temporäre Dateien und Locks werden bei Fehlern möglicherweise nicht freigegeben. Räumen Sie immer nach sich auf.

bash
# Clean up on exit (any reason)
cleanup() {
  echo "Cleaning up..."
  rm -f /tmp/myapp.lock
  exit 0
}
trap cleanup EXIT

# Handle specific signals
trap 'echo "Interrupted!"; cleanup' INT TERM
trap 'echo "Ctrl-C pressed"' INT

# Ignore a signal
trap '' INT  # ignore Ctrl-C

# Reset trap
trap - INT  # restore default behavior

# Common pattern: temp file cleanup
TMPFILE=$(mktemp)
trap 'rm -f "$TMPFILE"' EXIT
echo "data" > "$TMPFILE"
# ... use TMPFILE ...
# File is removed when script exits (even on error)

# Debug trap (trace execution)
trap 'echo "DEBUG: $BASH_COMMAND"' DEBUG

Bash-Skripte debuggen

set -x tracet die Ausführung (gibt jeden Befehl mit +-Präfix aus) — das primäre Debugging-Tool. set -euo pipefail (Strict Mode) fängt Fehler früh: -e beendet bei non-zero, -u fängt Tippfehler in Variablennamen, pipefail lässt Pipes korrekt fehlschlagen (ohne es zählt nur der Exit-Code des letzten Befehls). bash -n prüft Syntax ohne Ausführung. PS4 passt das Trace-Präfix an (file:line anzuzeigen ist sehr hilfreich). trap ERR feuert bei jedem Fehler (mit -e) und gibt Ihnen Zeilennummern-Kontext. Für komplexe Skripte fügen Sie set -x an verdächtigen Stellen hinzu statt global. Verwenden Sie immer Strict Mode in Produktions-Skripten.

bash
# Trace execution: print each command before running
bash -x script.sh
# Or within script:
set -x   # enable tracing
set +x   # disable tracing

# Verbose: print input lines as read
bash -v script.sh

# Strict mode (recommended for all scripts)
set -euo pipefail
# -e: exit on error
# -u: error on undefined variable
# -o pipefail: pipe fails if any command fails
# -o pipefail: pipe fails if any command fails

# Print specific debug info
echo "DEBUG: var=$var" >&2  # to stderr

# Check syntax without executing
bash -n script.sh

# PS4: customize trace prompt
export PS4='+${BASH_SOURCE}:${LINENO}: '
bash -x script.sh

# Trap ERR for error handling
trap 'echo "Error on line $LINENO"' ERR

curl & wget

curl und wget sind unerlässliche HTTP-Clients. curl ist für API-Tests (unterstützt alle HTTP-Methoden, Header, JSON). -d sendet POST-Daten; -H setzt Header; -X spezifiziert die Methode; -L folgt Weiterleitungen; -O speichert mit Remote-Dateinamen; -s ist silent (für Skripting). wget ist optimiert für Downloads (setzt mit -c fort, rekursiv mit -r). Für REST-API-Tests ist curl mit -H 'Content-Type: application/json' und -d für JSON-Body der Standard. -w '%{http_code}' extrahiert nur den Status-Code für Skripting. Für komplexe API-Tests ziehen Sie httpie (einfachere Syntax) oder postman in Betracht.

bash
# Basic HTTP request
curl https://api.example.com/data
curl -O https://example.com/file.zip  # save to file

# HTTP methods
curl -X POST https://api.example.com/users
curl -X PUT -d '{"name":"Alice"}' https://api.example.com/users/1
curl -X DELETE https://api.example.com/users/1

# Headers and JSON
curl -H "Content-Type: application/json" \
     -H "Authorization: Bearer TOKEN" \
     -d '{"key":"value"}' \
     https://api.example.com/data

# Download with wget
wget https://example.com/file.zip
wget -q https://example.com/file.zip  # quiet
wget -c https://example.com/file.zip  # continue/resume

# Follow redirects
curl -L https://short.url/abc

# Save response, show headers
curl -s -o response.json -w "%{http_code}" https://api.example.com

# Download with progress bar
curl --progress-bar -O https://example.com/largefile.iso

# POST form data
curl -d "name=Alice&[email protected]" https://api.example.com/form
15

Signale & Traps

trap Cleanup-Handler

trap registriert Handler für Signale. EXIT ist speziell — es feuert bei Shell-Exit aus irgendeinem Grund, was es perfekt für Cleanup macht. Bereinigen Sie immer Lock-Dateien, Temp-Verzeichnisse und Kind-Prozesse in einem Trap. Häufige Signale zum Trappen: INT (Ctrl-C), TERM (Default-kill), HUP (Terminal geschlossen), EXIT.

bash
cleanup() {
  echo "Cleaning up..."
  rm -f /tmp/myapp.lock
  exit 0
}

# catch INT (Ctrl-C), TERM, EXIT
trap cleanup INT TERM EXIT

# normal exit also triggers EXIT trap
echo "Working..."
sleep 30
# cleanup runs automatically

# remove trap
trap - EXIT                      # disable
trap - INT TERM EXIT             # disable all

Temp-Dateien mit Cleanup

Erstellen Sie nie manuell Temp-Dateinamen mit $$ oder $RANDOM — vorhersagbare Pfade laden zu Symlink-Angriffen ein. mktemp erstellt atomar eine eindeutig benannte Datei/Verzeichnis. Koppeln Sie mit einem Trap auf EXIT, um Cleanup selbst bei Fehler oder Unterbrechung zu garantieren. Setzen Sie die TMPDIR-Umgebungsvariable, um den Speicherort zu kontrollieren.

bash
TMPDIR=$(mktemp -d)
TMPFILE=$(mktemp)

cleanup() {
  rm -rf "$TMPDIR" "$TMPFILE"
}
trap cleanup EXIT

# use temp files safely
echo "data" > "$TMPFILE"
process_data < "$TMPFILE"

# mktemp is safer than $RANDOM — avoids symlink attacks
# -d creates a directory
# -p /var/tmp specifies parent
# mktemp -t myapp.XXXXXX   # use TMPDIR env

Signal-Behandlung in Skripten

Für langlaufende Skripte verwenden Sie eine Flag-Variable, die vom Signal-Handler umgekippt wird, um elegant aus der Hauptschleife auszubrechen. Dies lässt Sie die aktuelle Iteration beenden, Zustand persistieren und sauber beenden. Vermeiden Sie schwere Arbeit innerhalb des Trap-Handlers selbst — setzen Sie nur Flags.

bash
#!/bin/bash
# graceful shutdown for a long-running script

RUNNING=1
stop() {
  echo "Stopping..."
  RUNNING=0
}
trap stop INT TERM

while [ $RUNNING -eq 1 ]; do
  echo "Tick $(date)"
  sleep 1
done

echo "Done."

# send signals: kill -TERM <pid> or Ctrl-C

timeout und watch

timeout führt einen Befehl aus und killt ihn (Standard SIGTERM) nach einer Dauer. Exit-Code 124 bedeutet Timeout. Verwenden Sie --kill-after, um zu SIGKILL zu eskalieren, wenn der Prozess SIGTERM ignoriert. Kombinieren Sie mit Schleifen, um Health-Checks zu bauen. watch ist für menschenorientierte periodische Anzeige, nicht für Skripte.

bash
# timeout kills a command after N seconds
timeout 30 slow_command
timeout 5s curl https://slow.example.com
timeout --signal=KILL 60 build.sh  # escalate to SIGKILL
timeout -k 5 30 command            # kill -9 after 5s grace

# exit status: 124 if timed out
timeout 2 sleep 10
echo $?                            # 124

# watch reruns a command
watch -n 1 date
watch -n 5 'kubectl get pods'

# tlimit-style loops
while true; do
  timeout 5 curl -s https://api.example.com/health || echo "down"
  sleep 60
done

Ignorieren und Weiterleiten

trap '' SIGNAL ignoriert ein Signal (praktisch für un-unterbrechbare kritische Sektionen). Um Signale an einen Kind-Prozess weiterzuleiten, erfassen Sie seine PID mit $! und senden Sie erneut. Der ERR-Trap feuert bei jedem Befehlsfehler, wenn set -e aktiviert ist — nützlich zum Loggen der fehlschlagenden Zeilennummer via $LINENO.

bash
# ignore a signal entirely
trap '' INT                       # ignore Ctrl-C
trap - INT                        # restore default

# re-raise / propagate to children
trap 'kill -TERM $childpid; wait $childpid' TERM

# forward signals to a subprocess
childpid=
run_child() {
  some_server &
  childpid=$!
  wait $childpid
}
trap 'kill -TERM $childpid' TERM INT
run_child

# exit on any error
set -e
trap 'echo "Failed at line $LINENO"' ERR
16

Funktionen & Bibliotheken

Funktionen definieren

Funktionen gruppieren wiederverwendbare Befehle. Zwei Syntaxen: name() {...} und function name {...}. Argumente innerhalb von Funktionen sind $1, $2 usw. — $0 ist immer noch der Skriptname. $@ expandiert zu allen Argumenten (immer als "$@" quoten, um Argumente mit Leerzeichen zu erhalten). $# ist die Anzahl.

bash
# two equivalent syntaxes
greet() {
  echo "Hello, $1!"
}

function greet {                  # also valid
  echo "Hello, $1!"
}

greet Alice                       # Hello, Alice!
greet "Bob Smith"                 # quoted args

# arguments: $1, $2, ... $@, $#, $*
echo_args() {
  echo "Count: $#"
  echo "All: $@"
  echo "First: $1"
  for arg in "$@"; do echo "- $arg"; done
}

Rückgabewerte und Lokale

return setzt den Exit-Status (0=Erfolg, 1-255=Fehler) — es gibt NICHT einen Wert zurück. Um einen Wert zurückzubekommen, echoen Sie ihn und erfassen Sie mit $(). Verwenden Sie local für Variablen innerhalb von Funktionen, um den globalen Scope nicht zu verschmutzen. local ist unerlässlich in rekursiven Funktionen.

bash
# return is exit status (0-255), not a value
is_even() {
  if (( $1 % 2 == 0 )); then return 0; else return 1; fi
}

if is_even 4; then echo "even"; fi

# capture stdout as a value
get_date() { date +%Y-%m-%d; }
today=$(get_date)

# local variables
counter() {
  local count=0                  # scoped to function
  ((count++))
  echo $count
}

Bibliotheken sourcen

source (oder .) führt eine Datei in der aktuellen Shell aus — sodass Funktionen, Variablen und Aliase verfügbar werden. So bauen Sie wiederverwendbare Bibliotheken. BASH_SOURCE[0] vs $0 unterscheidet gesourced vs ausgeführt — praktisch für Dateien, die sowohl Bibliothek als auch ausführbares Skript sind.

bash
# library file: lib/utils.sh
log() {
  echo "[$(date +%H:%M:%S)] $*" >&2
}

CONFIG_PATH="/etc/myapp"

# main script
source lib/utils.sh              # or: . lib/utils.sh
log "Starting..."
echo "Config: $CONFIG_PATH"

# check if sourced vs executed
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
  echo "Running directly"
else
  echo "Sourced"
fi

Rekursive Funktionen

Bash unterstützt Rekursion, aber sie ist langsam (jeder Aufruf spawnt Subshells für $()) und hat eine begrenzte Stack-Tiefe (~1000er). Für rechenintensive Arbeit bevorzugen Sie awk, Python oder externe Tools. Verwenden Sie immer lokale Variablen in rekursiven Funktionen — sonst überschreiben sie sich gegenseitig über Aufrufe hinweg.

bash
# factorial
factorial() {
  local n=$1
  if (( n <= 1 )); then
    echo 1
  else
    local prev=$(factorial $((n - 1)))
    echo $((n * prev))
  fi
}
factorial 5                      # 120

# fibonacci
fib() {
  if (( $1 < 2 )); then echo $1; return; fi
  echo $(( $(fib $(($1 - 1))) + $(fib $(($1 - 2))) ))
}

# caution: bash recursion is slow and limited depth
# for heavy work, use awk, python, or external tools

Default und optionale Argumente

${1:-default} substituiert einen Default, wenn $1 ungesetzt oder leer ist. ${1:?message} beendet mit einem Fehler, wenn fehlt — großartig für Pflichtargumente. shift poppt $1 von der Argumentliste. Die Kombination case + shift ist die idiomatische Methode, Flags und positionale Argumente in Bash-Funktionen zu parsen.

bash
# default values
greet() {
  local name="${1:-World}"        # default if unset or empty
  local greeting="${2:-Hello}"
  echo "$greeting, $name!"
}
greet                            # Hello, World!
greet Alice                      # Hello, Alice!
greet Alice Hi                   # Hi, Alice!

# required argument
require_arg() {
  : "${1:?Usage: require_arg <name>}"   # exits with message if missing
  echo "Got: $1"
}

# shift through args
process() {
  while (( $# )); do
    case "$1" in
      -v) verbose=1; shift ;;
      *)  files+=("$1"); shift ;;
    esac
  done
}
17

Arrays & assoziative Arrays

Indizierte Arrays

Indizierte Arrays verwenden 0-basierte Integer-Keys. Quoten Sie immer "${arr[@]}" beim Iterieren, um Elemente mit Leerzeichen korrekt zu behandeln. ${#arr[@]} ist die Anzahl. ${!arr[@]} gibt die Indizes (nützlich für sparse Arrays). unset 'arr[i]' (gequotet) entfernt ein Element.

bash
# declare and assign
fruits=("apple" "banana" "cherry")
fruits+=("date")                 # append
fruits[10]="kiwi"                # sparse

# access
echo "${fruits[0]}"             # first
echo "${fruits[@]}"             # all values
echo "${#fruits[@]}"            # count
echo "${!fruits[@]}"            # indices

# iterate
for f in "${fruits[@]}"; do
  echo "$f"
done

# slice
echo "${fruits[@]:1:2}"         # 2 items starting at index 1

# delete
unset 'fruits[1]'

Assoziative Arrays (Maps)

Assoziative Arrays (declare -A) sind ein Bash 4+-Feature (macOS liefert standardmäßig Bash 3 — verwenden Sie brew install bash). Keys sind Strings. Der -v-Test prüft, ob ein Key existiert. Iterieren Sie Keys mit ${!arr[@]}. macOS default /bin/bash ist 3.2 — Skripte, die -A verwenden, benötigen #!/usr/bin/env bash, das auf ein neueres Bash zeigt.

bash
# requires bash 4+
declare -A ages
ages["alice"]=30
ages["bob"]=25
ages["carol"]=42

# access
echo "${ages["alice"]}"
echo "${ages[@]}"               # all values
echo "${!ages[@]}"              # all keys

# iterate keys
for name in "${!ages[@]}"; do
  echo "$name => ${ages[$name]}"
done

# check key exists
if [[ -v ages["alice"] ]]; then echo "yes"; fi

# count
echo "${#ages[@]}"

# delete
unset 'ages[bob]'

In Arrays lesen

mapfile (a.k.a. readarray) ist die schnellste Methode, Zeilen in ein Array zu laden — verwenden Sie immer -t, um nachfolgende Zeilenumbrüche zu strippen. Die while-read-Schleife ist portabel, aber langsamer. Um einen begrenzten String zu splitten, setzen Sie IFS und verwenden read -ra. < <(cmd) Prozess-Substitution vermeidet Subshell-Variablen-Scope-Probleme.

bash
# read lines into array
mapfile -t lines < file.txt      # bash 4+
echo "${lines[0]}"

# alternative (portable)
lines=()
while IFS= read -r line; do
  lines+=("$line")
done < file.txt

# split string into array
IFS=',' read -ra csv <<< "a,b,c,d"
echo "${csv[2]}"                # c

# from command
mapfile -t files < <(find . -name "*.js")

# process each
for f in "${files[@]}"; do
  echo "Processing $f"
done

Array-Operationen

Bash-Arrays haben viele eingebaute Operationen nicht (kein .indexOf, .reverse, .unique). Für diese schreiben Sie Schleifen oder pipen durch sort/uniq. Der Unique-Array-Trick verwendet ein assoziatives Array als Set. Für komplexe Array-Manipulation ziehen Sie awk oder eine echte Programmiersprache in Betracht — Bash-Arrays sind am besten für einfache Listen.

bash
# append
arr=(a b c)
arr+=(d e)                       # (a b c d e)

# length
echo "${#arr[@]}"

# slice
echo "${arr[@]:1:2}"            # (b c)

# find index (no built-in; loop)
idx=-1
for i in "${!arr[@]}"; do
  if [[ "${arr[$i]}" == "b" ]]; then idx=$i; break; fi
done

# reverse
n=${#arr[@]}
for ((i=n-1; i>=0; i--)); do
  echo "${arr[$i]}"
done

# unique (preserve order)
declare -A seen
unique=()
for x in "${arr[@]}"; do
  [[ -v seen[$x] ]] && continue
  seen[$x]=1
  unique+=("$x")
done

Verschachtelte Daten mit Arrays

Bash hat keine nativen verschachtelten Arrays. Workarounds: (1) Datensätze als begrenzungs-verbundene Strings speichern und mit IFS splitten, (2) ein assoziatives Array mit Komposit-Keys wie "r,c" verwenden, (3) Variablen-Indirektion mit ${!name} verwenden. Für echte verschachtelte Daten wechseln Sie zu jq + JSON oder einer echten Sprache.

bash
# array of "records" (delimiter-based)
users=("alice:30:admin" "bob:25:user")
for u in "${users[@]}"; do
  IFS=':' read -r name age role <<< "$u"
  echo "$name ($age) - $role"
done

# simulate 2D with name-mangled vars
declare -A grid
grid["0,0"]=1; grid["0,1"]=2
grid["1,0"]=3; grid["1,1"]=4
for key in "${!grid[@]}"; do
  IFS=',' read -r r c <<< "$key"
  echo "[$r,$c] = ${grid[$key]}"
done

# arrays of arrays (not directly supported)
# workaround: use indirection
rows=("row0" "row1")
row0=(a b c)
row1=(d e f)
ref="${rows[0]}[@]"
echo "${!ref}"                   # a b c
18

String-Manipulation

Parameter-Expansion

Parameter-Expansion ist Bashes mächtigstes String-Tool. # und ## strippen vom Anfang (kürzeste/längste Übereinstimmung); % und %% strippen vom Ende. / ersetzt die erste, // ersetzt alle. Diese vermeiden das Spawnen von Subshells — viel schneller als das Pipen durch sed für einfache Operationen.

bash
path="/var/log/app.log"

# length
echo "${#path}"                 # 16

# substring (offset:length)
echo "${path:5}"                # log/app.log
echo "${path:5:3}"              # log

# remove from front (shortest/longest)
echo "${path#*/}"               # var/log/app.log
echo "${path##*/}"              # app.log (basename)

# remove from end
echo "${path%/*}"               # /var/log (dirname)
echo "${path%%.*}"              # /var/log/app

# replace
echo "${path/log/LOG}"          # /var/LOG/app.log (first)
echo "${path//log/LOG}"         # all occurrences

Groß-/Kleinschreibungs-Konvertierung

Bash 4+ hat ,, (lower), ^^ (upper), ^ (erstes Zeichen groß), , (erstes Zeichen klein) hinzugefügt. declare -u/-l konvertiert Zuweisungen automatisch. Für älteres Bash (macOS-Standard) verwenden Sie tr oder awk. Case-Konvertierung ist häufig beim Normalisieren von Benutzereingabe oder beim Erstellen von Identifikatoren.

bash
s="Hello World"

# bash 4+
echo "${s,,}"                   # hello world (lower)
echo "${s^^}"                   # HELLO WORLD (upper)
echo "${s^}"                    # Hello World (capitalize first)
echo "${s,}"                    # hello World (lowercase first)

# pattern-based
echo "${s^^[aeiou]}"            # hEllO wOrld (vowels upper)

# portable (pre-bash 4)
echo "$s" | tr 'a-z' 'A-Z'
echo "$s" | awk '{print toupper($0)}'

# declare typed variables
declare -u UPPER="hello"         # HELLO
declare -l LOWER="WORLD"         # world

Splitten und Verbinden

Splitten: IFS setzen und read -ra verwenden. Verbinden: ${arr[*]} mit gesetztem IFS funktioniert, ist aber knifflig — die join_by-Funktion ist zuverlässiger. Der printf '%s' "$d%s"-Trick stellt das Trennzeichen vor jedes verbleibende Argument und produziert einen sauberen Join. tr kann zwischen Trennzeichen und Zeilenumbrüchen konvertieren.

bash
# split on delimiter
csv="a,b,c,d"
IFS=',' read -ra parts <<< "$csv"
echo "${parts[2]}"              # c
for p in "${parts[@]}"; do echo "$p"; done

# join with delimiter (bash 4+)
arr=(a b c d)
joined=$(IFS=,; echo "${arr[*]}")
echo "$joined"                   # a,b,c,d

# custom joiner
join_by() {
  local d=$1; shift
  local first=$1; shift
  printf '%s' "$first"
  printf '%s' "$d%s" "$@"
}
join_by " | " a b c              # a | b | c

# split into lines
tr ',' '\n' <<< "a,b,c"

Suche und Test

Pattern-Matching: == mit Wildcards (* ? [..]) macht Glob-Matching. =~ macht erweiterten Regex; Capture-Gruppen landen in BASH_REMATCH. shopt -s nocasematch macht [[ ]]-Vergleiche Groß-/Kleinschreibung-insensitiv. expr ist Legacy — bevorzugen Sie [[ ]] für neuen Code.

bash
s="Hello World"

# substring search
if [[ "$s" == *"World"* ]]; then echo "contains"; fi
if [[ "$s" == Hello* ]]; then echo "starts with"; fi
if [[ "$s" == *World ]]; then echo "ends with"; fi

# regex match
if [[ "$s" =~ ^H[a-z]+\sW[a-z]+$ ]]; then echo "match"; fi
echo "${BASH_REMATCH[0]}"       # whole match
echo "${BASH_REMATCH[1]}"       # first group

# case-insensitive
shopt -s nocasematch
[[ "$s" == *world* ]] && echo "yes"
shopt -u nocasematch

# index (no built-in; use expr)
expr "$s" : ".*World"            # length up to match

printf-Formatierung

printf ist mächtiger als echo — unterstützt C-Stil-Format-Spezifizierer (%s, %d, %f, %x), Breite/Präzision und Ausrichtung. %()T formatiert Zeit, ohne den date-Befehl zu spawnen. printf -v speichert das Ergebnis in einer Variablen. printf '=%.0s' {1..40} ist ein netter Trick, um ein Zeichen N-mal zu wiederholen.

bash
# basic
printf '%s\n' "hello" "world"   # one per line
printf '%s = %d\n' "count" 42

# width and padding
printf '%10s\n' "right"         # right-aligned, width 10
printf '%-10s|\n' "left"        # left-aligned
printf '%05d\n' 42              # 00042 (zero-padded)
printf '%.2f\n' 3.14159         # 3.14

# date formatting
printf 'Today: %(%Y-%m-%d)T\n' -1

# to variable
printf -v today '%(%Y%m%d)T' -1
echo "$today"                    # 20260621

# repeat
printf '=%.0s' {1..40}; echo     # 40 equals signs
19

Reguläre Ausdrücke

Bash-Regex mit =~

=~ verwendet ERE (erweiterter Regex). Capture-Gruppen füllen BASH_REMATCH (Index 0 = ganze Übereinstimmung, 1+ = Gruppen). Quoten Sie den String, aber NICHT den Regex (das Quoten des Regex macht ihn literal). Für Groß-/Kleinschreibung-insensitives Matching verwenden Sie shopt -s nocasematch. Regex ist POSIX ERE — kein \d, \w; verwenden Sie [0-9], [A-Za-z0-9_].

bash
s="user_42"

if [[ "$s" =~ ^([a-z]+)_([0-9]+)$ ]]; then
  echo "Match!"
  echo "Name: ${BASH_REMATCH[1]}"   # user
  echo "ID: ${BASH_REMATCH[2]}"     # 42
fi

# email-ish check
email="[email protected]"
if [[ "$email" =~ ^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$ ]]; then
  echo "valid"
fi

# case-insensitive
shopt -s nocasematch
[[ "$s" =~ USER ]] && echo "yes"
shopt -u nocasematch

grep-Regex-Patterns

grep defaultet zu BRE (Basic Regex), wo +, ?, |, () Backslashes benötigen. -E aktiviert ERE (sauberere Syntax). -P aktiviert PCRE mit \d, \w, \b, Lookarounds — am mächtigsten, aber weniger portabel (nur GNU grep). Für Skripte, die auf macOS/BSD laufen müssen, bevorzugen Sie -E.

bash
# basic (BRE) — default
grep 'foo' file
grep 'a\.' file                 # literal dot (escape in BRE)
grep 'a*b' file                  # zero or more a

# extended (ERE) — -E
grep -E 'a+b' file               # one or more
grep -E 'a|b' file               # alternation
grep -E '(ab)+' file             # grouping
grep -E '[0-9]{3}' file          # quantifier

# perl (PCRE) — -P
grep -P '\d{3}' file            # digits
grep -P '\bword\b' file        # word boundary
grep -P '(?<=foo)bar' file       # lookbehind

# anchors
grep -E '^Error' file            # line start
grep -E 'done$' file             # line end
grep -E '^Error.*timeout$' file

sed-Regex-Substitution

sed verwendet standardmäßig BRE (Capture-Gruppen benötigen \( \), Backrefs \1). -E schaltet zu ERE (sauberere () und +, ?, |). Das I-Flag macht Substitution Groß-/Kleinschreibung-insensitiv (GNU sed). Verwenden Sie ein anderes Trennzeichen (s|...|...|), wenn das Pattern Slashes enthält. [[:space:]] ist portabel für Whitespace.

bash
# BRE (default)
sed 's/foo/bar/' file
sed 's/a\.b/x/' file            # literal dot
sed 's/a\(b\)c/\1/' file      # capture with backrefs

# ERE with -E
sed -E 's/a+b/x/' file
sed -E 's/(foo|bar)/X/g' file
sed -E 's/([0-9]{4})/Year: \1/' file

# case-insensitive
sed 's/foo/bar/I' file
sed 's/[fF][oO][oO]/bar/g' file

# common recipes
sed -E 's/^[[:space:]]+//' file  # trim leading whitespace
sed -E 's/[[:space:]]+$//' file  # trim trailing
sed -E 's/ +/ /g' file           # collapse spaces
sed -E 's#https?://[^ ]+##g' file # strip URLs

awk-Regex

awk verwendet ERE. Der ~-Operator testet einen String gegen einen Regex. /pat1/, /pat2/ ist ein Bereich-Pattern (stimmt von pat1 bis pat2 inklusiv überein). gsub macht in-Place globale Substitution. gawks match() mit einem dritten Argument erfasst Gruppen in ein Array — praktisch, aber nicht portabel zu mawk/POSIX awk.

bash
# match operator ~
awk '$1 ~ /^[0-9]+/' file        # field 1 is numeric
awk '$0 !~ /debug/' file         # line doesn't contain
awk '/^Error/,/^$/' file         # from Error to blank line

# case-insensitive
awk 'BEGIN { IGNORECASE=1 } /error/' file
awk 'tolower($0) ~ /error/' file

# gsub (global substitute)
awk '{ gsub(/foo/, "bar"); print }' file
awk '{ gsub(/[0-9]+/, "N"); print }' file

# match with capture (gawk)
awk 'match($0, /id=([0-9]+)/, m) { print m[1] }' file

# split on regex
awk '{ n = split($0, parts, /[,;\t]/); print n }' file

Häufige Regex-Rezepte

Diese Rezepte decken häufige Text-Extraktions-Aufgaben ab. -o druckt nur den übereinstimmenden Teil. Beachten Sie, dass Regex nicht ideal zum Parsen von HTML, JSON oder XML ist — verwenden Sie richtige Parser (jq für JSON, xmllint für XML). Für IPv4 stimmt dieser Regex mit dem Format überein, aber nicht mit gültigen Bereichen (255.255.255.255 vs 999.999.999.999).

bash
# IPv4
grep -E '([0-9]{1,3}\.){3}[0-9]{1,3}' access.log

# URL
grep -Eo 'https?://[^[:space:]]+' file

# email
grep -Eo '[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}' file

# date YYYY-MM-DD
grep -E '[0-9]{4}-[0-9]{2}-[0-9]{2}' file

# hex color
grep -Eo '#[0-9A-Fa-f]{6}' file

# strong password (8+, upper, lower, digit)
grep -E '^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9]).{8,}$' file

# trim whitespace
sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//' file

# extract domain from URL
sed -E 's#https?://([^/]+).*#\1#' file
20

Debugging

set-Optionen

set -e beendet bei jedem Befehlsfehler (Must-have für Skripte). set -u fängt Tippfehler in Variablennamen. set -o pipefail lässt eine Pipe fehlschlagen, wenn irgendein Teil fehlschlägt (ohne es zählt nur der Exit-Code des letzten Befehls). set -x tracet die Ausführung. Kombiniert als -euo pipefail ist dies 'Strict Mode' — der sicherste Default.

bash
#!/bin/bash
set -e                           # exit on error
set -u                           # error on unset variable
set -o pipefail                  # pipe fails if any command fails
set -x                           # print commands (trace)
# or combined:
set -euo pipefail

# shorthand
set -Eeuxo pipefail              # -E: ERR trap inherits

# strict mode for robust scripts
set -euo pipefail
IFS=$'\n\t'

# trace specific section
set -x
complex_logic
set +x

# debug a script
bash -x script.sh
bash -xv script.sh               # verbose + trace

Tracing mit -x

set -x gibt jeden Befehl vor der Ausführung aus, mit + als Default-Präfix (PS4). Das Anpassen von PS4, um file:line:function einzubeziehen, macht Traces viel nützlicher zum Debuggen. BASH_XTRACEFD leitet den Trace zu einem anderen FD um, sodass Sie Trace-Output von stdout/stderr trennen können.

bash
# trace entire script
bash -x script.sh

# trace inside script
set -x
echo "tracing on"
set +x

# customize PS4 (the trace prompt)
export PS4='+ ${BASH_SOURCE:-shell}:${LINENO}: ${FUNCNAME[0]:-main}() '
set -x
my_function
set +x

# trace to file
exec 5> /tmp/trace.log
BASH_XTRACEFD=5
set -x
# ... commands ...
set +x
exec 5>&-

Fehlerbehandlung und ERR-Trap

Der ERR-Trap feuert, wenn ein Befehl fehlschlägt (mit set -e). $? innerhalb des Traps gibt den fehlschlagenden Exit-Code. $LINENO zeigt, wo der Fehler auftrat. Das caller-Builtin druckt file:line:function — es in einer Schleife zu verwenden, produziert einen Stack-Trace. set -E lässt den ERR-Trap auch innerhalb von Funktionen feuern (sonst ist er im Funktions-Kontext deaktiviert).

bash
set -e
set -E                           # ERR trap inherits functions

on_error() {
  local exit_code=$?
  local line=$1
  echo "ERROR: command failed at line $line with exit code $exit_code" >&2
  echo "Stack:" >&2
  local i=0
  while caller $i >&2; do ((i++)); done
  exit $exit_code
}
trap 'on_error $LINENO' ERR

# now any failing command triggers the handler
false                            # triggers on_error

# caller builtin gives file:line:func
# stack trace via: while caller $i; do ((i++)); done

shellcheck-Linter

shellcheck ist der De-facto-Linter für Shell-Skripte — fängt Quoting-Probleme, ungenutzte Variablen, häufige Fallstricke und schlägt Idiome vor. Führen Sie es auf jedem Skript aus. Der häufigste Fix ist SC2086: Variablen quoten, um Word-Splitting und Globbing zu verhindern. Viele CI-Pipelines erfordern, dass shellcheck bestanden wird.

bash
# install: apt install shellcheck (or brew, or use shellcheck.net)
# run:
shellcheck script.sh
shellcheck -x script.sh          # trust includes (no SC1090 warnings)
shellcheck -S warning script.sh  # only warnings+
shellcheck --exclude=SC2086 *.sh # exclude specific rules

# common warnings:
# SC2086: double-quote to prevent globbing (use "$var")
# SC2046: quote $(cmd) to prevent word splitting
# SC2034: unused variable
# SC2155: declare and assign separately (masks return code)
# SC2004: $/${} unnecessary in arithmetic

# in script: disable inline
# shellcheck disable=SC2086
echo $unquoted_var

# disable for next line
# shellcheck disable=SC2086
echo $args

Logging und Diagnostik

Ein geloggter Logger (DEBUG/INFO/WARN/ERROR) mit Zeitstempeln und Farben macht Skripte viel leichter zu debuggen. Loggen Sie immer nach stderr, sodass stdout für Daten sauber bleibt. Unterstützen Sie ein --verbose-Flag (aktiviert set -x) und --dry-run (druckt Befehle ohne Ausführung). ANSI-Farb-Codes: 31=rot, 33=gelb, 32=grün, 90=grau.

bash
# logging function with levels
log() {
  local level=$1; shift
  local msg="$*"
  local ts=$(date '+%Y-%m-%d %H:%M:%S')
  local color
  case $level in
    DEBUG) color='\033[90m' ;;
    INFO)  color='\033[32m' ;;
    WARN)  color='\033[33m' ;;
    ERROR) color='\033[31m' ;;
  esac
  printf "%b[%s] [%s] %s\033[0m\n" "$color" "$ts" "$level" "$msg" >&2
}

log INFO "Starting process"
log WARN "Disk space low"
log ERROR "Connection failed"

# verbose flag
[[ $VERBOSE ]] && set -x

# dry-run mode
if [[ $DRY_RUN ]]; then
  echo "Would: $cmd"
else
  eval "$cmd"
fi
21

Security

Quoting und Injektion

Die #1 Bash-Security-Regel: quoten Sie jede Variablen-Expansion. Unquoted $var unterliegt Word-Splitting und Globbing, was zu Bugs und Injektion führt. Für Befehle mit dynamischen Argumenten verwenden Sie ein Array (cmd=(ls "$dir"); "${cmd[@]}") statt eines Strings. Evaluen oder $() Sie niemals Benutzereingabe in einen Shell-String.

bash
# ALWAYS quote variables
file="my file.txt"
rm $file                         # BAD: rm my file.txt (3 args!)
rm "$file"                       # GOOD: rm "my file.txt"

# use arrays for command + args
files=("a.txt" "b file.txt" "c.txt")
rm "${files[@]}"                # each file as separate arg, safely

# avoid eval
eval "echo $user_input"          # DANGEROUS
echo "$user_input"               # safe

# avoid command substitution in unsafe places
# BAD: filename from user, executed
cmd="ls $userdir"
$cmd                             # word-splitting + globbing

# GOOD: use array
cmd=(ls "$userdir")
"${cmd[@]}"

Sichere Temp-Dateien und Secrets

mktemp erstellt Dateien mit unvorhersehbaren Namen und 600-Berechtigungen — sicher vor Symlink-Races. Bereinigen Sie immer mit einem Trap. Lesen Sie Secrets aus Umgebungsvariablen, niemals hardcodieren. Vermeiden Sie es, Secrets als Kommandozeilen-Argumente zu übergeben (sichtbar via ps) — verwenden Sie stdin, Umgebungsvariablen oder eine Config-Datei mit 600-Berechtigungen.

bash
# safe temp files
tmp=$(mktemp)                    # atomic, unpredictable name
trap 'rm -f "$tmp"' EXIT

# safe temp dir
tmpdir=$(mktemp -d)
trap 'rm -rf "$tmpdir"' EXIT

# secrets: read from env, never hardcode
api_key="${API_KEY:?Set API_KEY env var}"

# don't leak secrets to process list (ps)
# BAD: curl -H "Authorization: Bearer $TOKEN" ...
# GOOD: pass via stdin or config file
curl -H @- <<EOF https://api.example.com
Authorization: Bearer $TOKEN
EOF

# restrict file perms for secrets
umask 077
echo "$api_key" > ~/.secrets/api
chmod 600 ~/.secrets/api

Eingabe-Validierung

Validieren Sie alle Benutzereingaben vor der Verwendung. Regex prüft das Format; Arithmetik prüft Bereiche. Für Dateipfade lehnen Sie .. (Traversal) und absolute Pfade ab, es sei denn, sie sind explizit erlaubt. tr -cd strippt Zeichen, die nicht im erlaubten Set sind — nützlich zum Sanitieren von Identifikatoren. Whitelist (case) ist sicherer als Blacklist für erlaubte Aktionen.

bash
# validate numeric input
read -p "Enter a number: " n
if ! [[ "$n" =~ ^[0-9]+$ ]]; then
  echo "Not a number" >&2; exit 1
fi

# validate range
if (( n < 1 || n > 100 )); then
  echo "Out of range" >&2; exit 1
fi

# validate file path (no traversal)
path="$1"
case "$path" in
  *..*|/*) echo "Invalid path" >&2; exit 1 ;;
esac

# sanitize filename
safe=$(echo "$name" | tr -cd 'A-Za-z0-9._-')

# whitelist allowed values
case "$action" in
  start|stop|restart) ;;
  *) echo "Invalid action" >&2; exit 1 ;;
esac

Privilegien-Drop und setuid

setuid-Shell-Skripte sind unsicher — die meisten Systeme ignorieren das setuid-Bit auf Skripten. Verwenden Sie sudo mit einem strikten sudoers-Eintrag (spezifischer Befehl, kein Passwort) stattdessen. Droppen Sie root so früh wie möglich via exec sudo -u. Für Capabilities (z.B. Port 80 binden) verwenden Sie setcap statt als root zu laufen. chmod 700 hält Skripte privat zum Eigentümer.

bash
# drop privileges in a script running as root
if [[ $EUID -eq 0 ]]; then
  exec sudo -u appuser "$0" "$@"
fi

# check for root
if [[ $EUID -ne 0 ]]; then
  echo "Requires root" >&2; exit 1
fi

# setuid scripts are DANGEROUS — avoid them
# instead, use sudoers entries
# /etc/sudoers.d/myapp:
#   appuser ALL=(root) NOPASSWD: /usr/local/bin/myapp.sh

# restrict script permissions
chmod 700 script.sh              # only owner can run
chown appuser:appuser script.sh

# use capsh instead of full root for fine-grained caps
# setcap 'cap_net_bind_service=+ep' /usr/bin/myapp

Signaturen und Integrität

Verifizieren Sie immer Checksums und Signaturen für heruntergeladene Skripte, besonders Installer, die als root ausgeführt werden. sha256sum -c vergleicht gegen einen bekannten guten Hash. GPG-Signaturen beweisen sowohl Integrität als auch Authentizität (die Identität des Signierers). Das Pipen von curl zu sha256sum lässt Sie den Hash vor der Ausführung begutachten — aber das sicherere Muster ist Download-dann-Verifizieren-dann-Ausführen.

bash
# verify checksum
sha256sum -c file.sha256
# file.sha256 contains: <hash>  <filename>

# generate checksums
sha256sum release.tar.gz > release.tar.gz.sha256
sha256sum *.iso > isos.sha256

# GPG sign and verify
gpg --sign file.txt              # creates file.txt.gpg
gpg --verify file.txt.gpg
gpg --detach-sig --armor file.tar.gz
gpg --verify file.tar.gz.asc file.tar.gz

# download + verify in one go
curl -fsSL https://example.com/install.sh | sha256sum
curl -fsSL https://example.com/install.sh -o install.sh
sha256sum -c <(echo "expected_hash  install.sh")
22

Netzwerk

curl Fortgeschritten

curl überträgt Daten via URLs. -X setzt die Methode, -d sendet Daten, -H setzt Header. -O speichert mit Remote-Dateinamen. -I fetcht nur Header. -L folgt Weiterleitungen. -u user:pass für Auth. -v für verbose.

bash
# POST with data
curl -X POST -d "name=Alice" https://api.example.com
# JSON POST
curl -H "Content-Type: application/json" \
     -d '{"name":"Alice"}' https://api.example.com
# Download with progress
curl -O https://example.com/file.zip
# Headers only
curl -I https://example.com

wget

wget lädt Dateien nicht-interaktiv herunter. -m spiegelt Sites. -c setzt unterbrochene Downloads fort. -r rekursiert, -l limitiert die Tiefe. -b läuft im Hintergrund. Besser als curl für rekursive Downloads.

bash
# Download file
wget https://example.com/file.zip
# Mirror website
wget -m https://example.com
# Resume download
wget -c https://example.com/largefile.zip
# Recursive download
wget -r -l 2 https://example.com
# Background
wget -b https://example.com/file.zip

ssh & scp

ssh verbindet zu Remote-Maschinen. -i spezifiziert eine Key-Datei. -L erstellt lokales Port-Forwarding. scp kopiert Dateien über SSH. -r rekursiert Verzeichnisse. Verwenden Sie ssh-copy-id, um Keys zu installieren. Konfigurieren Sie ~/.ssh/config für Aliase.

bash
# SSH connect
ssh user@host
# SSH with key
ssh -i ~/.ssh/id_rsa user@host
# Port forwarding
ssh -L 8080:localhost:80 user@host
# Copy file
scp file.txt user@host:/path/
# Copy directory
scp -r dir/ user@host:/path/

netstat & ss

netstat und ss zeigen Netzwerk-Verbindungen. -t TCP, -u UDP, -l listening, -n numerisch, -p Prozess. ss ist schneller und detaillierter als netstat. Verwenden Sie es, um zu finden, welcher Prozess einen Port verwendet, oder Verbindungsprobleme zu diagnostizieren.

bash
# List listening ports
netstat -tlnp
ss -tlnp
# Show all connections
netstat -an
ss -tunap
# Show routing table
netstat -r
# Show interface stats
netstat -i

ping & traceroute

ping testet Erreichbarkeit und Latenz (-c limitiert die Anzahl). traceroute zeigt den Pfad zu einem Host. dig fragt DNS-Records ab (A, MX, NS, TXT). +short gibt prägnanten Output. Verwenden Sie es für Netzwerk-Diagnose und DNS-Troubleshooting.

bash
# Check connectivity
ping -c 4 google.com
# Trace route
traceroute google.com
# DNS lookup
dig example.com
dig +short example.com
# Get DNS records
dig MX example.com
dig NS example.com
23

Shell-Skripting Deep

Funktionen

Funktionen gruppieren Befehle. local erstellt funktions-scoped Variablen. $1, $2 sind Argumente. return setzt Exit-Status (0-255). Output mit $() erfassen. Funktionen müssen vor der Verwendung definiert sein. Verwenden Sie source, um aus Dateien zu laden.

bash
greet() {
    local name="$1"
    echo "Hello, $name"
    return 0
}
greet "Alice"
# Return value via $?
result=$(greet "Bob")

Bedingungen

[ ] ist der Test-Befehl. -f Datei, -d Verzeichnis, -z leerer String, -n nicht-leer. = für Strings, -eq/-ne/-gt/-lt für Zahlen. Quoten Sie immer Variablen, um Leerzeichen zu behandeln. [[ ]] ist Bash-erweitert mit Regex.

bash
if [ -f "/path/file" ]; then
    echo "File exists"
elif [ -d "/path" ]; then
    echo "Directory exists"
else
    echo "Not found"
fi
# String comparison
if [ "$str" = "hello" ]; then ... fi
# Numeric
if [ "$num" -gt 10 ]; then ... fi

Schleifen

for iteriert über Listen. C-Stil-for verwendet (( )). while liest, bis Bedingung fehlschlägt. while read verarbeitet Dateien sicher Zeile für Zeile. Quoten Sie immer Variablen. Verwenden Sie find für rekursive Datei-Iteration.

bash
# For loop
for i in 1 2 3; do
    echo $i
done
# C-style
for ((i=0; i<5; i++)); do echo $i; done
# While loop
while read line; do
    echo "$line"
done < file.txt
# Iterate files
for f in *.txt; do process "$f"; done

Arrays

Arrays verwenden Klammern. ${arr[@]} expandiert alle Elemente. ${#arr[@]} ist die Länge. Quoten Sie immer "${arr[@]}", um Leerzeichen zu behandeln. declare -A erstellt assoziative Arrays (Bash 4+). Verwenden Sie read -a zum Splitten in ein Array.

bash
# Declare array
fruits=("apple" "banana" "cherry")
# Access
echo ${fruits[0]}      # apple
echo ${fruits[@]}      # all
echo ${#fruits[@]}     # length
# Iterate
for fruit in "${fruits[@]}"; do
    echo "$fruit"
done
# Associative array
declare -A config
config[key]="value"

Fehlerbehandlung

set -e beendet bei Fehler, -u bei undefinierten Variablen, pipefail fängt Pipe-Fehler ab. trap ERR behandelt Fehler. trap EXIT führt Cleanup aus. command -v prüft, ob ein Befehl existiert. Beginnen Sie Skripte immer mit set -euo pipefail.

bash
set -e  # Exit on error
set -u  # Error on undefined variable
set -o pipefail  # Pipeline fails if any command fails
set -euo pipefail  # All three (recommended)
trap 'echo "Error on line $LINENO"' ERR
trap 'cleanup' EXIT
# Check command result
if ! command -v docker &> /dev/null; then
    echo "docker not installed"
fi
24

Datei-Operationen Deep

find Fortgeschritten

find durchsucht Dateien nach Kriterien. -type f/d für Dateien/Verzeichnisse. -size +1M größer als 1MB. -mtime -7 innerhalb von 7 Tagen modifiziert. -exec führt einen Befehl auf jedem Treffer aus. {} ist der Dateiname; \; beendet den Befehl.

bash
# By name
find . -name "*.txt"
# By type and size
find . -type f -size +1M
# By modification time
find . -mtime -7  # Modified in last 7 days
# Execute command
find . -name "*.log" -exec gzip {} \;
# Delete
find . -name "*.tmp" -delete

tar & Kompression

tar kombiniert Dateien; -z komprimiert mit gzip. -c create, -x extract, -t list, -v verbose, -f Dateiname. Für .tar.bz2 verwenden Sie -j. zip/unzip behandeln ZIP-Format. Verwenden Sie -C zum Extrahieren in ein spezifisches Verzeichnis.

bash
# Create tar.gz
tar -czvf archive.tar.gz dir/
# Extract
tar -xzvf archive.tar.gz
# List contents
tar -tzvf archive.tar.gz
# zip/unzip
zip -r archive.zip dir/
unzip archive.zip

rsync

rsync synchronisiert Dateien effizient und überträgt nur Unterschiede. -a Archiv-Modus (erhält Attribute), -v verbose, -z komprimieren. --delete entfernt Dateien, die nicht in der Quelle sind. Der nachfolgende Schrägstrich auf src/ ist wichtig: mit ihm kopiert es Inhalte; ohne kopiert es das Verzeichnis.

bash
# Sync local
rsync -av src/ dest/
# Sync to remote
rsync -avz src/ user@host:/path/
# Delete files not in source
rsync -av --delete src/ dest/
# Dry run
rsync -av --dry-run src/ dest/
# Exclude
rsync -av --exclude='*.log' src/ dest/

Datei-Berechtigungen

Berechtigungen: r=4, w=2, x=1. 755 = rwxr-xr-x (Eigentümer voll, Andere lesen/ausführen). 644 = rw-r--r-- (Dateien). u/g/o/a = Benutzer/Gruppe/Andere/Alle. chmod -R rekursiert. chown ändert Eigentümerschaft.

bash
# Symbolic
chmod u+x file      # User execute
chmod g-w file      # Group write off
chmod a=r file      # All read only
# Numeric
chmod 755 file      # rwxr-xr-x
chmod 644 file      # rw-r--r--
# Change owner
chown user:group file
# Recursive
chmod -R 755 dir/

ln & Symlinks

Hardlinks zeigen auf dasselbe Inode (nur dasselbe Dateisystem). Symbolische Links (-s) zeigen auf einen Pfad. Hardlinks überleben die Löschung des Originals. Symlinks brechen, wenn das Ziel verschoben wird. Verwenden Sie readlink, um Symlinks aufzulösen.

bash
# Hard link
ln file.txt hardlink.txt
# Symbolic link
ln -s /path/to/target symlink
# Find broken symlinks
find . -type l ! -exec test -e {} \; -print
# Copy symlink target
cp -L symlink dest/
25

Häufige Fallstricke

Unquoted Variablen

Unquoted Variablen splitten an Leerzeichen und globben. $(ls) bricht bei Dateinamen mit Leerzeichen. Verwenden Sie Globs (*.txt) oder find -print0 mit read -d. Quoten Sie immer Variablen: "$var". Verwenden Sie IFS= read -r für sicheres Zeilen-Lesen.

bash
# BUG: breaks with spaces
for f in $(ls); do echo "$f"; done
# GOOD: handle spaces
for f in *.txt; do echo "$f"; done
# Or with find
find . -name "*.txt" -print0 | while IFS= read -r -d '' f; do
    echo "$f"
done

cp vs mv

cp kopiert, mv verschiebt. Beide können ohne Warnung überschreiben. Verwenden Sie -i, um vor Überschreiben zu fragen. Verwenden Sie -n, um niemals zu überschreiben. mv ist atomar auf demselben Dateisystem, nützlich für Locks und atomare Updates.

bash
# cp: copy (original remains)
cp file.txt backup.txt
# mv: move/rename (original removed)
mv file.txt newname.txt
# Common mistake: overwriting
cp important.txt /tmp/  # If /tmp/important.txt exists, overwritten!
# Use -i for safety
cp -i file.txt /tmp/

rm-Gefahren

rm -rf ist gefährlich, besonders mit Variablen. Leere Variablen verursachen rm -rf /. Quoten und prüfen Sie immer Variablen. Ziehen Sie trash-cli für wiederherstellbares Löschen in Betracht. Führen Sie niemals rm -rf / oder mit sudo leichtsinnig aus.

bash
# DANGEROUS: recursive force
rm -rf /  # Never do this!
rm -rf $VAR/  # If $VAR is empty, becomes rm -rf /
# Safer: check first
if [ -n "$VAR" ]; then
    rm -rf "$VAR/"
fi
# Use trash-cli instead
trash-put file.txt

Shebang

Der Shebang (#!) spezifiziert den Interpreter. #!/usr/bin/env bash ist portabler als #!/bin/bash. Verwenden Sie /bin/sh für maximale Portabilität (vermeidet Bashismen). Machen Sie Skripte immer mit chmod +x ausführbar.

bash
#!/bin/bash          # bash script
#!/usr/bin/env bash  # Portable bash
#!/usr/bin/env python3  # Python
#!/bin/sh            # POSIX sh (more portable)
# Make executable
chmod +x script.sh
./script.sh

Exit-Status

$? hält den Exit-Status des letzten Befehls. 0 = Erfolg, non-zero = Fehler. Prüfen Sie sofort, bevor ein anderer Befehl ihn überschreibt. Besser, if Befehl direkt zu verwenden. Beenden Sie immer mit aussagekräftigen Status-Codes in Skripten.

bash
# Check last command
if [ $? -eq 0 ]; then
    echo "Success"
fi
# Better: direct check
if command; then
    echo "Success"
else
    echo "Failed"
fi
# Exit with status
exit 0  # Success
exit 1  # Failure

Was this helpful?

Learning path

Learn from scratch

Learn this language from the ground up with structured lessons.