Open Source im professionellen Einsatz
Linux-Magazin 09/2007

Tipps täglich verschicken

Füttern nach Programm

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.

1140

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:
»xaday«

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.

Linux-Magazin kaufen

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

Deutschland

Ähnliche Artikel

  • Perl-Snapshot Linux-Magazin 2011/06

    "Perlmeister" Michael Schilli hat seinen Snapshot aus dem Magazin 2011/06 als Screencast verarbeitet.

  • Datenspeicher für Sparsame

    SQLite bringt zwar die Funktionalität einer relationalen SQL-Datenbank mit, kommt aber ohne aufwändig zu administrierende Server aus. Dieser Artikel vergleicht die Bibliothek mit ihren Konkurrenten und zeigt, wie man sie in eigene Programme einbaut.

  • Perl-Snapshot

    Streicht ein Besitzer von Amazons E-Book-Reader Textstellen auf dem Gerät an, legt sein Kindle diese persönlichen Markierungen in einer Datei ab. Steckt der Kindle im USB-Port des Linux-Rechners, saugt ein Perl-Skript die Daten ab, speichert sie in einer Datenbank und erlaubt später Volltext-Suchabfragen.

  • Geiz ist geil

    Ein Perl-Skript verfolgt für Schnäppchenjäger die Preisentwicklung auf Amazon und schlägt per E-Mail freudig Alarm, falls sich überwachte Produkte plötzlich verbilligen.

  • Papiercontainer

    Manche Zeitschriftenartikel gibt es einfach nicht online. Das Skript, um das es in dieser Ausgabe des Perl-Snapshots geht, archiviert die eingescannte Druckversion solcher Beiträge im PDF-Format und nutzt eine kleine Datenbank, um sie später wiederzufinden.

comments powered by Disqus

Ausgabe 07/2017

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

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