5 Punkte von GN⁺ 2023-11-25 | 1 Kommentare | Auf WhatsApp teilen
  • 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, pkg oder brew install installieren 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 sh ist
    • anspruchsvollere semantische Probleme
    • verschiedene Probleme beim Umgang mit Anführungszeichen

Installation, Lizenz und Integration

  • Die lokale Installation ist mit cabal, apt, dnf, pkg und brew install mö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

 
GN⁺ 2023-11-25
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.

    • Das Problem mit "${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[@]}}"
    • Es gibt auch den verbreiteten Rat, keine Bash-/Shell-Optionen in den Shebang zu schreiben. Denn wenn jemand ein Skript ausführt, indem er den Interpreter explizit angibt, etwa bash my_script.sh statt ./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 set wie set -euo pipefail zu verwenden; das hilft auch in Fällen, in denen ein Shebang wie #!/usr/bin/env bash zusätzliche Argumente nur schwer handhaben kann.
    • Ich frage mich, ob es einen besonderen Grund gibt, -u in den Shebang zu schreiben. Warum der Shebang statt set -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.3 ein Problem zu sein: https://news.ycombinator.com/item?id=38397241
      Zum Beispiel laufen bash -uc 'unset x; echo "=> ${x[@]}"' und bash -uc 'x=(); echo "=> ${x[@]}"' mit leerem Wert durch, aber bash -uc 'x=(); echo "=> ${x[0]}"' ergibt bash: x[0]: unbound variable. Zsh mag das erste Beispiel nicht, aber beide sollten Default-Value-Expansion unterstützen, etwa bash -uc 'unset x; echo "=> ${x[@]:-null}"'. -e wird in Verbindung mit Funktionen sehr verwirrend, und je mehr Zeit vergeht, desto weniger mag ich es.
    • Das Problem mit -e lässt sich gut mit -o pipefail handhaben, und das ist seit letztem Jahr Teil von POSIX.
    • Wenn man Optionen verwendet, die ein Skript früh beenden, musste man normalerweise auch trap einsetzen, wenn es Dateien aufzuräumen gab.
      trap ist 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_VAR kontrollieren 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.

    • Was sollte man verwenden, wenn man mehr Sicherheit will?
  • 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/

    • Klingt gut, dürfte aber nur sinnvoll sein, wenn man es für die eigenen Skripte verwendet. Sonst müsste man jedes auszuführende Skript jedes Mal debuggen und reparieren.
  • 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

    • Ich habe Turtle kürzlich ausprobiert, es am Ende aber wieder verworfen und mich für typed-process entschieden.
      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 cd braucht; das passt gut zu großen und komplexen Workflows. Auch die Unterstützung für OverloadedStrings ist gut, sodass das, was man normalerweise in bash eingeben würde, meist per Copy-and-paste direkt funktioniert.
      Ich verwende außerdem das Paket interpolate und QuasiQuotes, um rohe Strings im Quellcode lesbarer zu machen; das ist aber nicht mit hlint kompatibel, 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.

    • Es wäre schön, wenn es ein Projekt gäbe, das das allgemeiner erledigt.
      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.
    • High five. Ich habe vor ganz Kurzem etwas Ähnliches gebaut: https://gitlab.com/engmark/shellcheck-gitlab-ci-scripts-hook
      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...
    • Wirklich großartig. Ich hatte früher versucht, dieses Problem aus einer anderen Perspektive zu lösen. Ich wollte eine Vorverarbeitung einbauen, die ein „normales“ Shell-Skript nimmt und es zur Build-Zeit als 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 sh ein Albtraum ist.

    • So geht es zum Beispiel: # 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.