Vor ein paar Wochen hab ich in einem anderen Post gezeigt, wie man eine Markdown-Datei auf der Linux-Kommandozeile formatiert ausgeben kann.
Ein Kollege hat mich aber zurecht darauf hingewiesen, dass die Lösung etwas “unschön” ist, weil man damit auf jeder noch so kleinen Kiste Python und noch vieles mehr installieren muss. Dazu kommt dann natürlich noch, dass es beim besten Willen keinen Spaß macht, wenn man einmal in Pip-Abhängigkeits-Probleme gelaufen ist und man diese wieder lösen muss.
Eine andere Lösung muss also her. Und die Lösung ist eigentlich denkbar einfach. Man braucht eine Sandbox, die man leicht installieren kann und die keine unlösbaren Konflikte auf dem system erzeugt. Meine Idee geht dabei sofort in Richtung Docker. Mit Docker kann man relativ einfach kleine in sich abgschlossene Systeme erstellen, die dann keine Abhängigkeiten auf dem System (außer natürlich Docker selbst) benötigen.

Fangen wir mal an mit der Planung eines kleinen Dockerfiles.
Was benötigen wir?

  • Als Basis ein Linux Alpine mit Python vorinstalliert. Alpine ist schön klein ohne jeglichen Balast.
  • less ist im Normalfall nicht vorinstalliert, da ja minimalistisch und muss daher noch hinzugefügt werden.
  • Die notwendigen Pip-Module müssen hinzugefügt werden
  • Das Projekt terminal_markdown_viewer von Github wird benötigt

Ich glaub, mit dieser Basis kann man schon anfangen und das würde dann ungefähr so aussehen:

FROM python:2.7-alpine
MAINTAINER mossi <mail@hofmann-robert.info>

RUN apk add less \
  && pip install markdown \
  && pip install pygments \
  && pip install pyyaml \
  && pip install docopt \
  && pip install tabulate \
  && pip install setuptools

Was jetzt noch fehlt, ist das Github-Projekt. Ich möchte allerdings vermeiden, dass ich Git und alles was dazugehört in diesem Image mitinstallieren muss. Daher habe ich beschlossen, dass ich stattdessen mit einem Build-Script arbeite, das den Quellcode holt und diesen füge ich dann einfach in das Image ein und installiere ihn.
Das Dockerfile geht also folgendermaßen weiter:

ADD terminal_markdown_viewer /build

WORKDIR /build

RUN /build/setup.py install \
  && rm -rf /build

Wie man sieht, lösche ich den Ordner /build gleich wieder nachdem ich mit der Installation fertig bin. Ich brauche die Dateien ja nicht mehr und kann mir damit auch den Speicherplatz dafür sparen.

Damit die Anzeige so ist, wie im alten Post, will ich das Ganze mit less anzeigen und darin auch entsprechend farbig ausgeben lassen. Da ich dafür eine pipe benötige, kann ich das nicht direkt als Command oder Entrypoint angeben (bzw. ich hab zumindest selbst nicht rausgefunden, wie ich das umsetzen könnte). Daher mache ich stattdessen ein kleines Script, dass ich ebenfalls hinzufüge.
Hier erst einmal das kleine Script:

#!/bin/sh
mdv "$1" | less -R

Dieses wird jetzt über das Dockerfile hinzugefügt und außerdem auch gleich noch das Arbeitsverzeichnis entsprechend gesetzt.
Hier also weiter mit dem Dockerfile:

ADD view.sh /app/view.sh
WORKDIR /data
RUN chmod 777 /app/view.sh
ENTRYPOINT ["/app/view.sh"]

Damit ist das Dockerfile quasi komplett.
Oben habe ich schon erwähnt, dass ich ein Build-Script verwenden will, dass mir die Daten aus Github holt und zudem auch noch das Erstellen des Images anstößt.

Hier das kleine Build-Script:

#!/bin/bash

git clone https://github.com/axiros/terminal_markdown_viewer
docker build -t mossi/mdv --rm .
rm -rf terminal_markdown_viewer

Auch hier mach ich es so, dass das Projekt am Ende wieder gelöscht wird. Falls ich nochmal kompilieren muss, wird es ja ohnehin wieder neu geholt und so kann ich auch sicher stellen, dass das Programm immer aktuell ist.

So. Wenn ich jetzt das Build-Script ausführe, hab ich mein Image und kann daraus schon einen Container erstellen und ausführen.
Zum Ausführen kann ich folgendes Kommando verwenden:

docker run -ti --rm mossi/mdv "sample.md"

Man wird aber jetzt feststellen, dass das so nicht klappt. Der Hintergrund ist ganz einfach. Selbst wenn wir die Datei sample.md lokal in unserem Verzeichnis liegen haben, so ist diese Datei dennoch nicht im Container vorhanden und kann daher auch nicht angezeigt werden. Man muss also die Datei irgendwie zur Laufzeit in den Container bringen.

Ich hab mir dabei gedacht, dass ich einfach das aktuelle Verzeichnis in den Container mounte und damit sollte die Datei dann ja auch verfügbar sein.

docker run -ti --rm -v $PWD:/data mossi/mdv "sample.md"

Super, das funktioniert schonmal.

Jetzt integrieren wir das wieder in die Linux-Shell, damit es leichter aufzurufen geht und man sich nicht immer das ganze Statement merken muss. Dazu war mein erster Gedanke, dass ich einfach einen Eintrag in der .bash_aliases erzeuge und das war’s dann auch schon:

alias view='docker run -ti --rm -v $PWD:/data mossi/mdv'

Einmal neu anmelden und schon kann ich meine Markdown-Dateien schön formatiert anzeigen indem ich mit dem Kommand view einfach die Datei öffne. Natürlich sollte man sich einen anderen Kommandonamen einfallen lassen, falls man view schon für was anderes verwendet wie zum Beispiel die alte Anleitung.

Leider gibt es doch noch ein kleines Problem. Ich habe meine Notizen in einem speziellen Ordner. Wenn ich mich jetzt im Ordner /etc befinde und von dort mit view ~/notes/note.md meine Notiz anzeigen lassen will, steht ich wieder vor dem Problem, dass die Datei im Comtainer nicht vorhanden ist. Eigentlich ist das auch klar, weil ich PWD in den Container mounte und in diesem Fall ist das leider der falsche Pfad.
Abhilfe zu diesem Problem kann man schaffen, indem man doch wiede eine function erzeugt.
Wie im alten Artikel öffne ich also die Datei .bash_functions und erweitere sie um folgenden Code:

function view {
    path="$(realpath "$1")"
    directory="$(dirname "$path")"
    filename="$(basename "$path")"

    docker run -ti --rm -u $(id -u):$(id -g) -v "$directory":/data mossi/mdv "$filename"
}
export -f view

Was passiert hier? Ich nehme die übergebene Datei und zerlege deren absoluten Pfad in Pfad und Dateiname. Dann mounte ich statt PWD diesen hier ermittelten Pfad und rufe über Docker die entsprechende Datei auf.

Den Eintrag in der .bash_aliases sollte man natürlich wieder entsprechend entfernen, da der ohnehin nicht korrekt funktioniert.
Aber jetzt sollte alles so klappen, wie man es sich vorstellt.