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.

SystemBefehl
Debian/Ubuntusudo apt install shellcheck
Fedorasudo dnf install ShellCheck
Archsudo pacman -S shellcheck
openSUSEsudo zypper install ShellCheck
macOS (Homebrew)brew install shellcheck
Alpineapk add shellcheck
Onlineshellcheck.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.

Bash ShellCheck auf ein Skript anwenden
shellcheck demo.sh
Output
In 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-CodeProblemBeispiel
SC2086Variablen ohne Quotes (Word-Splitting, Globbing)cp $datei /tmp statt cp "$datei" /tmp
SC2068$@ ohne Quotesmein_cmd $@ statt mein_cmd "$@"
SC2128Array ohne Index expandiertecho $arr statt echo "${arr[@]}"
SC2164cd ohne Fehlerbehandlungcd /tmp statt cd /tmp || exit
SC2069Falsche Reihenfolge bei Redirectscmd 2>&1 > file statt cmd > file 2>&1
SC2002„Useless use of cat”cat datei | grep x statt grep x datei
SC2044Word-Splitting in for-Schleifenfor f in $(ls) statt for f in *
SC2162read ohne -rread line statt read -r line
SC2034Variable zugewiesen, nie verwendetname="x" ohne weitere Verwendung
SC2046Word-Splitting bei Command-Substitutioncmd $(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.

Bash Sprache erzwingen
#!/usr/bin/env bash
# shellcheck shell=bash

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

Bash Einzelne Regel deaktivieren
# shellcheck disable=SC2086
rm $files

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

Bash Source-Datei explizit angeben
# 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:

Bash .shellcheckrc — globale Konfig
# Default: Bash annehmen
shell=bash

# SC2034 immer ignorieren
disable=SC2034

# External sources erlauben
external-sources=true

Diese 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:

EditorPlugin
VS Codetimonwong.shellcheck
Vim/Neovimdense-analysis/ale, vim-syntastic/syntastic
JetBrainsEingebaut, ShellCheck-Pfad in den Settings setzen
Sublime TextSublimeLinter-shellcheck
Emacsflycheck

Pre-Commit-Hook. Ein Skript darf nur ins Repo, wenn ShellCheck zufrieden ist. Klassisch über das Framework pre-commit (Python) oder Husky (Node):

Bash .pre-commit-config.yaml — pre-commit-Framework
repos:
  - repo: https://github.com/koalaman/shellcheck-precommit
    rev: v0.10.0
    hooks:
      - id: shellcheck

CI. In GitHub Actions reicht eine fertige Action:

Bash .github/workflows/lint.yml — GitHub Actions
- name: ShellCheck
  uses: ludeeus/action-shellcheck@master
  with:
    severity: warning

Damit 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

Bash Alle Shell-Skripte auf einmal prüfen
shellcheck *.sh

Der 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

Bash Nur Errors anzeigen
shellcheck -S error script.sh

Bei 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

Bash JSON-Format für CI-Integration
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

Bash Bewusste Ausnahme dokumentieren
# shellcheck disable=SC2086 # globbing hier beabsichtigt
rm $files_pattern

Wer 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

Bash ShellCheck folgt der source-Anweisung
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

Bash Self-rolled Pre-Commit-Hook
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

/ Weiter

Zurück zu Shell-Scripting

Zur Übersicht