Bash kann rechnen — aber anders, als man es aus C oder Python kennt. Es gibt vier verschiedene Wege, einen einfachen Ausdruck wie 5 + 3 auszuwerten, und alle haben ihre Eigenheiten. Dazu kommt eine harte Einschränkung: Bash kennt keine Gleitkommazahlen. Wer mit Brüchen oder Prozenten rechnet, ruft externe Tools wie bc oder awk. Dieser Artikel zeigt, welche Variante wann passt, welche Operatoren zur Verfügung stehen und wo die typischen Stolperfallen liegen.
(( )) — Arithmetic Compound Command
Das Arithmetic Compound Command ist die moderne, saubere Variante in Bash. Innerhalb der doppelten Klammern wird der Inhalt als arithmetischer Ausdruck ausgewertet, Variablen brauchen kein $, und das Quoting fällt weitgehend weg.
(( a = 5 + 3 ))
echo "$a"8Der Rückgabewert von (( )) ist kein Zahlenwert, sondern ein Exit-Code — und der ist umgekehrt zur intuitiven Erwartung: Ist das Resultat des Ausdrucks ungleich Null, gilt der Befehl als erfolgreich (Exit 0). Ist das Resultat Null, gilt er als fehlgeschlagen (Exit 1). Damit lässt sich (( )) direkt als Bedingung verwenden:
if (( a > 10 )); then
echo "groß"
fiVariablen werden innerhalb von (( )) automatisch dereferenziert — (( a = b + c )) reicht, das $ ist optional und stört eher. Das macht den Code lesbar und vermeidet die typischen Quoting-Fehler.
$(( )) — Arithmetic Expansion
Wenn du den Wert eines Ausdrucks brauchst — etwa zum Zuweisen, Ausgeben oder als Argument — verwendest du die Arithmetic Expansion $(( )). Sie ersetzt den Ausdruck durch sein numerisches Resultat, ähnlich wie $(...) für Command-Substitution.
result=$(( 5 + 3 ))
echo "$result"
a=4
b=7
echo "$(( a * b ))"8
28Die Faustregel ist einfach: (( )) für Anweisungen (Zuweisung, Bedingung, Counter), $(( )) für Ausdrücke, deren Wert weiterverwendet wird. Beide sind Bash-Builtins, beide sind schnell — keine Subshell, kein externer Prozess.
let
let ist ein älterer Bash-Builtin mit demselben Zweck wie (( )). Funktional identisch, aber mit zwei Nachteilen: Argumente mit Leerzeichen müssen gequotet werden, und * wird je nach Aufruf vom Glob-Mechanismus angefasst.
let "a = 5 + 3"
let a=5+3
let "c = a * b"Empfehlung: (( )) bevorzugen. Es liest sich besser, braucht keine Anführungszeichen für Leerzeichen und ist in jedem moderneren Bash-Code der Standard. let siehst du höchstens noch in alten Skripten.
expr
expr ist kein Builtin, sondern ein externer Befehl aus den Coreutils. Jeder Aufruf forkt eine Subshell und lädt das Programm — entsprechend langsam ist das in Schleifen. Außerdem ist die Syntax sperrig: Operatoren und Operanden müssen durch Leerzeichen getrennt werden, und Sonderzeichen wie * brauchen Backslash-Escape gegen die Shell.
expr 5 + 3
expr 4 \* 7
result=$(expr 10 / 3)
echo "$result"8
28
3Heute ist expr praktisch obsolet. Sinnvoll bleibt es nur in POSIX-Skripten ohne Bash (#!/bin/sh), wo (( )) und $(( )) nicht garantiert sind — wobei selbst POSIX $(( )) schon lange unterstützt.
Operatoren
In (( )) und $(( )) stehen alle aus C bekannten Operatoren zur Verfügung — inklusive Bit-Operationen, Compound-Zuweisungen und Ternär.
| Kategorie | Operatoren | Bedeutung |
|---|---|---|
| Arithmetik | + - * / % | Addition, Subtraktion, Multiplikation, ganzzahlige Division, Modulo |
| Potenz | ** | 2**10 ergibt 1024 |
| In/Dekrement | ++ -- | Pre- und Post-Form: i++, ++i |
| Bit-Shift | << >> | Links- und Rechts-Verschiebung |
| Bit-weise | & | ^ ~ | AND, OR, XOR, NOT |
| Logisch | && || ! | UND, ODER, NICHT (Kurzschluss-Auswertung) |
| Vergleich | < > <= >= == != | Liefert 1 (wahr) oder 0 (falsch) |
| Ternär | ?: | (( max = a > b ? a : b )) |
| Zuweisung | = | Einfache Zuweisung |
| Compound | += -= *= /= %= &= |= ^= <<= >>= | Operation und Zuweisung in einem Schritt |
Beispiele für die wichtigsten Compound-Operatoren:
x=10
(( x += 5 ))
echo "$x"
(( x *= 2 ))
echo "$x"
(( max = x > 50 ? x : 50 ))
echo "$max"15
30
50Bash kennt kein Float
Eine harte Wahrheit: Bash rechnet ausschließlich mit Integer. Jede Division ist eine Ganzzahl-Division, alles hinter dem Komma fällt weg.
echo "$(( 1 / 2 ))"
echo "$(( 7 / 3 ))"
echo "$(( 10 / 4 ))"0
2
2Wer Float braucht, ruft ein externes Werkzeug — meistens bc mit der Option -l (lädt die Math-Library und setzt scale), awk mit BEGIN-Block oder Python.
echo "scale=2; 1/2" | bc
bc -l <<< "scale=4; 22/7".50
3.1428awk 'BEGIN { printf "%.4f\n", 22/7 }'3.1429python3 -c "print(1/3)"0.3333333333333333Welches Tool? bc ist die klassische Wahl, in jeder Distro verfügbar. awk ist meist schon da, weil es ohnehin für Textverarbeitung gebraucht wird. python3 ist in Skripten praktisch, wenn ohnehin Python-Logik beteiligt ist — sonst Overkill.
Praxis-Patterns
Ein paar konkrete Muster, die in echten Skripten immer wieder auftauchen.
Counter inkrementieren — der Klassiker in Schleifen. i++ ist Post-Inkrement, gibt also den alten Wert zurück und erhöht danach. Innerhalb von (( )) reicht das ohne weitere Verpackung.
i=0
for f in *.log; do
(( i++ ))
done
echo "$i Dateien"Bedingungs-Counter — eine Bedingung als Zahl direkt in eine Summe einrechnen. Vergleiche liefern in (( )) 0 oder 1, und += addiert das einfach drauf.
found=0
for n in 3 7 12 4 9; do
(( found += n > 5 ))
done
echo "$found Werte über 5"Bit-Flags setzen — eine Maske mit OR-Zuweisung in eine Flag-Variable schreiben. Klassisch für Permission-Bits, Feature-Flags oder Hardware-Register.
READ=1
WRITE=2
EXEC=4
flags=0
(( flags |= READ ))
(( flags |= EXEC ))
echo "$flags"Float-Division mit bc — Hier-String an bc schicken. scale=2 bestimmt die Nachkomma-Stellen, -l aktiviert die Math-Library. Praktisch für Prozent-Berechnungen oder Mittelwerte.
ergebnis=$(bc -l <<< "scale=2; 5/3")
echo "$ergebnis"1.66Mittelwert mit awk — Werte als Variablen an awk übergeben (-v), im BEGIN-Block die Rechnung ausführen. Vorteil: keine Pipe, kein Parsing, klare Trennung von Bash-Variablen und awk-Logik.
sum=42
n=7
awk -v s="$sum" -v n="$n" 'BEGIN { print s/n }'6Basis-Konvertierung — In (( )) lassen sich Literale in beliebiger Basis schreiben: BASIS#WERT. Das macht Hex-, Oktal- oder Binär-Werte in Skripten lesbar.
(( h = 16#FF ))
echo "$h"
(( o = 8#777 ))
echo "$o"
(( b = 2#1010 ))
echo "$b"255
511
10Häufige Stolperfallen
Exit-Code von (( )) ist invertiert
(( )) liefert Exit 0, wenn das Resultat ungleich Null ist, und Exit 1, wenn es Null ist. Das ist sinnvoll als Bedingung (if (( x )) heißt „x ist ungleich 0”), kollidiert aber unangenehm mit set -e: Eine harmlose Zuweisung wie (( count = 0 )) bricht das Skript ab, weil das Resultat 0 ist. Workaround: (( count = 0 )) || true oder direkte Zuweisung mit count=0 ohne (( )).
Modulo bei negativen Zahlen ist Bash-spezifisch
Bei (( -7 % 3 )) liefert Bash -1, nicht 2. Das Vorzeichen folgt dem Dividenden — anders als in Python (-7 % 3 == 2). Wer das Verhalten von Python erwartet, baut sich einen Fehler ein. Robust ist (( ((a % m) + m) % m )), um immer einen nicht-negativen Rest zu bekommen.
* in $(( )) ist sicher, in let und expr nicht
Innerhalb von $(( a * b )) und (( a * b )) wird der Stern nicht vom Glob-Mechanismus angefasst. Bei let und besonders bei expr schlägt das Globbing zu, sobald passende Dateinamen im Verzeichnis liegen. expr 4 * 7 muss deshalb als expr 4 \* 7 geschrieben werden — sonst wird * durch die Dateiliste ersetzt und expr meldet einen Syntaxfehler.
Variablen brauchen kein $ in (( ))
(( a = b + c )) ist äquivalent zu (( a = $b + $c )) — und die erste Form ist lesbarer. Das $ schadet nicht, ist aber überflüssig. Viel wichtiger: niemals quoten innerhalb von (( )). (( "$a" + "$b" )) ist ein Syntaxfehler, weil die Quotes Teil des Ausdrucks werden.
Float-Werte aus bc mit User-Input — Code-Injection-Falle
bc parst alles, was reinkommt, als bc-Sprache — inklusive Funktionsaufrufen und Variablen-Definitionen. Wer User-Input direkt in bc <<< "$user" schickt, öffnet eine Hintertür. Beispiel: user='5; halt; print 99' wird sauber ausgeführt. Lösung: Input mit einer Regex auf Ziffern und einen einzelnen Punkt validieren, bevor er an bc geht.
$(( )) ist Integer — keine implizite Float-Promotion
Anders als in Python oder JavaScript wird in $(( )) nichts automatisch in Float konvertiert. $(( 7 / 2 )) ist 3, nicht 3.5. Wer das übersieht, baut Prozent-Berechnungen mit Off-by-Margin-Fehlern ein. Faustregel: Sobald irgendwo Division mit Brüchen vorkommt, raus aus (( )) und rein in bc oder awk.
expr trennt Operatoren mit Leerzeichen — sonst String-Konkatenation
expr 5+3 liefert nicht 8, sondern den String 5+3. expr braucht zwingend Leerzeichen zwischen jedem Operator und Operand: expr 5 + 3. Dazu kommt das Globbing für * und die Tatsache, dass jeder Aufruf einen externen Prozess startet. Drei gute Gründe, expr heute nur noch in echtem POSIX-Code zu nutzen.
Weiterführende Ressourcen
Externe Quellen
- Bash-Manpage: ARITHMETIC EVALUATION — Offizielle Spezifikation aller Operatoren und Auswertungsregeln
- GNU Bash-Handbuch: Shell Arithmetic — Vollständige Referenz mit Beispielen
- bc(1) Manpage — Arbitrary Precision Calculator, Math-Library und Skript-Modus
- awk(1) Manpage — POSIX-awk inklusive numerischer Funktionen
- BashFAQ: How can I do floating point arithmetic? — Praxis-orientierte Sammlung aller Float-Workarounds
Verwandte Artikel
- Bedingungen —
if,[[ ]]und numerische Vergleiche - Schleifen —
for,whileund C-style-Schleifen mit(( )) - Funktionen — Rückgabewerte, Parameter und lokale Variablen
- Exit-Codes — Was Exit-Codes bedeuten und warum
(( ))invertiert ist - Skript-Grundlagen — Shebang, robuster Header und
set -euo pipefail