Elefantenführer (Linux-Magazin, Oktober 2004)

Um nur schnell aufgesetzte Papierbriefe sauber formatiert auszudrucken, lohnt sich der Start von OpenOffice kaum. Hartgesottene Programmierer erledigen das lieber mit ihrem Lieblingseditor und fernsteuern Formatierung, Empfängerauswahl und den Druckvorgang mit Perl.

OpenOffice bietet mittlerweile eine echte Alternative zu tradionell Windows-dominierten Office-Produkten. Es ist erstaunlich leicht zu installieren und lässt sich zudem (anders als seine proprietären Kollegen) auch noch einfach von außen manipulieren, da es seine internen Datenstrukten, wie das ``Open'' im Namen verrät, vollständig veröffentlicht.

Das heute vorgestellte Skript mailit arbeitet mit einem einmal angefertigten OpenOffice-Dokument nach Abbildung 1. Diese Vorlage gibt stets gleichbleibende Angaben wie den Absender, die Grußformel, und die gewünschte Formatierung des Briefes vor und setzt für alle dynamisch eingesetzten Textpassagen folgende Platzhalter ein:

    [% date %]        # Datum
    [% recipient %]   # Empfänger-Adresse
    [% subject %]     # Betreff
    [% text %]        # Brieftext

Abbildung 1: Das vorgefertigte Office-Dokument C enthält Platzhalter für dynamisch eingesetzte Textpassagen.

Aufgabe von mailit ist es nun, aus einer reinen Textdatei wie in Abbildung 2 unter Verwendung der Vorlage einen sauber gesetzten Brief wie in Abbildung 3 zu generieren. Die Textdatei folgt einem einfachen absatzorientierten Format: Der erste Absatz enthält die Betreffzeile des späteren Briefes, alle folgenden geben die Absätze des Brieftextes an. Das Datum rechts oben im Brief wird dabei entsprechend dem aktuellen Tagesdatum generiert und entsprechend der eingestellten Landessprache formatiert.

Abbildung 2: Die Textversion des Briefs im Editor vi. Der erste Absatz gibt den Betreff an, der Rest den Brieftext.

Abbildung 3: Der fertige Brief in OpenOffice, nachdem Perl die Textpassagen eingearbeitet hat.

Auf den Schultern von Riesen

Bei der Implementierung von mailit kamen sage und schreibe fünf CPAN-Module zum Zug, deren Einsatzmöglichkeiten üblicherweise viel weiter reichen als nur für kurze Skripteskapaden.

Zum einen ist da OpenOffice::OODoc, das eine objektorientierte Schnittstelle auf den Inhalt und die Struktur von OpenOffice-Dokumenten bereitstellt. Für die Zwecke von mailit, das reine Textersetzung vornimmt, genügt die Unterklasse OpenOffice::OODoc::Text.

Der Konstruktor new öffnet in Zeile 24 zuerst die angegebene OpenOffice-Datei, die wie in Abbildung 1 gezeigt ein Template-Dokument im gewünschten Format mit den Platzhaltern enthält. Die Methode getTextElementList() extrahiert daraufhin eine Liste aller Text-Elemente im Text. Die zurückgegebenen Werte sind allesamt Referenzen auf Objekte vom Typ XML::XPath::Node::Element, da OpenOffice::OODoc unter der Haube heftig XML::XPath für die interne Darstellung der ihrerseits XML-basierten OpenOffice-Dokumente nutzt.

Diese Tatsache muss aber nicht weiter interessieren, denn um zum Beispiel den Text eines von der Listfunktion gelieferten Elements $element zuextrahieren, ruft man einfach die getText()-Methode des OpenOffice::OODoc::Text-Objekts auf und übergibt die Elementreferenz:

    $doc->getText($element);

Den so erhaltenen Text, der typischerweise einen Absatz im OpenOffice-Dokument repräsentiert, untersucht mailit dann auf vorkommende Platzhalter im Format [% xxx %] und ersetzt deren Werte entsprechend der Vorgaben. Das Ergebnis schreibt mailit anschließend mittels der setText()-Methode wieder zurück ins Dokument, ebenfalls unter Angabe der Elementreferenz:

    $doc->setText($element, $text);

Mit Kanonen auf Spatzen

Die Textersetzung nimmt das mächtige Template Toolkit vor, das eigentlich viel mehr kann als nur Platzhaltern Leben einzuhauchen: Es ist das neue IN-Modul für Web-Applikationen, die von Designern gestaltete HTML-Templates mit dynamischen Daten füllen.

Hierzu erzeugt Zeile 50 ein Objekt der Klasse Template. Der ab Zeile 52 definierte Hash %vars ordnet den Platzhaltern im Dokument ihre dynamisch zugewiesenen Werte zu.

Die anschließend aufgerufene process()-Methode des Template-Moduls nimmt in mailit drei Parameter entgegen:

Letztere ist optional, eignet sich aber in mailit gut dafür, den bearbeiteten Text gleich per setText() an das OpenOffice-Dokument weiter zu reichen.

Das so modifizierte OpenOffice-Dokument landet in später in Zeile 76 per save in einer neuen temporären Datei, die das Modul File::Temp elegant neu anlegt.

File::Temp ist der Cadillac unter den Tempfile-Modulen, denen es nur darum geht, temporäre Dateien zu erzeugen, ohne mit bereits bestehenden zu kollidieren. Der Benutzer darf wählen, in welchem Verzeichnis die Datei landen soll (/tmp), welche Endung sie aufweist (.sxw im vorliegenden Fall) und nach welcher Vorlage der Name generiert wird. TEMPLATE => 'ooXXXXX' gibt im vorliegenden Fall an, dass nach einem einleitenden oo (für OpenOffice) fünf zufällige Zeichen stehen, der komplette Name der Temp-Datei also beispielsweise etwa wie "/tmp/oo2hkss.sxw" aussieht.

Außerdem bestimmt der UNLINK-Parameter, ob das Modul die Datei wegputzt, wenn das zugehörige Objekt erlischt. Das zurückgelieferte Handle lässt sich einfach als File-Handle verwenden und verwandelt sich innerhalb eines Strings ("$temp") flugs in den Namen der temporären Datei.

Das bewährte Modul Date::Calc zur Datumsberechnung hilft mailit, das heutige Datum zu bestimmen und es landestypisch ins Format XX. Monat, Jahr umzuwandeln. Hierzu setzt es zunächst das Locale mit

    Language(Decode_Language("Deutsch"));

und ruft weiter unten Month_to_Text(), auf um die von der Funktion Today() zurückgegebenen Monatsnummer in den deutschen Monatsnamen umzuwandeln.

Menschenlesbare Adressenbank

Zur Bestimmung der mehrzeiligen Empfängeradresse, die den Platzhalter [% recipient %] im Dokument ersetzt, zieht mailit eine lokal erstellte Adressdatenbank heran.

Welches Format eignet sich für eine Datei mit bekannten Adressen, an die mailit Briefe adressieren kann? Computerlesbare Datenformate gibt es viele, an vorderster Stelle steht XML. Allerdings führen dessen exzessive Triangel beim menschlichen Leser schnell zu dreieckigen Augen und Kopfschmerzen.

Das von Brian Ingerson entwickelte YAML (YAML Ain't Markup Language) lässt sich hingegen nicht nur leicht parsen, sondern schmeichelt auch dem Auge des Betrachters. Eine Adressdatenbank, die ihre Datensätze per Kürzel indiziert und ihnen Werte für Name, Straße und Wohnort zuweist, schreibt sich in YAML einfach so:

    otto:
      - Otto Ollenhauer
      - Olle Straße 123
      - D-82922 Ottobrunn
    bea:
      - Beate Ballermann
      - Brezenweg 7
      - D-93312 Bremen
    ...

Reicht man den Namen der Datei an YAMLs LoadFile()-Funktion weiter, gibt diese eine Referenz auf einen Hash zurück, der die Kürzel als Schlüssel und die Einträge als Referenzen auf Arrays enthält:

  {
    'bea' => [
      'Beate Ballermann',
      'Brezenweg 7',
      'D-93312 Bremen'
    ],
    'otto' => [
      'Otto Ollenhauer',
      'Olle Straße 123',
      'D-82922 Ottobrunn'
    ]. ...
  }

YAML kann noch viel mehr. Es handelt sich tatsächlich nicht um eine Markup-Sprache, sondern um einen vielseitigen Daten-Serialisierer, der Perls beliebig tief verschachtelte Core-Datenstrukturen samt und sonders in leicht lesbaren ASCII-Text verwandelt und anschließend wieder zurück nach Perl importiert.

Es eignet sich ideal für Konfigurationsdateien, die von Programmen gelesen, aber auch von menschlichen Benutzern studiert und gewartet werden.

Abbildung 4: Das Adressbuch als Textdatei im YAML-Format.

Und ab geht die Post

mailit nimmt die Textversion des Briefs entweder als Dateiname entgegen oder erwaret ihn auf STDIN: mailit brief.txt und cat brief.txt | mailit funktionieren gleichermaßen, da Zeile 29 mit Perls magischer Eingaberaute arbeitet. Der reguläre Ausdruck in Zeile 33 trennt den ersten Absatz vom Rest des Briefs und legt die getrennten Bereiche in $subject und $body ab.

Die ab Zeile 82 definierte pick()-Funktion nimmt eine Reihe von Adressaten als Kürzel entgegen, präsentiert sie dem Benutzer als durchnumerierte Liste und lässt ihn eine per Nummer auswählen. Ein typischer Ablauf von mailit sieht folgendermaßen aus:

    mailit letter.txt
    [1] bea
    [2] otto
    [3] zephy
    Recipient [1]> 1
    Preparing letter for Beate Ballermann
    Printing /tmp/ooGd8H3.sxw

Um das temporäre Dokument zu drucken, wird in Zeile 71 einfach das OpenOffice-Binary mit der Option -p aufgerufen. Letztere verhindert einen langwierigen Start der Oberfläche und schickt statt dessen die überreichte *.sxw-Datei an den Standard-Drucker. Fertig!

Listing 1: mailit

    001 #!/usr/bin/perl
    002 ###########################################
    003 # mailit -- Print letters with OpenOffice
    004 # Mike Schilli, 2004 (m@perlmeister.com)
    005 ###########################################
    006 use warnings;
    007 use strict;
    008 
    009 my $CFG_DIR = "$ENV{HOME}/.mailit";
    010 my $OO_TEMPLATE   = "$CFG_DIR/usa.sxw";
    011 my $ADDR_YML_FILE = "$CFG_DIR/addr.yml";
    012 my $OO_EXE = "$ENV{HOME}/ooffice/soffice";
    013 
    014 use OpenOffice::OODoc;
    015 use Template;
    016 use YAML qw(LoadFile);
    017 use File::Temp;
    018 use Date::Calc qw(Language Decode_Language Date_to_Text
    019                   Today Month_to_Text);
    020 
    021 Language(Decode_Language("English"));
    022 my ($year,$month,$day) = Today();
    023 
    024 my $doc = OpenOffice::OODoc::Text->new(
    025     file => $OO_TEMPLATE,
    026 );
    027 
    028    # Read from STDIN or file given
    029 my $data = join '', <>;
    030 
    031    # Split subject and body
    032 my($subject, $body) = 
    033              ($data =~ /(.*?)\n\n(.*)/s);
    034 
    035    # Remove superfluous blanks
    036 my $text;
    037 for my $paragraph (split /\n\n/, $body) {
    038     $paragraph =~ s/\n/ /g;
    039     $text .= "$paragraph\n\n";
    040 }
    041 
    042 my $yml = LoadFile($ADDR_YML_FILE);
    043 my $nick = pick("Recipient", [keys %$yml]);
    044 
    045 my $recipient = $yml->{$nick};
    046 
    047 print "Preparing letter for ", 
    048       $recipient->[0], "\n";
    049 
    050 my $template = Template->new();
    051 
    052 my %vars = (
    053     recipient => join("\n", @$recipient),
    054     subject   => $subject,
    055     text      => $text,
    056     #date      => sprintf("%d. %s %d", 
    057     #  $day, Month_to_Text($month), $year),
    058     date      => Date_to_Text($year, $month, $day),
    059 );
    060 
    061 for my $e ($doc->getTextElementList()) {
    062 
    063     my $text_element = $doc->getText($e);
    064 
    065     $template->process(\$text_element,
    066         \%vars,
    067         sub { $doc->setText($e, $_[0]); });
    068 }
    069 
    070 my $oo_output = File::Temp->new(
    071     TEMPLATE => 'ooXXXXX',
    072     DIR      => '/tmp',
    073     SUFFIX   => '.sxw',
    074     UNLINK   => 1,
    075 );
    076 
    077 $doc->save($oo_output->filename);
    078 
    079 print "Printing $oo_output\n";
    080 system("$OO_EXE $oo_output");
    081 
    082 ###########################################
    083 sub pick {
    084 ###########################################
    085     my ($prompt, $options) = @_;
    086 
    087     my $count = 0;
    088     my %files = ();
    089 
    090     foreach (@$options) {
    091         print STDERR "[", 
    092               ++$count, "] $_\n";
    093         $files{$count} = $_;
    094     }
    095 
    096     print STDERR "$prompt [1]> ";
    097     my $input = <STDIN>;
    098     chomp($input);
    099 
    100     $input = 1 unless length($input);
    101     return "$files{$input}";
    102 }

Infos

[1]
Listings zu diesem Artikel: ftp://www.linux-magazin.de/pub/listings/magazin/2004/10/Perl

[2]
Homepage des OpenOffice projekts: http://openoffice.org

Michael Schilli

arbeitet als Software-Engineer bei Yahoo! in Sunnyvale, Kalifornien. Er hat "Goto Perl 5" (deutsch) und "Perl Power" (englisch) für Addison-Wesley geschrieben und ist unter mschilli@perlmeister.com zu erreichen. Seine Homepage: http://perlmeister.com.