Englische Vokabeln oder Vim-Kommandos in mundgerechten Stücken serviert sind ungleich verdaulicher als jedes Supersize-Mahl. Das diesmal im Snapshot vorgestellte Skript macht Lernwillige zu Verkostern eines täglich wechselnden E-Mail-Häppchens.
Obgleich oder weil ich seit zehn Jahren in den USA wohne, versuche ich mein Englisch zu verbessern. Wer außer Muttersprachlern weiß schon, was “Cynosure” heißt? Oder “Exonym”? Oder “Schtick”? Seit mir jeden Tag eine E-Mail von “A Word A Day” [2] ins Haus flattert, die ein neues Wort vorstellt, lerne ich Englisch im Vorbeigehen. Abbildung 1 zeigt, wie der Service das Wort mit (hoffentlich) einfacheren Vokabeln umschreibt. Auch zitiert er Beispiele aus öffentlichen Schriften, um die ordnungsgemäße Verwendung zu zeigen.

Abbildung 1: Jeden Tag eine neue Vokabel lernen: Die Erklärung für das Wort „Exonym“ hält der Dienst „A Word A Day“ bereit.
Jeden Tag ein Tipp
Die Effektivität des Verfahrens legt die Idee nahe, es auf andere Bereiche auszudehnen. So gibt es tägliche Tipps für den Editor Vim, warum nicht auch welche für Perl? Oder die Java-Falle des Tages? Fehlen nur ein Skript, das die Tipps ablegt, und ein Cronjob, der sie morgens an eine Liste mit nach Infohäppchen lechzenden Subscribern abschickt.
Das Skript »xaday« (x-a-day, Listing 1) nutzt eine SQLite-Datenbank ([3], [4]) als Tippspeicher, erlaubt aber das Editieren der Tipps in einem normalen Editor. Mit dem Parameter »-m« und einer E-Mail-Adresse aufgerufen, schickt es jeweils einen Tipp ab, merkt sich das Publikationsdatum in der Datenbank und wird bei einem erneuten Aufruf stets einen noch unveröffentlichten Tipp heranziehen. So arbeitet es eine Warteschlange Eintrag für Eintrag ab, bis nichts mehr übrig ist. Will der Tipp-Versender neue Tipps in die Warteschlange einfügen, ruft er »xaday -e« auf und trägt sie ein (Abbildung 2).
|
Listing 1: |
|---|
001 #!/usr/bin/perl -w
002 ###########################################
003 # xaday - Mail out a tip every day
004 # Mike Schilli, 2007 (m@perlmeister.com)
005 ###########################################
006 use strict;
007 use Rose::DB::Object::Loader;
008 use Getopt::Std;
009 use File::Temp qw(tempfile);
010 use Sysadm::Install qw(:all);
011 use Mail::Mailer;
012
013 my $RECSEP = qr/^=head1/;
014 my $HEAD = "=head1";
015 my $MAIL_FROM = 'me@_foo.com';
016
017 getopts("d:lepm:f:", my %opts);
018
019 die "usage: $0 -d dbfile ..."
020 unless $opts{d};
021
022 my $loader = Rose::DB::Object::Loader->new(
023 db_dsn => "dbi:SQLite:dbname=$opts{d}",
024 db_options => {
025 AutoCommit => 1, RaiseError => 1 },
026 );
027
028 $loader->make_classes();
029
030 if($opts{e} or $opts{l}) {
031 my($fh, $tmpf) = tempfile(UNLINK => 1);
032 my $tips =
033 Tip::Manager->get_tips_iterator();
034 my $data_before = "";
035
036 while(my $tip = $tips->next()) {
037 $data_before .=
038 "$HEAD " .
039 $tip->head() . " {" .
040 $tip->id() . "}" . "nn" .
041 $tip->text() . "nn";
042 }
043 if($opts{l}) {
044 print $data_before;
045 exit 0;
046 }
047 blurt($data_before, $tmpf);
048 system("$ENV{EDITOR} $tmpf");
049 my $data_after = slurp($tmpf);
050 die "No change" if
051 $data_before eq $data_after;
052
053 db_update($data_after);
054 }
055
056 if($opts{f}) {
057 db_update($opts{f});
058 }
059
060 if($opts{m}) {
061 my $tips = Tip::Manager->get_tips(
062 query => [ "published" => undef],
063 sort_by => 'id',
064 limit => 1,
065 );
066 if(@$tips) {
067 $tips->[0]->published(
068 DateTime->today());
069 $tips->[0]->update();
070 mail($opts{m}, $tips->[0]->head(),
071 $tips->[0]->text());
072 } else {
073 die "Nothing left to publish";
074 }
075 }
076
077 ###########################################
078 sub text2db {
079 ###########################################
080 my($text) = @_;
081 $text = "" unless defined $text;
082
083 my @fields = ();
084
085 while($text =~
086 /^($RECSEP.*?)
087 (?=$RECSEP|s*Z)/smgx) {
088 my($head, $info, $tip) = rec_parse($1);
089 $tip =~ s/s+Z//;
090 $tip =~ s/As+//;
091 push @fields, [$head, $info, $tip];
092 }
093 return @fields;
094 }
095
096 ###########################################
097 sub rec_parse {
098 ###########################################
099 my($text) = @_;
100
101 if($text =~ /$RECSEPs+(.*?)
102 (?:s+{(.*?)})?
103 $
104 (.*)
105 /smgx) {
106 return($1, $2, $3);
107 }
108
109 return undef;
110 }
111
112 ###########################################
113 sub db_update {
114 ###########################################
115 my($in) = @_;
116
117 my $data;
118
119 if( ref($in) ){
120 $data = $$in;
121 } else {
122 $data = slurp($in);
123 }
124
125 my $fields = text2db($data);
126
127 my @keep_ids = map { $_->[1] } @$fields;
128 my $gone;
129 if(@keep_ids) {
130 $gone = Tip::Manager->delete_tips(
131 where => ["!id" => @keep_ids] );
132 } else {
133 $gone = Tip::Manager->delete_tips(
134 all => 1 );
135 }
136 print "$gone rows deletedn" if $gone;
137
138 for(@$fields) {
139 my($head, $info, $tip) = @$_;
140
141 my $rec;
142
143 if(defined $info) {
144 $rec = Tip->new(id => $info);
145 $rec->load();
146 $rec->head($head);
147 $rec->text($tip);
148 $rec->update();
149 } else {
150 $rec = Tip->new(
151 text => $tip,
152 head => $head,
153 );
154 $rec->save();
155 }
156 }
157 }
158
159 ###########################################
160 sub mail {
161 ###########################################
162 my($to, $head, $body) = @_;
163
164 my $mailer = Mail::Mailer->new();
165
166 $mailer->open({
167 'From' => $MAIL_FROM,
168 'To' => $to,
169 'Subject' => $head,
170 });
171 print $mailer $body;
172 close $mailer;
173 }
|

Abbildung 2: Der Versender kann neue Tipps auch per Hand in einem normalen Editor verfassen – hier drei vorbereitete Vim-Tipps.
Ähnlich wie bei Perls POD-Format stehen die Tipps durch »=head1«-Überschriften getrennt in der editierten Datei. Die Überschrift wird zur Subject-Zeile, der Text zum Body der ausgehenden Tipp-E-Mail. Speichert der Benutzer die geänderte Datei, schiebt »xaday« den Inhalt zurück in die Datenbank. Hierbei ist es sowohl möglich, neue Tipps hinzuzufügen als auch wartende oder bereits ausgesandte zu korrigieren.
Abbildung 3 demonstriert, wo die Infohäppchen in der Tabelle »tips« der Datenbank landen. Der SQLite-Client zeigt dabei Tabellen nicht so schön in Ascii-Art an wie MySQL, aber mit den Formatierungskommandos ».width«, ».mode« und ».headers« wird\’s ansehnlich. Näheres zu SQLite findet sich in dem empfehlenswerten Buch [5], das nicht nur die dateibasierte Datenbank mit ihren Features preist, sondern auch Grundlagen der relationalen Theorie anschaulich zu vermitteln weiß.

Abbildung 3: Die vorbereiteten Tipps liegen in einer SQLite-Datenbank. Mit ein paar Formatierungsanweisungen zeigt deren Client die Tabelle ganz passabel an.
Für die einfache Applikation reicht eine Tabelle, die in den Spalten »head«, »text« und »published« die Überschrift, den Text und das Publikationsdatum der konservierten Tipps aufnimmt. Die Datei »xaday.sql« in Abbildung 4 zeigt die dafür notwendigen SQL-Kommandos. Der SQLite-Client »sqlite3« führt sie mit
sqlite3 dbname.dat <xaday.sql
aus. Das Kommando legt in der Datei »dbname.dat« eine Datenbank an, auf die der Perl-Client später mit SQL-Kommandos zugreift.

Abbildung 4: Der SQLite-Client bekommt die Datei »xaday.sql« vorgesetzt. Die SQL-Kommandos legen eine Datenbanktabelle an.
Auf Rose gebettet
Statt SQL-Kommandos abzusondern und die zurückkommenden Daten in Perl zu interpretieren, benutzt »xaday« wie einige früher vorgestellte Skripte die objektrationale Schnittstelle DB::Rose vom CPAN. Deren Loader beschnüffelt beim Programmstart die Datenbanktabelle nur kurz, die Methode »make_classes« erzeugt automatisch alle Mappings im Perl-Code.
Ruft der Benutzer »xaday« mit der Option »-e« auf, möchte er weitere Tipps einfügen. Zeile 31 in Listing 1 erzeugt dafür eine temporäre Datei, die der Editor speist. Den eigentlichen Neu-Tipp hängt der Bediener einfach mit einer »=head1«-Überschrift an. Die Environment-Variable »EDITOR« zeigt den bevorzugten Editor, »vim«, »emacs« oder sogar »pico« sind gängige Einstellungen.
Nach dem Editor-Aufruf prüft das Skript, ob sich die editierte Datei verändert oder ob sich der Benutzer dazu entschieden hat, die Änderungen abzubrechen. Im zweiten Fall verabschiedet sich das Programm mit einer Meldung. Hat sich die Datei jedoch verändert, müssen die Änderungen in die Datenbank wandern.
Schnelle Nummer
Damit das Programm erkennt, welche Beiträge aus der Datenbank stammen und welche der Benutzer neu eingetippt hat, hängt »xaday« an jede Überschrift aus der Datenbank deren Identifikationsnummer in geschweiften Klammern an. Steht also in der zu editierenden Datei zum Beispiel »”=head1 Überschrift {13}”«, dann stammt der Eintrag aus der Zeile der Datenbanktabelle mit der ID-Nummer 13.
Wer die Reihenfolge der Beiträge ändern will, passt die Nummern an, denn der E-Mailer geht streng nach aufsteigender ID vor. Neuen Einträgen ohne ID verpasst das Programm eine neue Nummer und fügt sie in die Datenbank ein. Der »AUTOINCREMENT«-Mechanismus in der SQL-Definition sorgt dafür, dass neue Einträge stetig aufsteigende IDs erhalten.
Den Stapel abarbeiten
Geht es ans Aussenden der Mail, muss das Skript den Tipp mit der niedrigsten ID-Nummer finden, dessen »published«-Eintrag noch kein Datum, sondern »NULL« enthält. Zeile 61 feuert mit »get_tips()« unter der Haube einen SQL-Query ab, der nach allen Records sucht, deren »published«-Eintrag »NULL« ist, und sortiert die Ergebnisliste aufsteigend nach der ID. Das auf »1« gesetzte Limit sorgt dafür, dass nur das erste Ergebnis aus der Datenbank zurückkommt.
Dem gefundenen Eintrag verpasst die Methode »published« mit dem Parameter »DateTime->today()« das heutige Datum, ein nachfolgendes »update()« speichert die Änderung in der Datenbank. Sind alle Tipps aufgebraucht, bricht das Skript in Zeile 73 mit einem Fehler ab, was der ausführende Cronjob dem Nutzer promt per E-Mail mitteilt.
Immer den Blick voraus
Um in der Textdatei die einzelnen Tipp-Einträge voneinander zu trennen, setzt die Funktion »text2db()« einen regulären Ausdruck mit positivem Look-Ahead ein. Das mit »(?=« eingeleitete Konstrukt schluckt den gefundenen Ausdruck nicht, sondern spitzelt nur ein wenig voraus. Ist ein mit »=head1« eingeleiteter Absatz gefunden, reicht ihn »text2db()« an die ab Zeile 97 definierte Funktion »rec_parse()« weiter. Diese versucht die Spaltenwerte für »head«, »id« und »text« zu extrahieren. Im Erfolgsfall reicht sie alle drei Werte zurück – im Fehlerfall hingegen »undef«. »text2db()« nimmt noch einige kosmetische Korrekturen vor und stutzt einleitenden und abschließenden Whitespace zurecht.
Die Funktion »db_update()« ab Zeile 113 nimmt entweder einen Dateinamen (als Skalar) oder einen Datenstring (als Referenz) entgegen. Sie ruft »text2db()« auf, extrahiert die Teilfelder aller Tipps und findet heraus, welche Felder der Bediener aus der ursprünglichen Datenbank gelöscht hat. Alle anderen IDs legt sie im Array »@keep_ids« ab.
Rose auffrischen
Mit der Bedingung »”!id” => @keep_ids« löscht die Methode »delete_tips()« alle Records aus der Datenbank, deren ID-Felder mit keinem der Werte im Array »@keep_ids« identisch sind. Ist das Array leer, stellt DB::Rose auf stur und weigert sich, alle Einträge zu löschen. Dann setzt der »else«-Zweig ab Zeile 132 einen »delete_tips()«-Aufruf mit dem Flag »all« ab. Die Anzahl der gelöschten Reihen zeigt das Skript in beiden Fällen auf der Standardausgabe an.
Zeile 138 iteriert über alle Tipps. Führen einige eine ID-Nummer, nimmt der Skalar »$info« diese IDs für die Aktualisierung der Daten des korrespondierenden Datenbankeintrags auf. Rose lädt hierzu ein neu erzeugtes Objekt der Klasse »Tip« erst mit »load()« aus der Datenbank, frischt die einzelnen Felder mit den bereitgestellten Methoden »head()«, »text()« und so weiter auf und bemüht anschließend »update()«, um die Daten vom lokalen Speicher in die Datenbank zurückzuschreiben.
Ist »$info« nicht definiert, legen die Zeilen 150 bis 153 einfach einen neuen Record mit den Werten für die Kopfzeile »head« und dem Tippinhalt »text« an und lassen die Datenbank mit »AUTOINCREMENT« eine neue ID bestimmen. Die Methode »save()« kümmert sich um den Datenbankzugriff. Ausgehende Mails sendet das CPAN-Modul Mail::Mailer. Die Funktion »mail()« ab Zeile 160 nimmt nur die Empfängeradresse, die Kopfzeile und den Inhalt des Tipps entgegen und verhandelt selbstständig mit dem lokalen »sendmail«-Programm.
Installation
Alle verwendeten Module stehen auf dem CPAN zum Download bereit, mit einer CPAN-Shell lösen sich Abhängigkeiten schnell auf. Die Variable »MAIL_FROM« aus Zeile 15 ist an die für die Tipps verwendete Mailingliste anzupassen. Damit das Skript jeden Morgen um 7:30 Uhr seine E-Mails verschickt, setzt
30 07 * * * /Pfad_zu/xaday -d /Pfad_zu/dbfile.dat -m mlist@somewhere.com
einen Cronjob auf. »dbfile.dat« ist die SQLite-Datei, die mit »-m« angegebene Mailingliste ist die Zieladresse, auf der Abonnenten auf tägliche Tipps lauern. Zum Auffüllen der Datenbank mit Tipps ruft der Ratgeber
xaday -d /Pfad_zu/dbfile.dat -e
auf, verfasst im aufgehenden Editor seine Tipps und sichert die Änderungen. Alternativ zum Editor darf er dem Skript mit der Option »-f« eine Textdatei unterjubeln. Um zu prüfen, ob die Tipps auch richtig in der Datenbank angekommen sind, zeigt die Option »-l« die bislang eingespeisten Daten an.
Erweiterungen
Wer seine Tipps nur an Werktagen abschicken möchte, passt den Cronjob an oder ändert das Skript dergestalt ab, dass es im »$opts{m}«-Zweig den Wochentag abfragt und sich beendet, falls dies ein Samstag oder Sonntag ist. Das Perl-Konstrukt »(localtime(time))[6])« liefert den aktuellen Wochentag als Zahl von 0 (Sonntag) bis 6 (Samstag).
Wer an allen gesetzlichen Feiertagen ruhen will, kann dies über eine zusätzliche Tabelle »holidays« einbauen, die bevorstehende Feiertage als Datum in einer Spalte »date« ablegt. Das Skript könnte hierzu auch eine Kommandozeilenoption »-d« anbieten, die ein angegebenes Datum in die Tabelle aufnimmt.
Das Wichtigste bleiben jedoch die Tipps selbst – die verfasst man am besten für etwa eine Woche im Voraus, sodass ein paar Tage Urlaub nicht den Tippreigen zum Erliegen bringen. Was nun noch fehlt, ist die Auflösung der beiden anderen rätselhaften Vokabeln: “Cynosure” ist der Anziehungspunkt und “Schtick” kommt aus dem Jiddischen und heißt Masche oder Nummer. (jk)
|
Infos |
|---|
|
[1] Listings zu diesem Artikel: [ftp://www.linux-magazin.de/pub/listings/magazin/2006/09/Perl] [2] A Word A Day: [http://wordsmith.org/awad/] [3] SQLite: [http://www.sqlite.org] [4] C. Dalitz, “SQLite – Datenbank-Engine im Kleinformat”: Linux-Magazin 09/04, S. 96 [5] Michael Owens, “The Definitive Guide to SQLite”: Apress 2006 |
|
Der Autor |
|---|
|
|






