Das Tool direnv kann hier Abhilfe schaffen, indem es erlaubt, projektspezifische Konfiguration automatisch zu laden, sobald ein Projektverzeichnis betreten wird.

Die folgende .envrc sorgt z.B. dafür, dass die Binaries im node-modules-Verzeichnis dem Pfad hinzugefügt werden, und setzt die JAVA_TOOL_OPTIONS, um einen UTF-8-Zeichensatz zu erzwingen.

PATH_add node/modules/.bin
export JAVA_TOOL_OPTIONS="-Dfile.encoding=UTF-8"

Die Direnv-Standardbibliothek verfügt noch über weitere Funktionalität, aber erst müssen wir uns der Installation des Tools widmen.

Installation

Da es sich bei Direnv um ein in der Programmiersprache Go geschriebenes Tool handelt, ist die Installation generell einfach. Wenn sich unter auf der Installationswebseite kein Paket findet, reicht es auch, das Binary aus den Releases in den Pfad zu kopieren.

Danach muss die Shell so konfiguriert werden, dass sie direnv jedes Mal ausführt, wenn sich das Arbeitsverzeichnis ändert. Diese Konfiguration unterscheidet sich je nach Shell: Generell muss der Output von direnv hook <shell> ausgeführt werden. Unter ZSH kann man dies mit eval $(direnv hook zsh) tun, unter Bash mit eval $(direnv hook bash). Die Fish-, TCSH- und Elvish-Shells werden ebenfalls unterstützt, die Konfiguration sieht immer ähnlich aus. Die Hook-Konfiguration muss natürlich in der Shell-Konfigurationsdatei persistent gemacht werden, damit der Hook in jeder neuen Shell auch ausgeführt wird.

Der Hook-Befehl führt dazu, dass Direnv immer aktiviert wird, wenn der Benutzer einen neuen Befehl ausführt oder das Arbeitsverzeichnis wechselt. Bei Bash passiert dies über die PROMPT_COMMAND-Variable, bei Zsh über Code in den precmd_functions und chpwd_functions-Arrays. Eine eventuell existierende .envrc wird gefunden und geladen. All dies passiert relativ performant, sodass man im täglichen Gebrauch vom Direnv-Hook nichts mitbekommt (außer der komfortableren Handhabung natürlich).

Konfiguration

Direnv wird größtenteils über Dateien im Projektverzeichnis konfiguriert. Hier können beliebige Shell-Variablen gesetzt oder modifiziert werden. Das direnv-Programm, welches wir oben in die Shellkonfiguration eingebaut haben, sucht bei jeder Ausführung nach einer Datei namens .envrc. Diese Datei wird dann in einer Kindshell ausgeführt, und alle exportierten Variablen in die aktuelle Shell übernommen. Aus Sicherheitsgründen muss jede .envrc erst vom Benutzer erlaubt werden, bevor sie von direnv ausgeführt wird.

Ein Anwendungsbeispiel soll dies verdeutlichen. Ziel ist es, den HTTP-Proxy für das Projekt automatisch zu setzen. Hierzu erzeugt man eine neue Datei namens .envrc im Projektverzeichnis. In diese Datei schreibt man dann die gewünschten Zuweisungen, etwa wie unten:

export HTTP_PROXY=http://127.0.0.1:8080/

Wie man sieht, handelt es sich bei einer .envrc um ein ganz reguläres Shellskript. Es kann beliebiger Code ausgeführt in der Datei ausgeführt werden. Sichtbar für die Benutzer sind jedoch nur Änderungen in den Umgebungsvariablen, da direnv nur diese Änderungen nach „außen“ an die Shell der Benutzer weitergibt.

Die neue Envrc wird jedoch nicht direkt geladen. Verständlicherweise ist die automatische Ausführung von Shellskripten ein Sicherheitsproblem. Deswegen muss jede .envrc einmalig erlaubt werden. Nach Lektüre der Datei kann dies über direnv allow erfolgen. Direnv speichert diese Freigabe, sodass man für jeden Pfad die .envrc nur einmal freigeben muss.

Nach dem Whitelisting der .envrc wird jetzt die Proxy-Variable jedes mal gesetzt, wenn man das Projektverzeichnis betritt, und wieder gelöscht, wenn man es wieder verlässt.

Fortgeschrittene Funktionen

Die Grundfunktionalität von direnv, das Exportieren und Modifizieren von Umgebungsvariablen, ist bereits sehr nützlich, um der Entwicklerin viel Kleinarbeit abzunehmen. Direnv verfügt jedoch auch über eine „Standardbibliothek“, die weitere Funktionalität zur Verfügung stellt.

Einerseits sind dies „Baukasten“-Funktionen, die es erlauben, kompliziertere Funktionalität in einer .envrc zu realisieren. Mit fetchurl können transparent Binärdateien von einer URL nachgeladen werden, die dann automatisch in den Pfad aufgenommen werden. Die verschiedenen source_-Funktionen erlauben es, .envrc-Dateien miteinander zu kombinieren, oder sie sogar von einer URL nachzuladen. Mit direnv_load kann die Konfiguration von Umgebungsvariablen aus externen Tools nachgeladen werden, load_prefix setzt automatisch diverse Variablen (Pfad, Linker-Verzeichnisse, Manpage-Verzeichnisse) für Programme, die mit --prefix insstalliert wurden. on_git_branch erlaubt es, Dinge nur auf bestimmten Branches zu tun. Mit diesem Baukasten fällt sicher jeder Entwicklerin, die in der Shell unterwegs ist, direkt ein Nutzen ein.

Mit weniger Konfiguration verbunden sind die use und layout-Befehle, die dazu dienen, sprach- und frameworkspezifische Layouts zu laden. layout node erweitert z.B. den Pfad um das node-modules/.bin-Verzeichnis, damit lokal mit Node installierte Programme ohne Pfadangabe ausgeführt werden können. layout python erzeugt ein Virtualenv und aktiviert es, was bedeutet, dass alle Pakete in diese projektspezifische Python-Umgebung installiert werden. Mit use nix können Pakete und deren Abhängigkeiten über den Nix-Paketmanager geladen werden, wenn ein Verzeichnis betreten wird. Die Standardbibliothek von Direnv bringt eine Menge anderer Layouts mit, welche auf der Webseite dokumentiert sind.

Wem das alles nicht reicht, kann in der Konfigurationsdatei .direnvrc im Homeverzeichnis noch weitere Skripte und Funktionen unterbringen, die dann in Projekten genutzt werden können. Man sollte allerdings bedenken, dass diese Skripte anderen Projektteilnehmern nicht verfügbar sind.

Direnv ist primär für Terminalbenutzer ausgelegt. Für die meisten verbreiteten Editoren und Entwicklungsumgebungen gibt es jedoch Plugins, die die Umgebungsvariablen aus der .envrc laden. Für Jetbrains-Entwicklungsumgebungen gibt es das Plugin Direnv Integration, für VSCode kann man das direnv-Plugin installieren.

Fazit

Als Fazit kann man sagen, dass Direnv ein mächtiger Weg ist, projektspezifische Konfiguration an Projektverzeichnisse zu knüpfen. Versionskonflikte, manuelle Einstellung und Schreibarbeit kann mit Direnv effizient reduziert werden.