Bash kann mit Strings deutlich mehr, als viele Skripte ausnutzen. Statt jeden Substring an sed, cut oder awk weiterzureichen, erledigt die Parameter-Expansion dieselben Aufgaben direkt in der Shell — ohne Subprocess und ohne externe Abhängigkeit. Wer die Ausdrücke {var:offset:length}, {var##prefix} und {var//pattern/replacement} beherrscht, schreibt schnellere und lesbarere Skripte.
Parameter-Expansion vs. externe Tools
Für jede einfache String-Operation hast du in Bash zwei Wege: die eingebaute Parameter-Expansion oder ein externes Tool wie sed, cut, tr oder awk. Beide liefern dasselbe Ergebnis, aber der Unterschied in der Praxis ist groß.
| Aufgabe | Externes Tool | Parameter-Expansion |
|---|---|---|
| Extension entfernen | echo "$file" | sed 's/\.[^.]*$//' | ${file%.*} |
| Basename | basename "$path" | ${path##*/} |
| Erste 3 Zeichen | echo "$s" | cut -c1-3 | ${s:0:3} |
| Klein in groß | echo "$s" | tr a-z A-Z | ${s^^} |
Externe Tools starten jedes Mal einen neuen Prozess. In einer Schleife mit tausend Durchläufen werden daraus tausend Forks, tausend Pipe-Verbindungen und tausend kurze Programmausführungen. Parameter-Expansion läuft direkt in der Shell und ist um Größenordnungen schneller. Für lange Texte oder komplexe Regex bleibt sed/awk natürlich die richtige Wahl — aber für die alltäglichen Substring- und Replace-Aufgaben gehört die Expansion in jedes Skript.
Länge eines Strings
${#var} liefert die Anzahl der Zeichen in der Variable. Das funktioniert mit jedem String — auch mit Umlauten, sofern das Locale auf UTF-8 eingestellt ist.
text="Hallo Welt"
echo "${#text} Zeichen"
leer=""
echo "leer: ${#leer}"10 Zeichen
leer: 0Bei Arrays bedeutet ${#arr[@]} die Anzahl der Elemente, ${#arr[0]} die Länge des ersten Elements. Das # ist also kontextabhängig — eindeutig nur durch die Klammer-Position.
Substring
Mit ${var:offset} schneidest du einen Teil ab Position offset heraus. Mit ${var:offset:length} zusätzlich begrenzt auf length Zeichen. Negative Offsets greifen vom Ende — aber nur mit einem Leerzeichen vor dem Minus, sonst kollidiert die Syntax mit dem Default-Operator.
s="Hallo Welt"
echo "${s:6}" # ab Position 6
echo "${s:0:5}" # erste 5 Zeichen
echo "${s:6:4}" # 4 Zeichen ab Position 6
echo "${s: -4}" # letzte 4 Zeichen (Space vor Minus!)
echo "${s: -4:3}" # 3 Zeichen, beginnend 4 vom EndeWelt
Hallo
Welt
Welt
WelDas Leerzeichen vor dem Minus ist Pflicht: ${s:-4} ohne Space ist der Default-Operator und liefert den Wert 4, falls s leer oder ungesetzt wäre. ${s: -4} mit Space ist der Substring-Operator mit negativem Offset. Diese subtile Falle ist in der Bash-Manpage explizit dokumentiert.
Prefix und Suffix entfernen
Vier Operatoren entfernen passende Teile vom Anfang oder Ende des Strings. Sie verwenden Glob-Pattern, kein Regex — * matcht beliebige Zeichen, ? ein einzelnes.
| Operator | Wirkung | Match |
|---|---|---|
${var#pattern} | Entfernt vom Anfang | kürzester passender |
${var##pattern} | Entfernt vom Anfang | längster passender |
${var%pattern} | Entfernt vom Ende | kürzester passender |
${var%%pattern} | Entfernt vom Ende | längster passender |
path="/home/user/projekt/datei.tar.gz"
echo "${path#*/}" # erstes / weg, kürzester Match
echo "${path##*/}" # alles bis letztes / weg
echo "${path%.*}" # letzte Extension weg
echo "${path%%.*}" # alle Extensions weghome/user/projekt/datei.tar.gz
datei.tar.gz
/home/user/projekt/datei.tar
/home/user/projekt/dateiEselsbrücke: # zeigt grafisch nach links (Anfang), % nach rechts (Ende) — analog zu ihrer Position auf der amerikanischen Tastatur. Verdoppelung bedeutet immer „längster Match”.
Substitution
Der Slash-Operator ersetzt Teile des Strings. Auch hier gilt: das Pattern ist ein Glob, kein Regex.
| Operator | Wirkung |
|---|---|
${var/pattern/replacement} | Ersetzt das erste Vorkommen |
${var//pattern/replacement} | Ersetzt alle Vorkommen |
${var/#pattern/replacement} | Ersetzt nur, wenn am Anfang |
${var/%pattern/replacement} | Ersetzt nur, wenn am Ende |
s="foo bar foo baz foo"
echo "${s/foo/XXX}" # nur das erste
echo "${s//foo/XXX}" # alle
echo "${s/#foo/XXX}" # nur am Anfang
echo "${s/%foo/XXX}" # nur am Ende
echo "${s// /_}" # alle Spaces zu UnderscoreXXX bar foo baz foo
XXX bar XXX baz XXX
XXX bar foo baz foo
foo bar foo baz XXX
foo_bar_foo_baz_fooWer das Replacement weglässt (${var/pattern/}), löscht das gefundene Pattern. Praktisch, um zum Beispiel alle Whitespace-Zeichen mit ${var// /} zu entfernen.
Case-Conversion
Bash 4 brachte vier Operatoren für Groß- und Kleinschreibung. Sie funktionieren ohne externe Tools wie tr, sind aber auf macOS Standard-Bash (Version 3.2) nicht verfügbar — dort hilft nur brew install bash oder weiterhin tr.
| Operator | Wirkung |
|---|---|
${var^^} | Alles in Großbuchstaben |
${var,,} | Alles in Kleinbuchstaben |
${var^} | Nur erstes Zeichen groß |
${var,} | Nur erstes Zeichen klein |
${var~~} | Alle Zeichen toggeln |
${var~} | Nur erstes Zeichen toggeln |
s="Hallo Welt"
echo "${s^^}"
echo "${s,,}"
echo "${s^}"
echo "${s,,[AEIOU]}" # nur Vokale kleinHALLO WELT
hallo welt
Hallo Welt
Hallo WltMit Pattern-Argument lassen sich gezielt nur bestimmte Zeichen umstellen. ${var^^[aeiou]} würde alle Kleinbuchstaben-Vokale in Großbuchstaben verwandeln, der Rest bleibt unverändert.
Default-Werte
Vier eng verwandte Operatoren entscheiden, was passiert, wenn eine Variable unset oder leer ist. Mit Doppelpunkt prüfen sie auf „unset oder leer”, ohne Doppelpunkt nur auf „unset”.
| Operator | Wirkung | Side-Effect? |
|---|---|---|
${var:-default} | Liefert default, wenn leer/unset | Nein |
${var:=default} | Liefert default und setzt var | Ja, ändert var |
${var:?msg} | Bricht Skript mit msg ab, wenn leer/unset | Beendet Skript |
${var:+alt} | Liefert alt, wenn var gesetzt | Nein |
unset name
echo "Hallo, ${name:-Welt}" # Default verwenden
echo "name ist immer noch: '$name'"
echo "Hallo, ${name:=Welt}" # Default verwenden + setzen
echo "name ist jetzt: '$name'"
log="aktiv"
echo "${log:+log ist gesetzt}" # nur wenn gesetztHallo, Welt
name ist immer noch: ''
Hallo, Welt
name ist jetzt: 'Welt'
log ist gesetzt${var:?Fehlermeldung} ist das robuste Pendant für Pflicht-Variablen: Wenn var nicht gesetzt ist, druckt Bash die Meldung auf stderr und beendet das Skript mit Exit-Code 1. Ideal für Konfigurations-Variablen, die zwingend in der Umgebung stehen müssen.
Indirect-Reference
Mit ${!varname} wird der Inhalt von varname als Variablen-Name interpretiert und dessen Wert geholt — eine „Variable einer Variable”. Der Mechanismus ist klassisch für dynamische Lookups, etwa wenn ein Schlüssel zur Laufzeit feststeht.
color_red="#ff0000"
color_blue="#0000ff"
color_green="#00ff00"
name="color_red"
echo "${!name}" # holt $color_red
for c in red blue green; do
key="color_$c"
echo "$c -> ${!key}"
done#ff0000
red -> #ff0000
blue -> #0000ff
green -> #00ff00In modernem Bash-Code wird das Pattern oft durch assoziative Arrays (declare -A) ersetzt — sauberer und besser nachvollziehbar. Aber für ältere Skripte oder Setups ohne Bash 4 bleibt ${!name} die Standard-Lösung.
Praxis-Patterns
Die folgenden Muster lösen wiederkehrende String-Aufgaben mit reiner Parameter-Expansion. Sie sind kürzer und schneller als externe Tools und kommen in praktisch jedem produktiven Bash-Skript vor.
Dateiname ohne Extension
file="archiv.tar.gz"
echo "${file%.*}" # archiv.tar%.* matcht den kürzesten Suffix, der mit einem Punkt beginnt. Bei archiv.tar.gz bleibt also archiv.tar übrig — die letzte Extension fällt weg. Wer alle Extensions entfernen will, nimmt ${file%%.*} (längster Match) und bekommt nur archiv.
Extension extrahieren
file="bericht.final.pdf"
echo "${file##*.}" # pdfSpiegelbildlich zum vorigen Pattern: ##*. entfernt vom Anfang den längsten Teil bis zum letzten Punkt. Übrig bleibt nur die Extension. Wenn die Datei keinen Punkt enthält, liefert der Ausdruck den ganzen Dateinamen — das musst du gegebenenfalls separat prüfen.
Pfad ohne Verzeichnis (basename-Ersatz)
path="/home/user/projekte/notiz.md"
echo "${path##*/}" # notiz.md##*/ entfernt vom Anfang alles bis einschließlich dem letzten Slash. Das Ergebnis entspricht basename "$path" — aber ohne Subprocess. In einer Schleife über tausende Dateien ist das ein spürbarer Geschwindigkeitsgewinn.
Verzeichnis ohne Datei (dirname-Ersatz)
path="/home/user/projekte/notiz.md"
echo "${path%/*}" # /home/user/projekte%/* entfernt vom Ende alles ab dem letzten Slash. Damit bekommst du das Verzeichnis — analog zu dirname. Achtung: Wenn der Pfad keinen Slash enthält, bleibt der Original-String unverändert. dirname würde in dem Fall einen Punkt liefern, die Expansion nicht.
Whitespace trimmen
text=" hallo welt "
# Vorne weg
ohne_links="${text#"${text%%[![:space:]]*}"}"
# Hinten weg
trimmed="${ohne_links%"${ohne_links##*[![:space:]]}"}"
echo "[$trimmed]"Der Klassiker mit verschachtelter Expansion: Der innere Ausdruck ${text%%[![:space:]]*} liefert den führenden Whitespace-Block, der äußere ${text#...} schneidet ihn ab. Spiegelbildlich für hinten. Mit aktiviertem extglob geht es auch in einer Zeile per ${text##+([[:space:]])}, aber die zweistufige Variante läuft überall ohne Sonder-Setup.
Lower-case für Vergleich
read -r input
if [[ "${input,,}" == yes ]]; then
echo "bestätigt"
fiStatt im case alle Schreibweisen aufzulisten (yes|Yes|YES|y|Y), normalisiert ${input,,} die Eingabe einmal zu Kleinbuchstaben. Der Vergleich gegen yes deckt damit YES, Yes, yEs und alle anderen Mischformen ab. Funktioniert ab Bash 4.
Häufige Stolperfallen
Negativer Substring-Offset braucht ein Leerzeichen
${var:-3} ist nicht „die letzten 3 Zeichen”, sondern der Default-Operator: liefert 3, wenn var leer oder unset ist. Erst ${var: -3} mit Leerzeichen vor dem Minus aktiviert den Substring-Operator mit negativem Offset. Diese Falle steht so explizit in der Bash-Manpage. Wer die letzten N Zeichen will und das Leerzeichen vergisst, bekommt entweder den unveränderten Wert oder den Default — und beide Fälle sehen oberflächlich nach „funktioniert” aus, bis der Test mit leerer Variable das Gegenteil zeigt.
Pattern in Parameter-Expansion ist Glob, nicht Regex
${var/pattern/replacement} und ${var#pattern} verwenden Shell-Glob-Pattern, nicht reguläre Ausdrücke. * matcht beliebig viele Zeichen, ? ein einzelnes, [abc] eine Zeichenklasse — aber .*, +, \d oder Anker wie ^ funktionieren nicht. Wer mit Regex aus sed oder Perl umsteigt, schreibt schnell ${var/^foo/bar} und wundert sich, warum nichts ersetzt wird (das ^ wird hier als literales Zeichen gesucht). Für echte Regex-Power weiterhin sed oder [[ $var =~ regex ]] mit Bash-Built-in-Regex.
macOS Bash 3.2 kennt kein ^^ und kein ,,
Apple liefert seit Jahren Bash 3.2 mit dem System aus — Lizenzgründe, Bash 4 steht unter GPLv3. Folge: ${var^^}, ${var,,} und auch assoziative Arrays funktionieren auf macOS Standard-Bash nicht. Skripte, die diese Features verwenden, scheitern mit bad substitution. Abhilfe: Shebang #!/usr/bin/env bash plus brew install bash, oder für maximale Kompatibilität auf tr a-z A-Z zurückfallen. Vor jedem geteilten Skript ein bash --version zur Sicherheit.
`:-` setzt nicht, `:=` schon
${var:-default} liefert den Default, ändert die Variable aber nicht — nach dem Ausdruck ist var immer noch leer oder ungesetzt. ${var:=default} weist den Default zusätzlich an var zu. Das ist ein leiser Side-Effect, der erst weiter unten im Skript auffällt, wenn man mit dem vermeintlich „leeren” var weiterrechnet. Faustregel: :- für Lese-Defaults, := nur wenn du die Zuweisung wirklich willst.
Glob-Pattern ist kein Contains-Operator
${var#foo} matcht nur, wenn der String mit foo beginnt — nicht irgendwo in der Mitte. Wer „enthält foo” will, braucht ${var#*foo*} mit Wildcards drumherum, oder besser den ==-Operator in [[ $var == *foo* ]]. Der häufigste Anfänger-Bug ist die Annahme, dass ${path#projekt} aus /home/user/projekt/datei den Teil /home/user/ liefern würde — tut es nicht, der String beginnt nicht mit projekt, also bleibt alles unverändert.
Performance bei sehr großen Strings
Parameter-Expansion arbeitet im Speicher der Shell und kopiert den String bei jeder Operation. Für Logfiles im Megabyte-Bereich oder lange Streams ist awk, sed oder grep deutlich effizienter — die laufen über Streams und brauchen den ganzen Inhalt nicht gleichzeitig im RAM. Faustregel: Bis ein paar Kilobyte ist Parameter-Expansion immer schneller (kein Fork), darüber hinaus überholen die externen Tools, weil sie bessere Algorithmen für Bulk-Verarbeitung haben.
`set -u` und Default-Operator: Reihenfolge der Auswertung
Mit set -u (oder set -o nounset) bricht das Skript bei jedem Zugriff auf eine ungesetzte Variable ab — eigentlich. Der Operator ${var:-default} umgeht diesen Check ausdrücklich: Er ist genau für „Wert oder Default” gedacht und löst kein unbound variable aus, auch wenn var nie gesetzt wurde. Das gleiche gilt für ${var:=default} und ${var:+alt}. ${var:?msg} dagegen bricht hart ab, sogar präziser als set -u, weil du eine eigene Fehlermeldung mitgeben kannst. Beide Ansätze sinnvoll kombinieren: set -u als globales Sicherheitsnetz, ${var:-default} für gewollte Defaults, ${var:?...} für Pflicht-Variablen.
Weiterführende Ressourcen
Externe Quellen
- Bash-Manpage: Parameter Expansion — Offizielle Referenz aller Expansion-Operatoren
- GNU Bash-Handbuch: Shell Parameter Expansion — Detaillierte Beschreibung mit Beispielen
- BashFAQ: How can I use parameter expansion? — Praxisorientierte FAQ mit Edge Cases
- Bash Hackers Wiki: Parameter Expansion — Sehr ausführliche Übersicht aller Formen
- Greg’s Wiki: Pattern Matching — Glob-Pattern in Bash, das Fundament der String-Expansion
Verwandte Artikel
- Variablen — Grundlagen der Variablen-Definition und Quoting
- Parameter — Argumente an Skripte und Funktionen übergeben
- Arrays — Mehrere Werte unter einem Variablennamen
- sed — Stream-Editor für komplexere Regex-Aufgaben
- Funktionen — Eigene Bash-Funktionen mit String-Verarbeitung