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ß.

AufgabeExternes ToolParameter-Expansion
Extension entfernenecho "$file" | sed 's/\.[^.]*$//'${file%.*}
Basenamebasename "$path"${path##*/}
Erste 3 Zeichenecho "$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.

Bash String-Länge ermitteln
text="Hallo Welt"
echo "${#text} Zeichen"

leer=""
echo "leer: ${#leer}"
Output
10 Zeichen
leer: 0

Bei 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.

Bash Substring extrahieren
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 Ende
Output
Welt
Hallo
Welt
Welt
Wel

Das 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.

OperatorWirkungMatch
${var#pattern}Entfernt vom Anfangkürzester passender
${var##pattern}Entfernt vom Anfanglängster passender
${var%pattern}Entfernt vom Endekürzester passender
${var%%pattern}Entfernt vom Endelängster passender
Bash Prefix und Suffix abschneiden
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 weg
Output
home/user/projekt/datei.tar.gz
datei.tar.gz
/home/user/projekt/datei.tar
/home/user/projekt/datei

Eselsbrü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.

OperatorWirkung
${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
Bash String ersetzen
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 Underscore
Output
XXX bar foo baz foo
XXX bar XXX baz XXX
XXX bar foo baz foo
foo bar foo baz XXX
foo_bar_foo_baz_foo

Wer 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.

OperatorWirkung
${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
Bash Case-Conversion
s="Hallo Welt"
echo "${s^^}"
echo "${s,,}"
echo "${s^}"
echo "${s,,[AEIOU]}"   # nur Vokale klein
Output
HALLO WELT
hallo welt
Hallo Welt
Hallo Wlt

Mit 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”.

OperatorWirkungSide-Effect?
${var:-default}Liefert default, wenn leer/unsetNein
${var:=default}Liefert default und setzt varJa, ändert var
${var:?msg}Bricht Skript mit msg ab, wenn leer/unsetBeendet Skript
${var:+alt}Liefert alt, wenn var gesetztNein
Bash Default-Operatoren
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 gesetzt
Output
Hallo, 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.

Bash Indirekte Variablen-Auflösung
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
Output
#ff0000
red -> #ff0000
blue -> #0000ff
green -> #00ff00

In 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

Bash Extension abschneiden
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

Bash Letzte Extension herausziehen
file="bericht.final.pdf"
echo "${file##*.}"     # pdf

Spiegelbildlich 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)

Bash Reiner Dateiname
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)

Bash Pfad-Komponente ohne Dateinamen
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

Bash Vor- und nachgestellte Whitespaces entfernen
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

Bash Case-insensitive prüfen
read -r input
if [[ "${input,,}" == yes ]]; then
    echo "bestätigt"
fi

Statt 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

  • 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
/ Weiter

Zurück zu Shell-Scripting

Zur Übersicht