Open Source im professionellen Einsatz
Linux-Magazin 06/2012
© Christopher Meder, 123RF

© Christopher Meder, 123RF

Haskell statt Shellskript

Starke Typen

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.

1323

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.

Abbildung 1: Die Haskell Platform ist die einfachste Möglichkeit, Haskell und seine Bibliotheken unter Linux, Windows und Mac OS X zu installieren.

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.

Diesen Artikel als PDF kaufen

Express-Kauf als PDF

Umfang: 4 Heftseiten

Preis € 0,99
(inkl. 19% MwSt.)

Linux-Magazin kaufen

Einzelne Ausgabe
 
Abonnements
 
TABLET & SMARTPHONE APPS
Bald erhältlich
Get it on Google Play

Deutschland

Ähnliche Artikel

comments powered by Disqus

Ausgabe 11/2017

Digitale Ausgabe: Preis € 6,40
(inkl. 19% MwSt.)

Stellenmarkt

Artikelserien und interessante Workshops aus dem Magazin können Sie hier als Bundle erwerben.