Haskell ist keine Skriptsprache, sondern wird in der Regel kompiliert. Mit einigen Handgriffen lassen sich aber Shellskripte in die funktionale Sprache einbinden. Haskells starkes Typensystem greift dann ein, um Fehler durch Argumente in falscher Anzahl oder Art zu verhindern.
In der stark typisierten Programmiersprache Haskell [1] lassen sich Programme schreiben, die sich fast wie ein Bash-Skript lesen (Listing 1). Doch in diesem Artikel geht es nicht um das bloße Nachahmen, sondern um eine zuverlässigere Umgebung für die Systemprogrammierung. Wer zu Haskell greift, erhält ein starkes Typensystem, das beim Kompilieren viele Fehler abfängt. Dabei muss der Programmierer aber nicht wie bei C oder Java ständig Typen angeben. Haskell kennt Typ-Inferenz und kann das meiste einfach erraten.
Listing 1
Shellkommandos in Haskell
01 main = shelly $ do 02 apt_get "update" [] 03 apt_get "install" ["haskell-platform"] 04 where 05 apt_get mode more = run "apt-get" (["-y", "-q", mode] ++ more)
Bei Programmen für die Systemverwaltung lohnt es sich besonders, Fehler zu reduzieren: Bei einem Versagen droht ein kaputtes Betriebsystem, dessen Wiederherstellung viel Aufwand erfordert. Andererseits lassen sich Skripte in der Regel schwer testen, da die Zusammenarbeit mit anderen Programmen und der Systemumgebung bei Erfolg oder Misserfolg eine große Rolle spielt.
Dynamische Skriptsprachen wie Perl, Ruby und Bash besitzen praktische und einfache Schnittstellen zur Interaktion mit dem Betriebssystem, was sie bei Admins beliebt macht. Gleichzeitig bieten sie einen hohen Abstraktionsgrad.
Abstraktion plus Typen
Dieser Artikel behält die Abstraktion bei, fügt ihr aber Haskells starke Typisierung hinzu. Damit die Interaktion mit dem System leichter gelingt, muss der Haskell-Programmierer allerdings noch ein wenig nachhelfen. Glücklicherweise bringt die Sprache alles mit, was er für ein schöneres API benötigt. Den Start erleichtert der Kasten “Installation”.
Installation
Bis vor Kurzem war es nicht einfach, Haskell samt seinen Bibliotheken zu installieren. Doch nun gibt es eine praktische Sammlung namens Haskell Platform [2], die als Paket für die meisten Distributionen existiert (Abbildung 1). Die Codebeispiele verwenden GHC 7.3, die im Artikel benutzte Bibliothek Shelly lässt sich mit Haskells Paketverwaltung via »cabal install shelly« einspielen.
Listing 2 verwendet Shelly [3], eine Bibliothek des Autors, und zeigt, wie ein API für die Systemprogrammierung mit Haskell aussehen kann. Das Programm schreibt Code in eine Quelltextdatei, kompiliert sie mit dem Glasgow Haskell Compiler »ghc« und führt die entstehende Binärdatei aus (Abbildung 2).
Listing 2
Shelly
01 {-# LANGUAGE OverloadedStrings #-}
02 import Shelly
03
04 main = shelly $ do
05 appendfile "a.hs" "main = print \"hello\""
06 run "ghc" ["a.hs"]
07 out <- run "./a" []
08 echo out

Abbildung 2: Funktioniert fast wie ein Shellskript: Haskell kompiliert Quellcode und führt das Resultat aus.
Wer das Beispiel nachvollziehen möchte, speichert Listing 2 in einer Haskell-Quelltextdatei namens »makea.hs« und setzt die Befehlszeile »ghc makea.sh && ./makea« ab. Spielereien mit den Argumenten von »appendfile« in Zeile 5, etwa das Einsetzen einer Zahl, quittiert GHC mit einer Fehlermeldung über den falschen Datentyp. Grundwissen zu Haskell und seinem Typensystem vermittelt das online lesbare Buch “Learn You a Haskell for Great Good!” [4].
Zwei Welten
Neben dem Typensystem gibt es eine weitere Eigenschaft, die Haskell grundlegend von den Skriptsprachen unterscheidet. Die Programmiersprache trennt die reine Berechnung strikt von der Interaktion mit dem Betriebssystem. Eine typische Funktionsdefinition sieht so aus:
add_two :: Int -> Int add_two x = x + 2
Die erste Zeile nennt den Funktionsnamen »add_two« und enthält daneben die Typsignatur. Diese besagt, dass die Funktion einen Integer in einen Integer überführt. Mit dem Betriebssystem tritt dieser Code gar nicht in Verbindung. Nicht einmal die Variable »x« könnte er neu definieren, denn sie verhält sich in der funktionalen Programmierung eher wie eine Konstante. Das mag sich unpraktisch anhören, ist für erwünschte Eigenschaften wie Nebenläufigkeit und Threadsicherheit aber hervorragend. Das rein funktionale Paradigma macht dem Compiler die Arbeit leichter, da er den Code optimieren kann. Auch für den menschlichen Leser ist gut nachzuvollziehen, was passiert.
Für das Automatisieren von Systemaufgaben wäre ein solches Programm aber vollkommen nutzlos. Die Schöpfer von Haskell haben der Sprache daher eine raffinierte Möglichkeit eingebaut, die funktionale Reinheit zu bewahren und dennoch mit der Außenwelt zu kommunizieren. Listing 3 definiert die Funktion »add_2_input« , die Eingaben vom Terminal liest und Ausgaben erledigt. Ihre Signatur »IO Int« signalisiert, dass sie In- und Output durchführt, und zwar mit dem Integer-Datentyp.
Listing 3
Ein- und Ausgabe
01 import IO 02 add_2_input :: IO Int 03 add_2_input = do 04 line <- getLine 05 return (add_two (read line)) 06 07 add_two :: Int -> Int 08 add_two x = x + 2 09 main = add_2_input >>= print
Die Funktion »read« liest eine Zeichenkette als Haskell-Wert ein. Dabei konvertiert sie die Eingaben in den Datentyp »Int« , was sie aus der Typsignatur in Zeile 1 herleiten kann. Übersetzt und mit dem Kommando »./add_two« aufgerufen, wartet das Programm auf eine Benutzereingabe und addiert 2 hinzu, falls der Anwender eine Ganzzahl eingibt.
»IO« bildet in Haskell eine so genannte Monade und verwendet in Zeile 2 die »do« -Notation. Stark vereinfacht ausgedrückt dient eine Monade als Ausführungsumgebung für Code. Die Shelly-Bibliothek definiert mit der Monade »ShIO« eine geeignete Umgebung für Shellbefehle. Mit ihrer Hilfe lässt sich auch die Aufgabe lösen, die in der Einführung zu diesem Linux-Magazin-Schwerpunkt dargestellt ist: Benutzerdaten aus einer CSV-Datei lesen und damit Benutzerkonten anlegen, ähnlich wie mit dem Kommando »newusers« .
Listing 4 setzt den rein funktionalen Teil dieser Aufgabe um: Er zerlegt die eingelesenen Zeile in ihre Bestandteile. Die Funktion »toUser« in Zeile 19 verwendet dazu die Haskell-typische Technik namens Pattern Matching: Sie erwartet eine Eingabe mit den drei Bestandteilen Login, Nachname und Vorname. Alle anderen Eingaben fängt das Jokerzeichen »_« in der folgenden Zeile auf und gibt eine Fehlermeldung aus.
Listing 4
Newusers-Programm, funktionaler Teil
01 {-# LANGUAGE OverloadedStrings #-}
02 import Data.Char (isLower, isNumber)
03 import Data.Text.Lazy (unpack, Text)
04 import Data.Monoid (mappend)
05 import Data.Text.Lazy (lines, Text, all, unpack, splitOn)
06 import Prelude hiding(lines, all)
07 import Shelly
08 (<>) = mappend
09 data NewUser = NewUser {
10 login :: Text
11 , lastName :: Text
12 , firstName :: Text
13 }
14
15 usersFromCSV :: Text -> [NewUser]
16 usersFromCSV csv = map userFromLine (lines csv) where
17 userFromLine line = toUser (line `splitOn` ",") where
18
19 toUser (l:lname:fname:[]) | checkLogin l = NewUser {login = l, lastName = lname, firstName = fname}
20 toUser _ = error $ unpack $ "expected \"login,lastname,firstname\" but got: " <> line
21
22 checkLogin = all (\char -> isLower char || isNumber char)
Dabei dient »$« als Operator mit niedriger Präzedenz, der dem Programmierer das Schreiben einiger Klammern erspart. Die Hilfsfunktion »checkLogin« prüft die Eingaben auf ihre Zugehörigkeit zu den erlaubten Zeichenmengen.
Listing 5 erledigt das eigentliche Äquivalent zum Skripting. In Zeile 7 startet »shelly« mit der »do« -Notation die »ShIO« -Monade. Die eingelesene CSV-Datei wertet die Map-Funktion »mapM_« , eine Variante für den Einsatz in einer Monade, in einer Schleife aus.
Listing 5
Skripting-Teil
01 mk_user :: NewUser -> ShIO Text 02 mk_user user = 03 run_sudo "addUser" [login user, "--gecos", firstName user <> " " <> lastName user] 04 run_sudo :: Text -> [Text] -> ShIO Text 05 run_sudo cmd args = run "/usr/bin/sudo" (cmd:args) 06 07 main = shelly $ do 08 csv <- readfile "users.csv" 09 mapM_ mk_user (usersFromCSV csv)
Skripting in Haskell muss also nicht schwerer sein als in populären Skriptsprachen, wie Listing 5 zeigt. Die Einteilung in reinen funktionalen und unreinen I/O-Code geht dem Haskell-Anwender bald in Fleisch und Blut über. Außerdem macht sie den Quelltext einfacher zu verstehen und zu warten.
Kompiliert der Code, ist sichergestellt, dass sich keine Tippfehler in den Namen von Funktionen oder Variablen befinden und dass der Programmierer die richtige Anzahl von Argumenten mit den richtigen Datentypen verwendet hat.
Sudo in Haskell
Außerdem lässt sich Haskells Typsystem auch einsetzen, um Berechtigungen anzufordern [5]. Zu diesem Zweck definiert Listing 6, dass die Funktion »mk_user« Sudo benötigt, um ihre Arbeit mit den Rechten des Superusers auszuführen. Damit lässt sich der Code aus Listing 5 nicht mehr kompilieren. Der aktualisierte Aufruf von »mk_user« muss »sudo« anfordern, wie Listing 7 zeigt.
Listing 7
Sudo anfordern
01 main = shelly $ do 02 csv <- readfile "users.csv" 03 mapM_ (sudo . mk_user) (usersFromCSV csv)
Listing 6
Sudo erforderlich
01 newtype Sudo a = Sudo { sudo :: ShIO a }
02
03 run_sudo :: Text -> [Text] -> Sudo Text
04 run_sudo cmd args = Sudo $ run "/usr/bin/sudo" (cmd:args)
05
06 mk_user :: NewUser -> Sudo Text
07 mk_user user =
08 run_sudo "addUser" [login user, "--gecos", firstName user <> " " <> lastName user]
Die Shelly-Bibliothek steht unter BSD-Lizenz und findet sich unter [3]. Der Autor verwendet sie derzeit in einem Installationsprogramm namens Cabal-meta [6] und seinem persönlichen Deployment-Skript. Im Zusammenspiel mit dem Make-Ersatz Shake [7] ergibt das eine schöne Werkzeugsammlung.
Einen ähnlichen Zweck erfüllt auch HSH [8]. Das im Web lesbare Buch “Real World Haskell” [9] beschreibt die Umsetzung der Bibliothek. HSH kümmert sich vor allem um die Weiterleitung der Ausgaben von der Kommandozeile an Haskell-Funktionen oder andere Befehle:
import HSH runIO $ "ls -l" -|- "wc -l"
Auch Hsshellscript [10] übernimmt Aufgaben, die der Administrator ansonsten mit Shellskripten erledigt. Auch dieses Modul bietet eine umfangreiche Auswahl an praktischen Programmen, die den Funktionsumfang einer Haskell-Standardinstallation erweitern.
Ausblick
Die Haskell-Community lernt derzeit auf Shellskripte zu verzichten und greift für immer mehr Aufgaben zu ihrer Lieblingssprache. Möchte dieser Artikel also jedermann zu Haskell bekehren? Nein, aber er hat hoffentlich einige Anregungen gegeben, die man auch in anderen Sprachen umsetzen kann. (mhu)
Infos
- Haskell: http://www.haskell.org
- Haskell Platform: http://hackage.haskell.org/platform/
- Shelly: http://hackage.haskell.org/package/shelly
- Miran Lipovaca, “Learn You a Haskell for Great Good!”: http://learnyouahaskell.com
- Don Stewart, “Scripting With Types”: http://donsbot.files.wordpress.com/2009/01/semicolon.pdf
- Cabal-meta: http://hackage.haskell.org/package/cabal-meta
- Shake: http://hackage.haskell.org/package/shake
- HSH: http://hackage.haskell.org/package/HSH
- Bryan O’Sullivan, Don Stewart und John Goerzen, “Real World Haskell”: http://book.realworldhaskell.org/read/systems-programming-in-haskell.html
- Hsshellscript: http://hackage.haskell.org/package/hsshellscript






