2 Punkte von GN⁺ 2025-01-29 | 1 Kommentare | Auf WhatsApp teilen
  • Wenn du Automatisierungsskripte in ~/bin/ ablegst und ausführst, lassen sich Python-Skripte mit uv und Inline-Skript-Metadaten schlanker handhaben
  • Wenn ~/bin im PATH enthalten ist, kannst du als Shebang #!/usr/bin/env -S uv run --script festlegen und den Dateinamen wie einen Befehl ausführen
  • Diese Konfiguration sorgt dafür, dass die Shell zuerst uv run --script ausführt und die aktuelle Skriptdatei als Argument übergibt
  • uv ignoriert die Shebang-Zeile und führt den restlichen Inhalt wie eine normale Python-Datei aus
  • Mit chmod a+x {filname} kannst du Ausführungsrechte vergeben und das Skript dann als einfaches Python-Kommandozeilen-Skript mit automatischer Abhängigkeitsverwaltung verwenden

uv als Shebang ausführen

  • Für die Arbeitsautomatisierung werden oft mehrere Skripte im Verzeichnis ~/bin/ erstellt und verwendet
  • Wenn ~/bin im PATH enthalten ist, kannst du Skripte direkt nur über ihren Dateinamen ausführen
  • Die dabei verwendete Shebang-Zeile sieht so aus
#!/usr/bin/env -S uv run --script

Ausführungsablauf und Rechtevergabe

  • Die obige Shebang-Zeile führt auf der Kommandozeile uv run --script aus und übergibt die Skriptdatei als Argument
  • uv ignoriert die Shebang-Zeile und führt den Rest der Datei als normalen Python-Code aus
  • Vor der Ausführung musst du dem Skript Ausführungsrechte geben
chmod a+x {filname}
  • Nach der Einrichtung kannst du ein einfaches Python-Kommandozeilen-Skript mit automatischer Abhängigkeitsverwaltung direkt ausführen

1 Kommentare

 
GN⁺ 2025-01-29
Hacker-News-Meinungen
  • Ich habe heute zum ersten Mal von env -S erfahren, und als ich den Shebang im Artikel sah, dachte ich sofort: „Das wird unter Linux nicht funktionieren, weil ein Shebang nur ein einziges Argument übergeben kann.“
    Wenn man zum Beispiel foo.py ausführt, das mit #!/usr/bin/env -S uv run --script beginnt, übergibt das Betriebssystem die gesamte Shebang-Zeile tatsächlich als ein Argument, etwa /usr/bin/env '-S uv run --script' foo.py.
    Aber -S von env zerlegt diesen String wieder in mehrere Argumente, sodass es funktioniert. Sehr elegant und nützlich.

    • Unter macOS ist dieses Verhalten nicht identisch, was frustrierend ist: https://unix.stackexchange.com/a/774145
    • Das erinnert mich daran, wie GNU Guile die Beschränkung auf ein Argument mit einem mehrzeiligen Shebang umgeht.
      Setzt man nach #!/usr/bin/guile \ in die nächste Zeile -e main -s und schließt mit !#, wird daraus so etwas wie /usr/bin/guile -e main -s filename.
      Ich frage mich, warum man das ausgerechnet so gelöst hat. Vermutlich wurde env -S erst kürzlich hinzugefügt oder war nicht auf allen Plattformen verfügbar, die relevant waren.
      https://www.gnu.org/software/guile/manual/html_node/The-Meta-Switch.html
    • Dieser Ansatz lässt sich gut mit tusk verwenden, einer Go-CLI ähnlich wie make, die aber YAML für die Konfiguration nutzt.
      Man setzt #!/usr/bin/env -s go run github.com/rliebz/tusk@latest -f als Shebang und verwendet als Interpreter die Go-Shell gosh, etwa interpreter: go run mvdan.cc/sh/v3/cmd/gosh@latest -s.
      So entsteht eine CLI, die auf jeder Architektur ausführbar ist, solange Go installiert ist.
    • Wenn der Wrapper mitspielt, kann man auch in den nachfolgenden Zeilen weitere Informationen unterbringen.
      nix-shell erlaubt zum Beispiel nach #!/usr/bin/env nix-shell eine weitere Zeile #!nix-shell --pure -i runghc ./default.nix, um Abhängigkeiten und Argumente anzugeben; danach kann direkt Haskell-Code folgen.
    • env -S hätte eigentlich gar nicht nötig sein sollen.
      Die seltsamen Regeln zur Trennung von Leerzeichen in Shebang-Zeilen sind ein alter Bug, der zu einem nicht mehr behebbaren Makel von Unix geworden ist.
      Jedes Mal, wenn man solche Workarounds nutzt, denke ich, dass man mit 30 Minuten Aufwand in den 1980ern jahrelangen Ärger hätte vermeiden können. Shebang-Zeilen hätten von Anfang an wie /bin/sh aufgeteilt werden sollen.
  • Kein Shebang, aber ich nutze uv in letzter Zeit wie ein Installationstool.
    Weil es schwierig ist, Python-CLI-Tools mit komplexen Abhängigkeiten zu paketieren und zu verteilen, habe ich es auf zwei Arten eingesetzt.
    Die eine besteht darin, ein curl | sh-Installationsskript zu kopieren und am Ende uv tool install --python python3.12 my-tool hinzuzufügen, sodass Nutzer die CLI mit einer einzigen Zeile installieren können. Die andere besteht darin, ein kleines PyPI-„Installer“-Paket zu erstellen, das nur uv als Abhängigkeit hat und uv tool install ausführt.
    Beide Methoden installieren das CLI-Tool, Python 3.12 und alle Python-Abhängigkeiten in einer isolierten Umgebung. Nutzer müssen keine virtuelle Umgebung verwalten, und bei der curl | sh-Variante muss nicht einmal Python installiert sein.
    Seitdem gibt es deutlich weniger GitHub-Issues, die dadurch entstehen, dass Nutzer komplexe Abhängigkeiten kaputtmachen.
    Details und Code-Links: https://aider.chat/2025/01/15/uv.html

    • Ich habe nicht geprüft, wie es intern funktioniert, aber wenn es sogar das CLI-Tool und Python 3.12 installiert, braucht man auf dem Endnutzersystem dann nicht Compiler, Kernel-Header sowie Library-Header wie openssl-dev, gzip, ffi und muss Python kompilieren?
      Zumindest mit ASDF-VM habe ich wegen anderer Python-Installationstools intern solche Erfahrungen gemacht.
    • Klingt gut. mise und aqua scheinen ebenfalls ein nützliches Konzept zur Tool-Installation zu haben.
  • Wo wir gerade bei interessanten Shebang-Geschichten sind: Ich habe so etwas gebaut, um Prolog-.pl-Dateien aus der Shell auszuführen:
    /*usr/bin/env scryer-prolog "$0" "$@" ; exit #*/
    Für Skriptsprachen, die /**/-Kommentare haben, aber #-Kommentare oder #!-Shebangs nicht direkt unterstützen, dürfte das im Großen und Ganzen funktionieren.
    Ursprünglicher Kommentar: https://github.com/mthom/scryer-prolog/issues/2170#issuecomment-1821713993
    Diese Datei ist gleichzeitig eine gültige Shell-Datei und eine gültige Prolog-Datei. Wird sie von der Shell ausgeführt, sucht die erste Zeile nach dem Glob-Pattern /*usr/bin/env und startet /usr/bin/env; env führt dann scryer-prolog "test.pl" aus. Auf Prolog-Seite ist die erste Zeile ein /* ... */-Kommentar und wird ignoriert.
    Danach führt die Shell nach dem ; ein exit aus, damit der restliche Prolog-Code nicht als Shell interpretiert wird, und das # sorgt dafür, dass die Shell den Prolog-Kommentarabschluss */ ignoriert, wodurch Fehlermeldungen vermieden werden.
    Vielleicht das Beste und zugleich Schlimmste, was ich je gebaut habe.

    • Es gibt auch eine ähnliche Methode, Go-Quellcode ausführbar zu machen: https://unix.stackexchange.com/questions/162531/shebang-starting-with
      Darüber hinaus kann man auch C-Programme mit einer ersten Zeile der Form //usr/bin/env sh -c ... ausführbar machen. Beim Ausführen wird es mit make gebaut und anschließend das resultierende Binary gestartet; wenn man also den Quellcode ändert und erneut ausführt, sieht man die geänderte Ausgabe sofort.
    • Ich habe es mir nicht selbst ausgedacht, aber in TCL verwendet man so etwas ebenfalls.
      Nach #!/bin/sh kann man in die nächste Zeile # the next line restarts using tcl \ schreiben und dann exec tclsh "$0" "$@" ausführen lassen.
      Das nutzt aus, dass in TCL auch Kommentarzeilen mit Zeilenfortsetzung möglich sind.
    • Das ist wirklich beeindruckend. Mit ein paar Anpassungen an Scryers Ein-/Ausgabe-Redirection könnte es ein perfekter Ersatz für Bash-Skripte werden.
  • In Trey Hunners Artikel Lazy self-installing Python scripts with uv gibt es ausführlichere Informationen mit Beispielen:
    https://treyhunner.com/2024/12/lazy-self-installing-python-scripts-with-uv/

    • Das Beispiel „ein Skript, das 80 Nullen ausgibt“ geht auch in der Shell mit printf %.1s 0{1..80} $'\n' oder for i in {1..80}; do echo -n 0; done; echo.
      Beim ffmpeg-Beispiel sehe ich auch nicht so recht, worin der Unterschied zu ffmpeg -i in.mp4 -c:v copy -filter:a volumedetect -pass 1 -f null /dev/null && ffmpeg -i in.mp4 -c:v copy -filter:a "loudnorm" -pass 2 out.mp4 besteht.
      Ehrlich gesagt wirkt die Python-Variante komplizierter.
  • Für kleine Aufgaben kann man diesen Trick zusammen mit mise (mise-en-place) verwenden: https://mise.jdx.dev/tasks/toml-tasks.html#shell-shebang
    Dabei setzt man unter tools uv = 'latest', deklariert im run-Block der Aufgabe #!/usr/bin/env -S uv run --script sowie dependencies = ["requests<3", "rich"] und fügt danach den Python-Code ein.

  • Vor 16 Tagen gab es schon einen verwandten Artikel samt Diskussion: Uv's killer feature is making ad-hoc environments easy https://news.ycombinator.com/item?id=42676432 (502 Punkte, 417 Kommentare)

  • Ein nützlicher Trick, den man sich merken sollte.
    Ich habe neulich meinen Python-Alias auf uv run python umgestellt, und das ist ziemlich angenehm. Man muss keine .venv usw. verwalten, sich nicht um Systeminstallationen oder globale/benutzerspezifische Installationen kümmern, und unter Debian hilft es besonders.
    Auch wenn man in einem Projekt oder einer Umgebung eine REPL startet, kann man sie ohne activate aufrufen, was die gedankliche Last reduziert.
    Ein Nachteil ist allerdings: Wenn man .py direkt mit uv aufruft, werden Projekt-/Umgebungsdateipfade anhand des aktuellen Arbeitsverzeichnisses bestimmt, nicht anhand des Speicherorts der .py-Datei.
    Es gibt zwar den expliziten Schalter --project, sodass man etwa uv run --project /script.py verwenden kann, aber ein relativer Projekt-Schalter bezogen auf die Zieldatei wäre schön, weil er Wiederholungen reduzieren würde.

  • Ich habe zwar ein paar Fallstricke erlebt, wenn man uv oder uvx als Command-Runner verwendet, aber meistens funktioniert es gut – und dann spart es enorm viel Zeit.
    Als Python-Entwickler und neugieriger Mensch liegen in meinem Home-Verzeichnis überall flache Klone und Wegwerf-Testverzeichnisse herum; dass das diesen Verwaltungsaufwand reduziert, ist wirklich nützlich.
    Diese Idee möchte ich mir abschauen. Ich frage mich, ob dafür eine eigene Erweiterung statt .py verwendet wird, oder ob schon ein .py-File mit gesetztem Execute-Bit und Shebang als Signal dient, dass es nicht mit dem System-Python, sondern direkt als Skript ausgeführt werden kann.

    • Im vergangenen Jahr habe ich uv selbst genutzt, um zu sehen, ob ich es empfehlen kann, und dabei eine Liste von Fallstricken gesammelt; überraschenderweise waren es nicht viele.
      Nicht null, aber ich suche ziemlich gründlich. Wenn ich erfahren kann, auf welche Fallstricke du gestoßen bist, würde ich sie gern in die Liste aufnehmen.
    • Soweit ich es verstanden habe, verwendet man .py mit gesetztem Execute-Bit und der im Artikel gezeigten Shebang-Zeile.
    • Ich würde wirklich gern wissen, welche Fallstricke es gab.
      Bei Pipenv gab es Probleme, wenn eine bestimmte Abhängigkeit je nach Plattform unterschiedliche Wheels hatte; bei Poetry funktionierte das gut.
      Die Lock-Datei von Pipenv enthielt nur die Abhängigkeiten der Plattform, auf der gelockt wurde, während Poetry alle Plattformvarianten in die Lock-Datei aufnimmt.
      Ich habe noch keine Dokumentation dazu gefunden, wie sich uv in diesem Fall verhält.
  • Der Kern ist folgender:
    Ein kürzlich veröffentlichter PEP definiert, wie Python-Skripte ihre Abhängigkeiten in Startkommentaren deklarieren, und uv ist ein Python-Skript-Runner und Paketmanager, der diese Abhängigkeiten scannt, erfüllt und das Skript dann so ausführt, dass die entsprechenden Module importiert werden können.
    Wenn man in der Shebang, also dem Kommentar in der ersten Zeile eines Python-Skripts, die Datei mit uv statt mit python aufrufen lässt, bekommt man diesen Effekt automatisch, sobald man das Skript „ausführt“.
    Dadurch laufen auch Python-Skripte mit Abhängigkeiten, die eigentlich Konfiguration benötigen, einfach direkt.

    • Allerdings muss uv installiert und in $PATH sein.
      Und auch wenn uv die Abhängigkeiten installiert, ist nicht klar, ob es auf diese Weise auch den Python-Interpreter automatisch installiert.
    • Am Ende läuft es also erst dann „einfach so“, wenn man uv auf dem System installiert und allen Skripten Abhängigkeitszeilen voranstellt.
      Persönlich sehe ich nicht, dass das gegenüber dem alten Weg, eine Umgebung anzulegen und aufzurufen, Zeit spart.
      Ich bin mir auch nicht sicher, ob uv Umgebungen nur in interaktiven Sessions laden kann oder ob es nur für die Skriptausführung verwendet wird.
  • Wenn du TypeScript magst, kannst du denselben Ansatz auch mit Deno nutzen. Mit Bun dürfte es vermutlich ebenfalls gehen, ausprobiert habe ich es aber nicht.
    Man schreibt zum Beispiel #! /usr/bin/env -S deno run; wenn Berechtigungs-Flags nötig sind, ergänzt man sie etwa so: #! /usr/bin/env -S deno run --allow-env --allow-read --allow-net.