ShellCheck ist der De-facto-Standard für statische Analyse von Shell-Skripten. Während bash -n nur reine Syntaxfehler findet, geht ShellCheck eine Stufe tiefer: fehlende Anführungszeichen, kaputtes Word-Splitting, riskante Vergleichsoperatoren, vergessene Quotes um $@ und Dutzende weitere Klassiker. Wer einmal mit ShellCheck gearbeitet hat, will nicht mehr ohne — und wundert sich, wie viele subtile Bugs in scheinbar funktionierenden Skripten schlummern.
Was ShellCheck ist
ShellCheck ist ein statischer Linter für Shell-Skripte: Ein Programm, das den Quellcode liest, analysiert und Probleme meldet, ohne das Skript auszuführen. Es ist in Haskell geschrieben, kostenlos und Open Source unter der GPLv3, gepflegt vom Entwickler Vidar Holen seit 2012.
Der praktische Wert: Erfahrungsgemäß findet ShellCheck den größten Teil der typischen Bash-Bugs, ohne dass auch nur eine Zeile ausgeführt wird. Quoting-Fehler, Word-Splitting-Probleme, falsch verwendete Built-ins, vergessene || exit nach cd — die ganze klassische Fehlerliste deckt der Linter ab. Für jeden gefundenen Punkt liefert er einen SC-Code (z. B. SC2086) mit einer ausführlichen Erklärung im zugehörigen Wiki.
ShellCheck konzentriert sich auf Bash und POSIX-Shell. Für Zsh und Fish ist die Unterstützung minimal — beide Shells haben eigenständige Syntax-Bereiche, die der Linter nicht versteht.
Installation
ShellCheck ist auf allen großen Distributionen paketiert und auch als Standalone-Binary verfügbar.
| System | Befehl |
|---|---|
| Debian/Ubuntu | sudo apt install shellcheck |
| Fedora | sudo dnf install ShellCheck |
| Arch | sudo pacman -S shellcheck |
| openSUSE | sudo zypper install ShellCheck |
| macOS (Homebrew) | brew install shellcheck |
| Alpine | apk add shellcheck |
| Online | shellcheck.net — Browser-Variante ohne Installation |
Wer keinen Root-Zugriff hat oder eine bestimmte Version braucht, lädt die Binaries direkt vom GitHub-Release-Bereich des Projekts. Die Online-Variante ist praktisch für schnelle Checks oder Code-Reviews am fremden Rechner — sie schickt das Skript allerdings über eine fremde Webseite, was bei Skripten mit Firmen-Internas problematisch sein kann.
Verwendung
Der einfachste Aufruf ist shellcheck script.sh. Findet ShellCheck Probleme, listet er sie mit Zeilennummer, SC-Code, Severity und einer kurzen Erklärung.
shellcheck demo.shIn demo.sh line 4:
cp $datei /backup/
^----^ SC2086 (info): Double quote to prevent globbing and word splitting.
Did you mean:
cp "$datei" /backup/
For more information:
https://www.shellcheck.net/wiki/SC2086 -- Double quote to prevent...Jeder Befund hat einen SC-Code mit eigenem Wiki-Eintrag — dort steht eine ausführliche Erklärung mit Hintergrund, Beispielen und Workarounds. Die Severity-Stufen sind error, warning, info und style. Findet ShellCheck einen Fehler, endet er mit Exit-Code ungleich 0 — perfekt zur Integration in Pre-Commit-Hooks und CI.
Was ShellCheck typisch findet
Eine kleine Auswahl der häufigsten Fundstellen — mit den passenden SC-Codes:
| SC-Code | Problem | Beispiel |
|---|---|---|
| SC2086 | Variablen ohne Quotes (Word-Splitting, Globbing) | cp $datei /tmp statt cp "$datei" /tmp |
| SC2068 | $@ ohne Quotes | mein_cmd $@ statt mein_cmd "$@" |
| SC2128 | Array ohne Index expandiert | echo $arr statt echo "${arr[@]}" |
| SC2164 | cd ohne Fehlerbehandlung | cd /tmp statt cd /tmp || exit |
| SC2069 | Falsche Reihenfolge bei Redirects | cmd 2>&1 > file statt cmd > file 2>&1 |
| SC2002 | „Useless use of cat” | cat datei | grep x statt grep x datei |
| SC2044 | Word-Splitting in for-Schleifen | for f in $(ls) statt for f in * |
| SC2162 | read ohne -r | read line statt read -r line |
| SC2034 | Variable zugewiesen, nie verwendet | name="x" ohne weitere Verwendung |
| SC2046 | Word-Splitting bei Command-Substitution | cmd $(other) statt cmd "$(other)" |
Jeder dieser Fälle ist eine Quelle realer Bugs in Produktion. Besonders SC2086 (fehlende Quotes) ist der Klassiker: In neun von zehn Skripten ohne ShellCheck-Lauf findet sich mindestens eine Stelle, die mit Datei- oder Verzeichnisnamen mit Leerzeichen leise zerbricht.
Konfiguration per Annotation
ShellCheck akzeptiert spezielle Kommentare im Skript, mit denen du das Verhalten lokal oder global steuerst. Sie beginnen alle mit # shellcheck.
#!/usr/bin/env bash
# shellcheck shell=bashDie shell=-Direktive sagt ShellCheck explizit, welche Shell er annehmen soll. Sinnvoll, wenn der Shebang fehlt oder ungewöhnlich ist (etwa bei Library-Dateien ohne Shebang). Mögliche Werte: bash, sh, dash, ksh, bats.
# shellcheck disable=SC2086
rm $filesDie disable=-Direktive schaltet bestimmte Codes ab — entweder vor einer einzelnen Zeile oder am Anfang der Datei für das ganze Skript. Mehrere Codes durch Komma trennen: disable=SC2086,SC2034.
# shellcheck source=lib/helpers.sh
source "$SCRIPT_DIR/lib/helpers.sh"ShellCheck folgt source-Anweisungen normalerweise nicht (zu viel Magie mit Variablen-Pfaden). Mit source= zeigst du ihm explizit, welche Datei eingebunden wird — er prüft sie dann mit und versteht die dort definierten Variablen. Pflicht in größeren Projekten mit ausgelagerten Library-Dateien.
Globale Defaults gehen über ~/.shellcheckrc oder .shellcheckrc im Projekt-Root:
# Default: Bash annehmen
shell=bash
# SC2034 immer ignorieren
disable=SC2034
# External sources erlauben
external-sources=trueDiese Datei ist ideal für Team-Projekte: Alle teilen dieselbe Konfiguration, keiner muss sich Flags merken.
Integration in Editor, Pre-Commit und CI
ShellCheck wird erst dann zum echten Helfer, wenn er automatisch läuft — nicht nur, wenn man dran denkt. Drei Ebenen lohnen sich:
Editor. Plugins zeigen Fundstellen direkt im Code mit Unterstreichung und Tooltip:
| Editor | Plugin |
|---|---|
| VS Code | timonwong.shellcheck |
| Vim/Neovim | dense-analysis/ale, vim-syntastic/syntastic |
| JetBrains | Eingebaut, ShellCheck-Pfad in den Settings setzen |
| Sublime Text | SublimeLinter-shellcheck |
| Emacs | flycheck |
Pre-Commit-Hook. Ein Skript darf nur ins Repo, wenn ShellCheck zufrieden ist. Klassisch über das Framework pre-commit (Python) oder Husky (Node):
repos:
- repo: https://github.com/koalaman/shellcheck-precommit
rev: v0.10.0
hooks:
- id: shellcheckCI. In GitHub Actions reicht eine fertige Action:
- name: ShellCheck
uses: ludeeus/action-shellcheck@master
with:
severity: warningDamit prüft jeder Push und jeder Pull Request automatisch alle Shell-Skripte. Bei einem Fund schlägt der Build fehl — der Bug kann gar nicht erst gemerged werden.
Praxis-Patterns
Die folgenden Aufrufe und Tricks decken den Alltag mit ShellCheck ab.
Erst-Lauf über alle Skripte
shellcheck *.shDer Glob expandiert alle .sh-Dateien im aktuellen Verzeichnis. ShellCheck verarbeitet sie alle und meldet Funde aus allen Dateien mit Pfad-Prefix. Erste Anlaufstelle in einem fremden oder gewachsenen Skript-Bestand.
Auf eine Severity einschränken
shellcheck -S error script.shBei einem Skript mit hundert Findings kann es sinnvoll sein, sich erst auf die echten Fehler zu konzentrieren und Style-Hinweise zunächst zu ignorieren. -S akzeptiert error, warning, info und style — die Schwelle filtert alles unterhalb aus.
JSON-Output für Maschinen
shellcheck -f json script.sh | jq '.'Mit -f json produziert ShellCheck strukturierte Ausgabe statt menschenlesbarem Text — perfekt für CI-Reporter, die Funde aufbereitet darstellen wollen. Weitere Formate: gcc (compatible mit Compiler-Output für Editor-Integration), checkstyle, diff und tty (Default).
Ignore mit Begründung
# shellcheck disable=SC2086 # globbing hier beabsichtigt
rm $files_patternWer eine Regel ausschaltet, sollte immer einen Kommentar dazu schreiben, warum. In sechs Monaten weiß sonst niemand mehr, ob das disable= notwendig oder nur faul war. Der Kommentar nach dem # zählt nicht zur Direktive — er ist normaler Code-Kommentar.
Sourced Libraries explizit angeben
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=lib/helpers.sh
source "$SCRIPT_DIR/lib/helpers.sh"Der SCRIPT_DIR-Ausdruck ist für ShellCheck zur Analyse-Zeit eine Black Box — er kann dem Pfad nicht selbst folgen. Mit # shellcheck source=lib/helpers.sh zeigst du ihm den Pfad explizit (relativ zum Skript), damit er die Library mitprüft und dort definierte Variablen kennt.
Im Pre-Commit-Hook über alle geänderten Skripte
find . -name '*.sh' -not -path './node_modules/*' -exec shellcheck {} +Ein einfacher Aufruf, der alle Shell-Skripte im Projekt findet und in einem ShellCheck-Aufruf prüft. -exec ... {} + ist effizienter als \;, weil ShellCheck nur einmal startet und mehrere Dateien auf einmal verarbeitet. Für sehr große Repos lohnt sich git diff --cached --name-only --diff-filter=ACM | grep '\.sh$' | xargs shellcheck, das nur die geänderten Dateien prüft.
Besonderheiten
Das SC-Code-Wiki ist Pflichtlektüre
Jeder Befund verweist auf einen Artikel unter shellcheck.net/wiki/SCxxxx. Diese Einträge sind kein nüchternes Regelwerk, sondern oft kleine Essays über Bash-Eigenheiten — mit Beispielen, Hintergrund und sauberen Lösungsvorschlägen. Wer fünf SC-Codes nachgelesen hat, schreibt automatisch besseres Bash. Die Liste ist auch ohne konkreten Anlass lesenswert: durchscrollen, ein paar Codes lesen, und du verstehst die Bash deutlich besser.
Manche Warnungen sind Geschmacksache
Nicht jeder Fund ist ein Bug. SC2034 (variable assigned but never used) ist legitim bei Variablen, die nur durch source von außen gelesen werden. SC2155 (declare and assign separately) ist Stilfrage. SC2317 (unreachable command) kann bei Trap-Handlern falsch positiv ansprechen. Solche Fälle gezielt mit # shellcheck disable= und Kommentar deaktivieren — keine Warnung pauschal global ausschalten, ohne sie verstanden zu haben.
False Positives sind selten, aber möglich
ShellCheck ist konservativ — er meldet im Zweifel, statt zu schweigen. In sehr dynamischer Bash (etwa mit eval, indirekten Variablen-Referenzen über ${!var}, oder Bash-Versionen-spezifischen Features) kann er Dinge anmahnen, die korrekt sind. In solchen Fällen: lokales disable= mit Begründung. Pauschal misstrauen sollte man dem Linter nicht — die echte False-Positive-Rate ist niedrig.
Begrenzte Unterstützung für Zsh und Fish
ShellCheck versteht POSIX-Shell, Bash, Dash, Ksh und Bats. Zsh-spezifische Syntax (Glob-Qualifier, =()-Substitutionen, setopt) wird nicht erkannt; Fish wird gar nicht unterstützt. Für Zsh-Skripte gibt es keine echte Alternative — manuelles Reviewing und Tests bleiben dort der Hauptweg. Für Bats-Tests dagegen funktioniert ShellCheck gut, weil Bats syntaktisch nahe an Bash ist.
Bash-Versions-Prüfung mit shell=bash
Der Direktiv # shellcheck shell=bash zwingt die Bash-Annahme — auch wenn der Shebang /bin/sh sagt. Praktisch, wenn du POSIX-Skripte schreibst, sie aber gegen Bash-Features prüfen willst, oder umgekehrt: bei shell=sh warnt ShellCheck vor Bash-only-Konstrukten wie [[ ... ]] oder Arrays, die in einer reinen POSIX-Shell nicht funktionieren würden.
external-sources für mehrteilige Projekte
Mit --external-sources (oder external-sources=true in .shellcheckrc) folgt ShellCheck source-Anweisungen auch in Dateien, die nicht im aktuellen Pfad liegen. Pflicht für Projekte mit ausgelagerten Library-Skripten — sonst meldet der Linter undefinierte Variablen, die in der Library sehr wohl deklariert sind. Aus Sicherheitsgründen ist das Default off.
Kombination mit shfmt als Formatter
ShellCheck ist ein Linter — kein Formatter. Für die Code-Optik gibt es shfmt, das Skripte konsistent einrückt, Quotes normalisiert und Whitespace ordnet. Die typische Pre-Commit-Kette: shfmt -w (formatieren), dann shellcheck (linten). Beide Tools verstehen sich gut und ergänzen sich, weil shfmt sich um Form und ShellCheck um Inhalt kümmert.
Weiterführende Ressourcen
Externe Quellen
- shellcheck.net — Online-Tester direkt im Browser
- GitHub: koalaman/shellcheck — Hauptprojekt mit Releases und Issue-Tracker
- ShellCheck Wiki: SC-Codes — Vollständige Liste aller Checks mit Erklärungen
- shfmt (mvdan/sh) — Begleitender Formatter für Shell-Skripte
- pre-commit Framework — Verwaltung mehrsprachiger Pre-Commit-Hooks
Verwandte Artikel
- Shell-Scripting Debugging —
set -x,bash -nund Trace-Strategien - Shell-Scripting Fehlerbehandlung —
set -e,trapund Cleanup-Patterns - Shell-Scripting Skript-Grundlagen — Shebang, Header und Ausführungsmodi
- Shell-Scripting Funktionen — Funktionen,
returnund lokale Variablen - Shell-Scripting Exit-Codes —
$?,pipefailund Status-Auswertung