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).
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.