ShellCheck: Analyse-Tool zum Finden von Bugs in Shell-Skripten
(shellcheck.net)- ShellCheck ist ein Tool, mit dem sich häufige Fehler in Shell-Skripten direkt im Web prüfen lassen; Probleme im eingefügten Code werden in der Ausgabe unterhalb des Editors angezeigt
- In lokalen Umgebungen lässt es sich unter anderem mit
cabal,apt,dnf,pkgoderbrew installinstallieren und damit paketmanagerbasiert nutzen - Die Beispiele zeigen Stellen, die in realen Skripten oft übersehen werden, etwa Portabilitätswarnungen bei einem
sh-Shebang, semantische Probleme und Fehler beim Umgang mit Anführungszeichen - Es ist freie Software unter der GPLv3-Lizenz und wird auch über GitHub, ein Wiki sowie als integrierter Linter für wichtige Editoren bereitgestellt
- Für automatische Prüfungen von GitHub-Repositories kann es in CodeClimate, Codacy und CodeFactor genutzt werden; ShellCheck selbst ist in Haskell geschrieben
Analysefunktionen für Shell-Skripte
- ShellCheck ist ein Analyse-Tool zum Finden von Bugs in Shell-Skripten
- Wenn man ein Skript auf der Website einfügt, sind die Prüfergebnisse direkt im Ausgabefenster unterhalb des Editors zu sehen
- Die enthaltenen Beispiele zeigen, dass verschiedene Problemtypen erkannt werden können
- mehrere in typischen Skripten versteckte Probleme
- Portabilitätswarnungen, die auftreten, wenn der Shebang
shist - anspruchsvollere semantische Probleme
- verschiedene Probleme beim Umgang mit Anführungszeichen
Installation, Lizenz und Integration
- Die lokale Installation ist mit
cabal,apt,dnf,pkgundbrew installmöglich - Es ist freie Software unter der GPLv3-Lizenz
- Die Dokumentation ist im ShellCheck Wiki verfügbar
- Das Projekt ist auf GitHub veröffentlicht; auch der Website-Code ist öffentlich
- Pakete für Distributionen oder Paketmanager sind bereits verfügbar
- In wichtigen Editoren lässt es sich als integrierter Linter verwenden
- In CodeClimate, Codacy und CodeFactor kann es für automatische Prüfungen von GitHub-Repositories genutzt werden
- ShellCheck ist in Haskell geschrieben
1 Kommentare
Meinungen auf Hacker News
Mein Tipp ist folgender: In den Shebang sollte man fast immer
-u(nounset) aufnehmen, damit nicht deklarierte Variablen zu Fehlern werden. Die häufigste Ausnahme, auf die man stößt, ist, dass beim Expandieren eines leeren Arrays mit der Syntax"${arr[@]}"dies als unbound behandelt wird.-n(noexec) verhindert die Ausführung von Befehlen und kann daher als eine Art Dry-Run für Arme dienen.-e(errexit) ist ebenfalls nützlich, aber man muss aufpassen, dass im Grunde nur ein fehlgeschlagener Befehl „für sich selbst“ das Beenden auslöst; persönlich vermeide ich es daher eher und hänge oft|| fail "..."an Befehle an."${arr[@]}"gibt es nur in bash 3 und älter; ab bash 4 wirft[@]selbst dann keine unbound variable, wenn die Variable tatsächlich nicht definiert ist.Trotzdem bleibt es ein Problem, weil macOS immer noch standardmäßig bash v3 installiert und sie auch nicht automatisch aktualisiert. Dass das letzte Release von bash 3 vor 20 Jahren war, ist wirklich verrückt. Unbound-Fehler beim Expandieren leerer Arrays lassen sich mit der
${var+alter}-Expansion umgehen:echo "${arr+${arr[@]}}"bash my_script.shstatt./my_script.sh, werden die Optionen nicht angewendet.Viele machen das, um das Ausführungs-Bit nicht setzen zu müssen, manchmal auch aus mangelndem Verständnis. Deshalb ist der Rat verbreitet, direkt in der ersten Zeile nach dem Shebang ein
setwieset -euo pipefailzu verwenden; das hilft auch in Fällen, in denen ein Shebang wie#!/usr/bin/env bashzusätzliche Argumente nur schwer handhaben kann.-uin den Shebang zu schreiben. Warum der Shebang stattset -u?In Bash scheint
"${arr[@]}"gut zu funktionieren. Wie auch in anderen Kommentaren erwähnt, ist es in neueren Bash-Versionen besser geworden und scheint nur bei<= 4.3ein Problem zu sein: https://news.ycombinator.com/item?id=38397241Zum Beispiel laufen
bash -uc 'unset x; echo "=> ${x[@]}"'undbash -uc 'x=(); echo "=> ${x[@]}"'mit leerem Wert durch, aberbash -uc 'x=(); echo "=> ${x[0]}"'ergibtbash: x[0]: unbound variable. Zsh mag das erste Beispiel nicht, aber beide sollten Default-Value-Expansion unterstützen, etwabash -uc 'unset x; echo "=> ${x[@]:-null}"'.-ewird in Verbindung mit Funktionen sehr verwirrend, und je mehr Zeit vergeht, desto weniger mag ich es.-elässt sich gut mit-o pipefailhandhaben, und das ist seit letztem Jahr Teil von POSIX.trapeinsetzen, wenn es Dateien aufzuräumen gab.trapist eine großartige Scripting-Funktion, über die meinem Gefühl nach nicht genug gesprochen wird.Kürzlich habe ich wegen arithmetischer Expansion in einem Shell Script eine Privilege-Escalation-Schwachstelle gefunden. Es war ein ähnlicher Typ wie in https://research.nccgroup.com/2020/05/12/shell-arithmetic-ex... beschrieben.
Zum Beispiel ermöglicht
$((1 + ENV_VAR))Code Injection, wenn man$ENV_VARkontrollieren kann. Leider hat ShellCheck das zumindest mit der Standardkonfiguration nicht erkannt. Aber wenn man irgendetwas auch nur ansatzweise Sicherheitskritisches implementiert, sollte man ohnehin keine Shell verwenden.ShellCheck ist wirklich ein Lebensretter. Ich habe früher einen kleinen Wrapper gebaut, https://github.com/jamespwilliams/strictbash, der als Shebang eines Skripts verwendet werden kann.
Er lässt vor der Ausführung des Skripts ShellCheck laufen, sodass es bei Fehlern gar nicht erst ausgeführt wird, und setzt außerdem alle „strict mode“-Flags von bash. Siehe: http://redsymbol.net/articles/unofficial-bash-strict-mode/
Dieses Thema kam schon mehrfach auf: https://news.ycombinator.com/from?site=shellcheck.net
Die letzte große Diskussion war 2021, mit 301 Punkten und 54 Kommentaren: https://news.ycombinator.com/item?id=27030504
Vor Kurzem habe ich Build- und Deployment-Skripte sowie ein paar bash-Skripte für einen einzelnen Produktionsserver auf Turtle in Haskell umgestellt.
Das war gut, weil ich die Duplikation stark reduzieren konnte, und der resultierende Code wurde auch deutlich kürzer. https://hackage.haskell.org/package/turtle
Soweit ich weiß, hat ein Turtle-Programm nur ein aktuelles Verzeichnis, was schwierig ist, wenn man parallele Aufgaben ausführen will, die in bestimmten Verzeichnissen laufen müssen. Mit Locks, Queues und Workern ließ sich das teilweise lösen, aber als das aktuelle Verzeichnis von Turtle gelöscht wurde und dadurch Fehler auftraten, wurde es zu schwer handhabbar.
typed-process startet dagegen separate Prozesse und kann sie in einem Arbeitsverzeichnis ausführen, ohne dass man
cdbraucht; das passt gut zu großen und komplexen Workflows. Auch die Unterstützung fürOverloadedStringsist gut, sodass das, was man normalerweise in bash eingeben würde, meist per Copy-and-paste direkt funktioniert.Ich verwende außerdem das Paket
interpolateundQuasiQuotes, um rohe Strings im Quellcode lesbarer zu machen; das ist aber nicht mithlintkompatibel, daher werde ich mich wohl nach einem anderen Paket für String-Verarbeitung umsehen.Ziemlich unverhohlene Eigenwerbung, aber ich habe ShellCheck und mehrere Linter in meiner pre-commit-Konfiguration, mit dem Prinzip, alle Warnungen vor dem Commit oder spätestens vor dem Merge zu beheben.
Allerdings steckt der größte Teil der Shell in meinen Projekten in
.gitlab-ci.yml-Dateien, was schwer zu prüfen ist. Deshalb habe ich einen Wrapper gebaut, der das automatisch erledigt: https://pypi.org/project/glscpc/Mit dem ShellCheck-Projekt und etwas Magie zeigt er ShellCheck-Hinweise mit nahezu korrekten Zeilennummern an.
Ich nutze GitLab CI nicht, verwende aber ziemlich viele Dateiformate, die im Kern Shell-Skripte inline einbetten, etwa Dockerfile, GitHub Actions und Justfile.
Normalerweise lagere ich alles, was komplexer als ein paar Befehle ist, schon allein wegen ShellCheck in ein separates Shell-Skript aus und rufe es aus dem Inline-Skript im Dockerfile auf. Dieses Muster hilft auch dabei, CI nicht zu eng an GitHub Actions zu koppeln.
Das Anwendungsbeispiel ist ein pre-commit-Hook für
.gitlab-ci.yml, und ein Konfigurationsbeispiel gibt es hier: https://gitlab.com/engmark/root/-/blob/9f7d9b93c2297d0b170e5...script-Teil des jeweiligen Jobs rendert.Der Vorteil ist, dass weiterhin alles in dem GitLab-CI-Job selbst enthalten ist. Aber mit all den Merkwürdigkeiten der Shell in der GitLab-CI-Runner-Umgebung umzugehen, war zu schmerzhaft, also habe ich aufgehört; inzwischen verschiebe ich alle Jobs in Python-Skripte.
Es gibt auch den bash language server: https://github.com/bash-lsp/bash-language-server/
Gut. Bei dem ersten Produktions-
/bin/sh-Skript, über das ich es habe laufen lassen, habe ich sofort ein paar Dinge gelernt, und solche Skripte fasse ich seit den 80ern an.Wenn etwas zu lang wird, um es in Bash zu schreiben, und man es eigentlich nicht mehr so machen sollte, empfehle ich auch https://github.com/bach-sh/bach
ShellCheck ist großartig, aber die Behandlung von source/import ist wirklich schmerzhaft. Das ist nicht ShellChecks Schuld, sondern liegt daran, dass
shein Albtraum ist.# shellcheck source=./deployment/deployment-example.env, gefolgt von. "${1}".Wenn es aber mehrere untergeordnete Shell-Skripte und viele Dateien gibt, die per source eingebunden werden sollen, versteht man, warum das schmerzhaft ist.