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 |
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. |
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);
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:
%vars
Eine Referenz auf eine Funktion, der process()
nach erfolgreicher
Textersetzung den Ergebnisstring übergibt.
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.
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. |
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!
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 }
Michael Schilliarbeitet 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. |