Bash · Shell-Scripting · Arrays · Linux
Arrays in Bash praktisch nutzen
statt String-Hacks und Leerzeichen-Fallen

String-Hacks in Bash – Dateilisten als Leerzeichen-getrennte Strings, Konfigurationswerte als kommaseparierte Felder – brechen systematisch, sobald Werte Sonderzeichen enthalten. Arrays in Bash sind die saubere Alternative: indexed Arrays für geordnete Listen, assoziative Arrays für Key-Value-Strukturen, Slices für Teilmengen und sichere Iteration für jeden Anwendungsfall.

13 Min. Lesezeit indexed · assoziativ · Iteration · Slices · Manipulation Bash 4.x · 5.x · Linux

1. Das String-Hack-Problem in der Praxis

Der klassische String-Hack in Bash-Skripten sieht harmlos aus: FILES=$(find /var/log -name "*.log") speichert Ergebnisse in einer Variable, und for f in $FILES iteriert darüber. Das funktioniert zuverlässig genau so lange, wie kein Dateiname ein Leerzeichen, einen Tab, einen Zeilenumbruch oder ein Glob-Zeichen enthält. Sobald einer dieser Fälle auftritt – und in produktiven Umgebungen tritt er irgendwann auf – bricht das Skript auf unvorhersehbare Weise. Schlimmer noch: es bricht oft nicht mit einem Fehler, sondern mit stillem, falschen Verhalten.

Der zweite häufige String-Hack ist das Speichern von Konfigurationswerten als kommaseparierte oder leerzeichen-getrennte Strings: SERVICES="nginx mysql redis". Das erscheint einfach, bis ein Service-Name einen Bindestrich oder eine Versions-Nummer enthält und die Trennung plötzlich mehrdeutig wird. Arrays in Bash lösen dieses Problem fundamental: jedes Element wird separat gespeichert, ohne Trennzeichen-Logik, und korrekt übergeben, solange man "${array[@]}" mit doppelten Anführungszeichen schreibt.

Das mentale Modell für den Umstieg von String-Hacks auf Bash-Arrays: eine Variable ist ein einzelner Wert, ein Array ist eine geordnete Liste von Werten. Wann immer man in der Beschreibung des Problems das Wort "Liste" oder "mehrere" verwendet, ist ein Array in Bash die korrekte Datenstruktur. Das gilt für Dateilisten, Dienst-Namen, IP-Adressen, Validierungsregeln, Kommandozeilen-Argumente und Konfigurationsschlüssel gleichermassen.

2. Indexed Arrays: Grundlagen und Deklaration

Indexed Arrays in Bash sind nullbasierte Listen von Strings. Die Deklaration mit declare -a name ist optional aber explizit – ohne declare -a wird Bash beim ersten Zuweisungsversuch ebenfalls ein Array anlegen. Die Syntax array=(elem1 elem2 elem3) initialisiert ein Array mit Literalwerten. Einzelne Elemente werden mit array[0]="wert" gesetzt und mit ${array[0]} gelesen. Der Zugriff auf alle Elemente erfolgt mit ${array[@]}, die Anzahl der Elemente mit ${#array[@]}. Diese vier Grundoperationen reichen für die meisten Anwendungsfälle.

Ein wichtiger Unterschied zwischen Bash-Arrays und Arrays in Programmiersprachen: Bash-Arrays müssen nicht dicht befüllt sein. Es ist völlig legitim, Index 0, 5 und 100 zu belegen ohne die Zwischenwerte. Das führt zu unerwarteten Ergebnissen beim Iterieren über numerische Indizes, wenn man annimmt, dass sie lückenlos von 0 bis n-1 laufen. Das sicherste Muster ist immer die Element-Iteration mit "${array[@]}" statt eine Index-Schleife von 0 bis ${#array[@]}, weil letztere bei Lücken die falschen Indizes anspricht.


#!/usr/bin/env bash
# arrays_basics.sh — indexed array fundamentals
set -euo pipefail

# Declaration and initialization
declare -a services=("nginx" "mysql" "redis" "php-fpm")

# Read single element
echo "First service: ${services[0]}"

# Number of elements — no subshell needed
echo "Total services: ${#services[@]}"

# Last element (Bash 4.2+)
echo "Last service: ${services[-1]}"

# All indices (not values) — useful for sparse arrays
echo "Indices: ${!services[@]}"

# Append to array
services+=("memcached")
echo "After append: ${#services[@]} services"

# Slice: elements 1 and 2 (offset=1, length=2)
slice=("${services[@]:1:2}")
echo "Slice [1:2]: ${slice[*]}"

# Unset a single element — array becomes sparse
unset 'services[2]'
echo "After unset index 2: ${#services[@]} elements"

# Safe iteration — always double-quote [@]
for svc in "${services[@]}"; do
  echo "  -> $svc"
done

3. Arrays befüllen: Literal, Schleife und find

Das sicherste Muster zum Befüllen eines Bash-Arrays mit Dateipfaden ist die Kombination aus find -print0 und einer while IFS= read -r -d '' f-Schleife. -print0 trennt Dateinamen durch das Null-Byte statt durch Newlines – das einzige Zeichen, das in einem Dateipfad unter Linux nicht vorkommen kann. read -d '' liest bis zum nächsten Null-Byte, -r verhindert Backslash-Interpretation, IFS= verhindert Whitespace-Trimming. Jeder Dateiname landet als eigenes Element im Array, korrekt – unabhängig von Sonderzeichen.

Eine weitere wichtige Befüll-Methode für Arrays in Bash: mapfile (Synonym: readarray), verfügbar seit Bash 4. mapfile -t array < <(befehl) liest jede Zeile der Ausgabe eines Befehls als ein Array-Element und entfernt den nachfolgenden Zeilenumbruch durch -t. Das ist kompakter als die while-Schleife, aber hat denselben Nachteil: Newlines im Dateinamen werden als Trenner interpretiert. Für Dateilisten mit potenziell problematischen Namen ist die find -print0-Methode weiterhin die sicherste Wahl. Für normale Befehlsausgaben wie git branch --list oder Zeilen einer Konfigurationsdatei ist mapfile -t die kompakteste Lösung.

4. Korrekte Iteration: [@] vs [*] und Quoting-Regeln

Das kritischste Detail bei der Arbeit mit Bash-Arrays ist der Unterschied zwischen "${array[@]}" und "${array[*]}". Mit doppelten Anführungszeichen expandiert @ das Array in separate, einzeln quotierte Elemente – jedes Element ist ein eigenes Argument, Leerzeichen im Elementwert werden korrekt beibehalten. Mit * dagegen werden alle Elemente zu einem einzigen String zusammengeführt, getrennt durch das erste Zeichen von IFS. Der Unterschied ist nur bei Elementen mit Leerzeichen sichtbar, aber dann fatal: for f in "${array[*]}" iteriert genau einmal über alle Elemente als einen String statt n-mal über n Elemente.

Ohne Anführungszeichen ist das Verhalten noch problematischer: for f in ${array[@]} unterliegt Word Splitting und Glob-Expansion. Ein Element "file name.txt" wird zu zwei Schleifeniterationen, ein Element "*.log" wird durch Glob-Expansion zu einer Liste von Dateien im aktuellen Verzeichnis. Das ist der Kern des String-Hack-Problems – und der Grund, warum "${array[@]}" mit doppelten Anführungszeichen das einzige korrekte Muster für die Iteration über Arrays in Bash ist.


#!/usr/bin/env bash
# iteration.sh — safe array population and iteration
set -euo pipefail

# CORRECT: null-delimited find — handles all special characters
declare -a log_files=()
while IFS= read -r -d '' f; do
  log_files+=("$f")
done < <(find /var/log -maxdepth 2 -name "*.log" -print0 2>/dev/null)

echo "Found ${#log_files[@]} log files"

# CORRECT: mapfile for normal line-based output (no filenames with newlines)
declare -a branches=()
mapfile -t branches < <(git branch --list 2>/dev/null | sed 's/^[* ] //')
echo "Git branches: ${#branches[@]}"

# CORRECT: iterate with double-quoted [@] — always
for f in "${log_files[@]}"; do
  size=$(stat --format="%s" "$f" 2>/dev/null || echo 0)
  echo "  $f ($size bytes)"
done

# WRONG (for illustration — do NOT use):
# for f in ${log_files[*]}  — splits on IFS, glob-expands
# for f in "${log_files[*]}" — one giant string instead of N elements

# Passing array to command — each element as separate argument
if [[ ${#log_files[@]} -gt 0 ]]; then
  ls -lh "${log_files[@]}"
fi

5. Array-Slices, Länge, letztes Element und Subarrays

Die Slice-Syntax für Bash-Arrays folgt demselben Prinzip wie die Substring-Expansion für Strings: ${array[@]:offset:length} gibt length Elemente ab Position offset zurück. Das Ergebnis ist eine Liste von Elementen, die man in ein neues Array zuweisen kann: sub=("${array[@]:2:5}"). Lässt man length weg, erhält man alle Elemente von offset bis zum Ende. Das Muster ${array[@]: -1} (Leerzeichen vor dem Minus ist Pflicht, damit Bash es nicht als Parameter-Expansion für den Default-Wert interpretiert) liefert das letzte Element in Bash vor 4.2. Ab Bash 4.2 geht auch ${array[-1]} direkt.

Batch-Verarbeitung mit Array-Slices ist ein mächtiges Muster für ressourcenintensive Operationen: statt alle Elemente gleichzeitig zu verarbeiten, nimmt man immer eine feste Anzahl (batch_size=10) und iteriert mit einer for ((i=0; i<${#arr[@]}; i+=batch_size))-Schleife. In jeder Iteration extrahiert man den aktuellen Batch als Subarray und verarbeitet ihn. Dieses Muster schützt vor der Erschöpfung von Ressourcen bei sehr großen Listen und ermöglicht Fortschrittsanzeigen zwischen den Batches.

6. Arrays manipulieren: Anhängen, Entfernen, Sortieren

Das Anhängen an ein Bash-Array ist mit array+=(neues_element) eine einzelne Operation. Das Entfernen eines Elements ist weniger direkt: unset 'array[index]' entfernt das Element, lässt aber eine Lücke im Array zurück. Wer ein dichtes Array ohne Lücken braucht, muss das Array nach dem Entfernen neu aufbauen: array=("${array[@]/zu_entfernendes_element/}") entfernt alle Vorkommen eines Wertes durch leere Substitution, lässt aber leere Elemente zurück. Sauberer ist ein Filter-Muster mit einer Schleife, die alle Elemente außer dem zu entfernenden in ein neues Array kopiert.

Sortieren von Bash-Arrays erfordert einen Umweg über externe Tools, da Bash keine eingebaute Sort-Funktion für Arrays hat. Das Standardmuster: mapfile -t sorted < <(printf '%s\n' "${array[@]}" | sort) gibt alle Elemente an sort weiter und liest das Ergebnis zeilenbasiert zurück in ein neues Array. Für numerische Sortierung: sort -n. Für Rückwärtssortierung: sort -r. Deduplizierung: sort -u. Das Muster ist einfach und effizient – und man muss kein eigenes Sortieralgorithmus in Bash implementieren.


#!/usr/bin/env bash
# manipulation.sh — array manipulation patterns
set -euo pipefail

declare -a servers=("web01" "web02" "db01" "cache01" "web03")

# Append
servers+=("monitor01")
echo "After append: ${servers[*]}"

# Filter: remove all "web*" entries (rebuild without matching elements)
declare -a non_web=()
for s in "${servers[@]}"; do
  [[ "$s" != web* ]] && non_web+=("$s")
done
echo "Non-web: ${non_web[*]}"

# Deduplicate while preserving order
declare -A seen=()
declare -a unique=()
for s in "${servers[@]}"; do
  [[ -v seen["$s"] ]] && continue
  seen["$s"]=1
  unique+=("$s")
done
echo "Unique count: ${#unique[@]}"

# Sort array
declare -a sorted=()
mapfile -t sorted < <(printf '%s\n' "${servers[@]}" | sort)
echo "Sorted: ${sorted[*]}"

# Reverse sort numerically
declare -a numbers=(42 7 19 3 100 55)
declare -a num_sorted=()
mapfile -t num_sorted < <(printf '%d\n' "${numbers[@]}" | sort -rn)
echo "Numeric reverse: ${num_sorted[*]}"

# Find index of an element
target="db01"
for i in "${!servers[@]}"; do
  [[ "${servers[$i]}" == "$target" ]] && echo "Found '$target' at index $i"
done

7. Assoziative Arrays: Key-Value ohne externe Tools

Assoziative Arrays in Bash (seit Bash 4, deklariert mit declare -A) sind Key-Value-Strukturen, bei denen die Schlüssel beliebige Strings sein können. Sie ersetzen eine Klasse von Shell-Skripten, die vorher auf externe Tools wie awk, sed oder sogar temporäre Dateien angewiesen waren, um Key-Value-Paare zu verwalten. Die Syntax declare -A config=([host]="localhost" [port]="3306") initialisiert ein assoziatives Array. Zugriff über ${config[host]}, alle Schlüssel über ${!config[@]}, alle Werte über ${config[@]}.

Ein praktischer Anwendungsfall für assoziative Bash-Arrays: Zähler für Häufigkeiten. Statt externe Werkzeuge zu bemühen, kann man Vorkommnisse direkt in einem assoziativen Array zählen: ((counter["$key"]++)). Das funktioniert für Log-Analyse, Statistiken über Deployment-Ergebnisse oder Häufigkeitsverteilungen von Konfigurationswerten. Ein zweiter Anwendungsfall: Lookup-Tabellen für schnellen Mitgliedschaftstest. [[ -v seen["$element"] ]] prüft, ob ein Schlüssel existiert, in O(1) statt durch lineare Suche in einem indexed Array.


#!/usr/bin/env bash
# assoc_arrays.sh — associative array patterns
set -euo pipefail

# Database configuration as associative array
declare -A db=(
  [host]="db.internal"
  [port]="3306"
  [name]="magento"
  [user]="app_user"
)

echo "Connecting to ${db[host]}:${db[port]}/${db[name]}"

# Check if key exists
if [[ -v db[password] ]]; then
  echo "Password configured"
else
  echo "[WARN] No database password set — check env variables"
fi

# Iterate over all key-value pairs
echo "--- Database config ---"
for key in "${!db[@]}"; do
  printf "  %-12s = %s\n" "$key" "${db[$key]}"
done

# Frequency counter — count log levels in a file
declare -A level_count=()
while IFS= read -r line; do
  for level in ERROR WARNING INFO DEBUG; do
    if [[ "$line" == *"[$level]"* ]]; then
      ((level_count["$level"]++)) || true
      break
    fi
  done
done < /var/log/app.log 2>/dev/null || true

echo "--- Log level frequencies ---"
for level in ERROR WARNING INFO DEBUG; do
  printf "  %-10s %d\n" "$level" "${level_count[$level]:-0}"
done

# Lookup table for environment-specific config
declare -A deploy_host=(
  [dev]="dev.mironsoft.local"
  [staging]="staging.mironsoft.de"
  [prod]="mironsoft.de"
)
ENV="${DEPLOY_ENV:-dev}"
echo "Target host: ${deploy_host[$ENV]:-unknown}"

8. Arrays an Funktionen übergeben und zurückgeben

Das Übergeben von Arrays in Bash an Funktionen ist eines der Themen, das in vielen Anleitungen falsch erklärt wird. Bash-Arrays können nicht direkt wie in Programmiersprachen als Parameter übergeben werden – myfunc "${my_array}" übergibt nur das erste Element. Das korrekte Muster ist myfunc "${my_array[@]}", wobei die Funktion die Elemente als "$@" empfängt und intern in ein lokales Array kopiert: local -a items=("$@"). Das funktioniert, solange die Funktion genau ein Array entgegennimmt.

Für das Zurückgeben von Arrays aus Bash-Funktionen gibt es zwei Muster. Erstens: die Funktion gibt Werte via echo aus und der Aufrufer fängt sie mit mapfile -t result < <(myfunc) auf – einfach, aber erzeugt eine Subshell. Zweitens: Namenreferenzen (local -n outarray="$1", Bash 4.3+) – die Funktion erhält den Variablennamen als ersten Parameter und schreibt direkt in das Array des Aufrufers. Das zweite Muster ist effizienter, erfordert aber Bash 4.3 und sorgfältiges Naming (kein Namenskonflikt zwischen Namenreferenz und lokalem Parameter).

9. String-Hacks vs. Arrays im direkten Vergleich

Der Vergleich zeigt deutlich, warum Arrays in Bash String-Hacks in nahezu jedem Anwendungsfall überlegen sind.

Aufgabe String-Hack (unsicher) Array (korrekt) Vorteil
Dateiliste FILES=$(find …) find -print0 | read -d'' Sonderzeichen und Leerzeichen sicher
Dienst-Liste SVCS="nginx mysql" declare -a svcs=(…) Saubere Iteration, keine Trennzeichen-Logik
Key-Value CONFIG="host=db:port=3306" declare -A cfg=([host]=db) Direkter Zugriff per Schlüssel, keine Parsing-Logik
Deduplizierung echo "$str" | sort -u declare -A seen; [[ -v seen[$k] ]] O(1) Lookup, kein Kindprozess
Batch-Verarbeitung Kaum möglich ohne tmpfile ${array[@]:i:batch_size} Slice-Syntax, kein Hilfsprogramm nötig

In der Praxis ist der häufigste Grund, warum Entwickler trotzdem auf String-Hacks zurückgreifen, die Gewohnheit und die vermeintlich kompaktere Schreibweise. Tatsächlich ist die Zeile mapfile -t arr < <(befehl) nicht länger als VAR=$(befehl) – aber korrekt. Mit set -euo pipefail und ShellCheck werden String-Hack-Muster als Warnungen markiert, was den Umstieg auf Bash-Arrays unterstützt.

Mironsoft

Shell-Automatisierung, DevOps-Tooling und Deployment-Infrastruktur

Bash-Skripte mit fragilen String-Hacks ersetzen?

Wir analysieren bestehende Shell-Skripte auf String-Hack-Muster, ersetzen sie durch robuste Array-basierte Lösungen und integrieren ShellCheck in eure Pipeline – damit Sonderzeichen nie wieder ein Deployment stoppen.

Array-Refactoring

String-Hacks durch indexed und assoziative Arrays ersetzen

ShellCheck-Integration

Statische Analyse in CI einbauen und bestehende Warnungen beheben

Code-Review

Manuelle Prüfung auf versteckte Quoting- und Array-Fehler

10. Zusammenfassung

Arrays in Bash praktisch nutzen statt String-Hacks bedeutet: die Datenstruktur wählen, die dem Problem entspricht. Indexed Arrays für geordnete Listen von Elementen, assoziative Arrays für Key-Value-Strukturen, Array-Slices für Teilmengen. Das Schlüsselmuster ist immer "${array[@]}" mit doppelten Anführungszeichen – das ist der einzige Unterschied zwischen einem Skript, das mit Sonderzeichen funktioniert, und einem, das bricht. find -print0 mit read -r -d '' ist das Standardmuster für Dateilisten. mapfile -t für zeilenbasierte Ausgaben.

Assoziative Arrays seit Bash 4 sind oft die eleganteste Lösung für Probleme, bei denen man früher awk, temporäre Dateien oder komplexe String-Parsing-Logik gebraucht hätte. Die häufigsten Anwendungsfälle: Lookup-Tabellen für schnellen O(1)-Mitgliedschaftstest, Zähler für Häufigkeiten und Konfigurationsobjekte mit benannten Feldern. Mit ShellCheck in der CI-Pipeline werden String-Hack-Muster automatisch als Warnungen markiert, was die schrittweise Migration auf saubere Array-Muster in Bash systematisch unterstützt.

Arrays in Bash nutzen — Das Wichtigste auf einen Blick

Indexed Arrays

find -print0 | read -r -d '' befüllt sicher. Immer "${array[@]}" mit doppelten Anführungszeichen iterieren. ${array[@]:offset:len} für Slices.

Assoziative Arrays

declare -A (Bash 4+). Schlüssel mit ${!arr[@]}, Werte mit ${arr[@]}. [[ -v arr[key] ]] für O(1)-Existenzprüfung.

[@] vs [*]

@ mit doppelten Anführungszeichen: separate quotierte Elemente. * fasst alles zu einem String zusammen. Für Iteration: immer "${array[@]}".

mapfile

mapfile -t arr < <(befehl) — kompakt und korrekt für zeilenbasierte Ausgaben. Kein manuelles while-read nötig. -t entfernt den Zeilenumbruch.

11. FAQ: Arrays in Bash praktisch nutzen

1${array[@]} vs. ${array[*]}?
@ mit doppelten Anführungszeichen: separate quotierte Elemente, Leerzeichen innerhalb eines Elements bleiben erhalten. * fasst alles zu einem IFS-getrennten String zusammen. Für Iteration immer "${array[@]}".
2mapfile funktioniert nicht in Pipe?
Pipes laufen in Subshells – Variablen-Änderungen sind außen nicht sichtbar. Process Substitution verwenden: mapfile -t arr < <(cmd). Läuft mapfile im aktuellen Shell-Kontext.
3Leeres Array prüfen?
[[ ${#array[@]} -eq 0 ]] für Anzahl. [[ -z ${array+x} ]] prüft ob die Variable überhaupt deklariert ist. Bei set -u die zweite Form sicherer, wenn das Array nicht garantiert deklariert wurde.
4Arrays in POSIX sh verfügbar?
Nein – Arrays sind nicht POSIX. Nur Bash, ksh, zsh und ähnliche erweiterte Shells. Für Bash-spezifische Skripte (#!/usr/bin/env bash) uneingeschränkt empfehlenswert.
5Array an Funktion übergeben?
myfunc "${array[@]}" übergibt alle Elemente separat. In der Funktion: local -a local_arr=("$@"). Für Array-Rückgabe: Namenreferenzen mit local -n ref=$1 (Bash 4.3+).
6Wann assoziativ statt indexed?
Bei Named-Zugriff: Konfigurationsfelder, Lookup-Tabellen, Zähler. Indexed für geordnete Sequenzen und Batch-Verarbeitung. Assoziative Arrays erfordern Bash 4 (declare -A).
7Duplikate aus Array entfernen?
Assoziatives Seen-Array: for e in "${arr[@]}"; do [[ -v seen[$e] ]] && continue; seen[$e]=1; unique+=($e); done. Oder: mapfile -t arr < <(printf '%s\n' "${arr[@]}" | sort -u) — sortiert aber neu.
8Konfigurationsdatei in assoziatives Array?
while IFS='=' read -r key value; do config["$key"]="$value"; done < config.ini. Kommentarzeilen mit [[ "$key" == '#'* ]] && continue überspringen.
9find -print0 sicherer als ohne?
-print0 trennt durch Null-Byte — das einzige Zeichen, das in einem Dateipfad nie vorkommt. Newlines können in Dateinamen legal sein. read -d '' liest bis zum Null-Byte.
10Array numerisch sortieren?
mapfile -t sorted < <(printf '%d\n' "${array[@]}" | sort -n). Rückwärts: sort -rn. Bash hat keine eingebaute Sortierfunktion – sort ist das Standard-Werkzeug.