Das Perl-Skript »dagobert« zeigt auf Kommando die aktuellen Vermögensverhältnisse seines Besitzers an. Es rechnet den Inhalt mehrerer Konten und Aktiendepots zusammen und gestattet es dem Anwender, eigene Plugins einzuhängen.
Entgegen früheren Erkenntnissen belegen neue sozialwissenschaftliche Studien: Reiche Menschen sind nicht unglücklicher als mittellose. Ein Aufatmen geht durchs Land – die Angst vor monetär bedingter Miesepetrigkeit ist weg. Viele trauen sich nach Jahren erstmals an eine Summation ihrer Vermögensverhältnisse. Aber wie ging das nur?
Wer nicht alles in bar unter den Dielen seiner Villa versteckt hat, sondern in Konten und Depots bei der Bank, greift zu Programmen wie Gnucash, die bei der Buchführung unterstützen und Kontostände sauber formatiert oder gar grafisch ausgeben. Doch verlangen die in der Tradition von Quicken und Microsoft Money stehenden freien Ableger eiserne Disziplin bei der Buchführung.
Aber welcher Freizeitbuchhalter hat die Zeit, minutiös alle Ausgaben einzutragen? Mal ganz abgesehen von der Installationsorgie, die Gnucash den Anwenderfreuden vorausschickt, birgt es eine weitere Hürde: Es ist nicht einfach und beliebig erweiterbar. Das vorgestellte Perl-Skript dient kleinen Dagoberts, die nur einmal im Monat zehn Minuten dafür erübrigen, Kontostände aufzufrischen, dann aber täglich mit online verfügbaren Börsenkursen ruck zuck ihr Hab und Gut in bare Münze umrechnen können.
Außerdem lässt sich das System per Plugin flexibel erweitern. Währungsumrechnungen oder Steuergesetze sind schnell eingearbeitet und der Benutzer passt das System an seine speziellen Bedürfnisse an, ohne gleich Bloatware zu erzeugen.
Freilich kann man\’s auch übertreiben: Für Aktiensplits, die sich alle paar Jahre mal ereignen, ist kein besonderes Modul erforderlich, die kann man auch von Hand korrigieren. (Motto: Wer jedermanns Liebling sein will, ist irgendwann jedermanns Depp.) Das Skript »dagobert« sucht den Mittelweg. Es bietet Basisfunktionen, die den Inhalt mehrerer Konten und Aktiendepots zusammenrechnen, überlässt es aber dem Anwender, Plugins für spezielle Anforderungen einzuhängen.
Skript als Interpreter
Kontendaten definiert der Anwender in einer Datei »money« nach Abbildung 1. Das Schlüsselwort »account« definiert ein neues Konto, eine Aktienposition startet mit »stock« und der Bargeldbestand mit »cash«. Der Interpreter dieser Finanzdaten ist »dagobert« aus Listing 1, der die Kontodefinitionen einliest, aktuelle Börsenkurse einholt und Gewinne, Verluste und Gesamtstand ausrechnet. Das Finanzskript lässt sich mit
dagobert money
aus der Kommandozeile aufrufen, aber es geht einfacher: Man macht die Konfigurationsdatei »money« ausführbar und hängt den »dagobert«-Interpreter in die She-Bang-Zeile. So läuft »money« als Skript ab, nicht mit »perl« als Interpreter, sondern mit »dagobert«.
Wer nicht gerade unter der hypermodernen Zsh-Shell arbeitet, sondern mit der guten alten Bash, die Skripte und Programme ohne Magie gleich vom Kernel ausführen lässt, muss aber in der She-Bang-Zeile auf Skripte verzichten. Stattdessen wird einfach schnell ein C-Wrapper in einem C-Programm »dago.c« herumgewickelt:
main(int argc, char **argv) {
execv("/usr/bin/dagobert", argv); }
Kompiliert man »dago.c« jetzt mit
cc -o dago dago.c
dann lässt sich das Executable »dago« im She-Bang als Interpreter der Finanzdaten verwenden:
#!/usr/bin/dago account DeutscheBank stock SIEGn.DE 10 62.38 # ...
Falls nun die diesen Code enthaltende Datei »money« ausführbar ist, genügt der Aufruf »money« – und der Geldzähler startet. Was eigentlich aussieht wie eine Konfigurationsdatei, ist in Wirklichkeit ein ausführbares Skript. Abbildung 2 zeigt die Ausgabe. Praktisch!

Abbildung 1: Die Daten des Kontoinhabers definiert eine Konfigurationsdatei, die gleichzeitig ein ausführbares Skript ist.
|
Listing 1: |
|---|
01 #!/usr/bin/perl -w 02 ########################################### 03 # dagobert - Money Counting Interpreter 04 # Mike Schilli, 2004 (m@perlmeister.com) 05 ########################################### 06 use strict; 07 08 use Plugger; 09 my $string = join '', <>; 10 11 my $plugger = Plugger->new(); 12 $plugger->init(); 13 $plugger->parse($string); |

Abbildung 2: Geldzähler »dagobert« in Aktion: Von der Kommandozeile aus aufgerufen zeigt er Einzelkonten und Gesamtvermögen farbig angehübscht an.
Interpreter lernt durch Plugins dazu
Der Interpreter in Listing 1 ist sehr kurz gehalten: Er erzeugt eine neue Instanz eines Objekts vom Typ »Plugger«, initialisiert die dahinter liegende Plugin-Architektur mit »init()« und übergibt die zuvor mit »<>« von der Standardeingabe eingelesenen Konfigurationsdaten der »parse()«-Methode des Plugin-Systems. Das Framework in Listing 2 interpretiert das jeweils erste Wort einer Zeile als Kommando. Ohne Plugins ist es jedoch erst mal nicht in der Lage, irgendein Kommando zu interpretieren. Lediglich Kommentarzeilen, die mit einem »#« beginnen, verwirft es vorsorglich.
|
Listing 2: |
|---|
01 ###########################################
02 package Plugger;
03 ###########################################
04 use strict; use warnings;
05
06 use Module::Pluggable
07 require => 1,
08 search_path => [qw(Plugger)];
09
10 our %DISPATCH = ();
11 our %MEM = ();
12
13 ###########################################
14 sub new {
15 ###########################################
16 my($class) = @_;
17
18 bless my $self = {}, $class;
19
20 return $self;
21 }
22
23 ###########################################
24 sub init {
25 ###########################################
26 my($self) = @_;
27
28 $_->init($self) for $self->plugins();
29 }
30
31 sub mem { return %MEM; }
32
33 ###########################################
34 sub parse {
35 ###########################################
36 my($self, $string) = @_;
37
38 for(sort keys %DISPATCH) {
39 $DISPATCH{$_}->{start}->($self) if
40 $DISPATCH{$_}->{start};
41 }
42
43 for(split /n/, $string) {
44
45 s/#.*//;
46 next if /^s*$/;
47 last if /^__END__/;
48 chomp;
49
50 my($cmd, @args) = split ' ', $_;
51
52 die "Unknown command: $cmd" unless
53 exists $DISPATCH{$cmd};
54
55 $DISPATCH{$cmd}->{process}->($self,
56 $cmd, @args);
57 }
58
59 for(sort keys %DISPATCH) {
60 $DISPATCH{$_}->{finish}->($self) if
61 $DISPATCH{$_}->{finish};
62 }
63 }
64
65 ###########################################
66 sub register_cmd {
67 ###########################################
68 my($self, $cmd, $start,
69 $process, $finish) = @_;
70
71 $DISPATCH{$cmd} = {
72 start => $start,
73 process => $process,
74 finish => $finish,
75 };
76 }
77
78 1;
|
»Plugger.pm« liest jedes zum Verzeichnis »Plugger/« hinzugefügte Modul während der Compile-Phase automatisch ein. Dies erledigt das in Zeile 6 eingebundene CPAN-Modul Module::Pluggable, denn die Zeilen 7 und 8 setzen dessen »require«-Flag und den Suchpfad zu den Plugins relativ zum aktuellen Verzeichnis oder zum »@INC«-Pfad.
Die Plugins haben keinen »new()«-Konstruktor, wie in der Objektorientierung eigentlich üblich, sondern eine »init()«-Funktion, die »Plugger.pm« als Herr aller Plugins für jedes gefundene Plugin-Modul nacheinander aufruft. Module::Pluggable fügt in seinen Wirt – »Plugger« in diesem Fall – automatisch die Methode »plugins()« ein. Sie gibt die Namen aller gefundenen Plugins als Liste zurück. Zeile 28 nutzt diesen Mechanismus, um durch die »init()«-Funktionen aller Plugins zu orgeln.
Damit ein Plugin weiß, wer der Aufrufer ist und gegebenenfalls dessen Methoden aufruft, übergibt »Plugger.pm« der »init()«-Methode des Plugins jeweils die Referenz » (von Context). Dahinter steckt nichts anderes als eine Referenz auf das einzige existierende Plugger-Objekt, den Plugin-Verwalter. Damit vermag ein Plugin nun seinerseits Anweisungen an den Verwalter »Plugger« zu schicken. Da »Plugger« die Kommandos in einer Konfigurationsdatei interpretiert, ruft das Plugin die Verwalter-Methode »register_cmd()« auf, um neue Kommandos zu registrieren.
Account-Plugin bekommt Argumente-Hilfe
Listing 3 zeigt ein im Verzeichnis »Plugger/« hängendes Plugin: »Account.pm«. Es nutzt den beschriebenen »register_cmd()«-Mechanismus, um dem Plugin-Verwalter das Kommando »account« beizubringen:
$ctx->register_cmd("account",
&start, &process, &finish);
Der Zweizeiler definiert gemäß den Regeln des Framework, dass Plugger nach der Interpretation des Schlüsselworts »account« in der Konfigurationsdatei die Funktion »process()« in »Plugger/Account.pm« aufruft und ihr die aufgesplitteten Teile der Konfigurationszeile als Argumente überreicht. Außerdem ruft »Plugger.pm« die Funktion »start()« aus Listing 3 auf, bevor die Interpretation der Konfigurationsdatei beginnt, und zum Schluss die »finish()«-Funktion.
Das Account-Plugin nutzt dies, um vor Beginn des Parse-Reigens den in der globalen Variablen »account_total« gespeicherten Gesamtwert aller definierten Konten auf null zu setzen. Wäre noch zu klären, wo man einen derartigen Zähler definiert, auf den womöglich nicht nur »Account.pm«, sondern auch andere Plugins zugreifen. Hierfür legt das Modul »Plugger.pm« den Hash »%MEM« an. Das Modul wird jedem, der mit dem Accessor »mem()« danach fragt, eine Referenz darauf zuspielen. So kann ein Plugin wie »Account.pm« mit
$ctx->mem()->{account_total} = 0;
eine Variable setzen, auf die andere Plugins ebenso zugreifen dürfen, die ebenfalls dank » eine Referenz auf den Plugin-Verwalter »Plugger« besitzen. Auf diese Weise kommunizieren die beiden Plugins »Account.pm« und »Position.pm« miteinander: »Account.pm« setzt »account_total« am Anfang auf null. »Position.pm«, das mit jeder »stock«- oder »cash«-Definition drankommt, rechnet deren Wert aus und addiert ihn zu »account_total« hinzu.
|
Listing 3: |
|---|
001 ###########################################
002 package Plugger::Account;
003 # 2004, Mike Schilli <m@perlmeister.com>
004 ###########################################
005
006 use strict;
007 use warnings;
008 use Term::ANSIColor qw(:constants);
009
010 ###########################################
011 sub init {
012 ###########################################
013 my($class, $ctx) = @_;
014
015 $ctx->register_cmd("account",
016 &start, &process, &finish);
017 }
018
019 ###########################################
020 sub start {
021 ###########################################
022 my($ctx) = @_;
023
024 $ctx->mem()->{account_total} = 0;
025 }
026
027 ###########################################
028 sub account_start {
029 ###########################################
030 my($ctx, $name) = @_;
031
032 print BOLD, BLUE,
033 "Account: $namen",
034 RESET;
035
036 $ctx->mem()->{account_subtotal} = 0;
037 $ctx->mem()->{account_current} = $name;
038 }
039
040 ###########################################
041 sub account_end {
042 ###########################################
043 my($ctx, $name) = @_;
044
045 print BOLD, BLUE;
046 printf "%-47s %9.2fnn", "Subtotal:",
047 $ctx->mem()->{account_subtotal};
048 print RESET;
049 }
050
051 ###########################################
052 sub account_end_all {
053 ###########################################
054 my($ctx) = @_;
055
056 print BOLD, BLUE;
057 printf "%-47s %9.2fnn", "Total:",
058 $ctx->mem()->{account_total};
059 print RESET;
060 }
061
062 ###########################################
063 sub process {
064 ###########################################
065 my($ctx, @args) = @_;
066
067 my $c = $ctx->mem()->{account_current};
068 account_end($ctx, $c) if $c;
069 account_start($ctx, $args[1]);
070 }
071
072 ###########################################
073 sub finish {
074 ###########################################
075 my($ctx) = @_;
076
077 my $c = $ctx->mem()->{account_current};
078 account_end($ctx, $c) if $c;
079 account_end_all($ctx);
080 }
081
082 ###########################################
083 sub position {
084 ###########################################
085 my($type, $ticker, $n, $at, $price,
086 $value, $gain) = @_;
087
088 unless(defined $ticker) {
089 printf "%-47s %9.2fn",
090 $type, $value;
091 return;
092 }
093
094 my $clr = $gain > 0 ? GREEN : RED;
095
096 printf "%-8s %-10s %9.3f %9.3f %7.2f" .
097 " %9.2f %s(%+9.2f)%sn",
098 $type, $ticker, $n, $at,
099 $price,
100 $value, $clr, $gain, RESET;
101 }
102 1;
|
Farblich verschönert
Damit »Account.pm« die Kopfzeilen eines Kontos und dessen Stand schön fett und in Blau anzeigt, nutzt es das CPAN-Modul Term::ANSIColor. Dessen »:constants«-Tag am Ende der »use«-Anweisung exportiert Textattribut-Konstanten wie »BLUE« (blauer Text), »BOLD« (Fettdruck) und »RESET« (zurück zur Normalschrift) in den Namensraum des aufrufenden Skripts. Ab jetzt geben »print«-Anweisungen wie
print BLUE, BOLD,
"In Blau und fett!", RESET;
Ansi-Sonderzeichen aus, die den angegebenen Text im aktuellen Terminal fett und blau markieren, bevor »RESET« für die nächste »print«-Anweisung wieder in den Normalmodus zurückschaltet. Aktuelle (genauer: 20 Minuten verzögerte) Börsenkurse holt »Position« in Listing 4 einfach mit Finance::YahooQuote von Yahoos Finanzseite.
|
Listing 4: |
|---|
01 ###########################################
02 package Plugger::Position;
03 # 2004, Mike Schilli <m@perlmeister.com>
04 ###########################################
05 use strict; use warnings;
06 use Log::Log4perl qw(:easy);
07 use Finance::YahooQuote;
08 use Term::ANSIColor;
09
10 ###########################################
11 sub init {
12 ###########################################
13 my($class, $ctx) = @_;
14
15 DEBUG "Registering @_";
16
17 $ctx->register_cmd("stock",
18 undef, &process, undef);
19 $ctx->register_cmd("cash",
20 undef, &process_cash, undef);
21 }
22
23 ###########################################
24 sub process {
25 ###########################################
26 my($ctx, $cmd, @args) = @_;
27
28 my $value = price($args[0]) * $args[1];
29 my $gain = $value -
30 $args[2] * $args[1];
31
32 Plugger::Account::position(
33 ucfirst($cmd),
34 @args[0..2], price($args[0]),
35 $value, $gain);
36
37 my $mem = $ctx->mem();
38 $mem->{account_subtotal} += $value;
39 $mem->{account_total} += $value;
40 }
41
42 ###########################################
43 sub process_cash {
44 ###########################################
45 my($ctx, $cmd, @args) = @_;
46
47 my $mem = $ctx->mem();
48 $mem->{account_subtotal} += $args[0];
49 $mem->{account_total} += $args[0];
50
51 Plugger::Account::position(
52 ucfirst($cmd),
53 (undef) x 4, $args[0], undef);
54 }
55
56 use Cache::FileCache;
57
58 my $cache = Cache::FileCache->new(
59 { namespace => 'Dagobert',
60 default_expires_in => 600,
61 });
62
63 ###########################################
64 sub price {
65 ###########################################
66 my($stock) = @_;
67
68 DEBUG "Fetching quote for $stock";
69
70 my $cached = $cache->get($stock);
71
72 if(defined $cached) {
73 DEBUG "Using cached value: $cached";
74 return $cached;
75 }
76
77 my @quote = getonequote $stock;
78 die "Cannot get quote for $stock"
79 unless @quote;
80 $cache->set($stock, $quote[2]);
81
82 return $quote[2];
83 }
84
85 1;
|

Abbildung 3: Der Plugin-Verwalter »Plugger.pm« liest mit Module::Pluggable alle Plugins unter »Plugger::« ein und initialisiert sie mit »init()«. Die Plugins registrieren daraufhin Kommandos mit »register_cmd()«.
Onlinekurse mit Zwischenspeicher
Die exportierte Funktion »getonequote()« erlaubt Tickersymbole wie »BMWG.DE« für die deutsche Notierung der BMW-Aktie oder »EBAY« für die Ebay-Notierung an der amerikanischen Nasdaq. Eine ausführliche Liste deutscher Tickersymbole findet sich auf[3].
Da »dagobert« aber unter Umständen denselben Kurs mehrmals braucht, puffert »Position« die Kurse einfach in einem zehn Minuten lang bestehenden Cache. Das Modul Cache::Cache vom CPAN besitzt die denkbar einfachen Schnittstellen »set()«, um einen Cache-Eintrag zu setzen, und »get()«, um ihn wieder zu holen. An Implementierungen bietet es unter anderem einen In-Memory-Cache namens »Cache::MemoryCache« und mit »Cache::FileCache« einen dateigestützten persistenten Cache. »Position.pm« legt mit
my $cache = Cache::FileCache->new(
{ namespace => 'Dagobert',
default_expires_in => 600,
});
ein Cache-Objekt an und kümmert sich um alle Details, beispielsweise das effiziente Speichern in temporären Dateien, ohne mit anderen Applikationen zu kollidieren. Der Anwender ruft nur »-<$>> set()« und »-<$>>get()« auf.
Reich gegen arm
Für eine einfache Kontoanzeige ist »dagobert« vollkommen over-engineered – offensichtlich das Werk eines Architekturastronauten, wie Joel Spolsky es in [4] treffend formuliert. Das Plugger-Framework läuft erst zu voller Spielstärke auf, wenn es benutzerspezifische Funktionalität einhängen muss, ohne den ursprünglichen Code zu verändern.
Das Plugin »Plugger/TaxedPosition.pm« in Listing 5 zeigt ein Beispiel: Es verbucht bei mit »txstock« definierten Aktien (eventuelle) Gewinne nicht 1:1, sondern zieht vorher 50 Prozent für den Finanzminister ab. In diesem “Reicht\’s für \’ne Insel?”-Modus kann der Spekulatius ausrechnen, wie viel Geld er beim Realisieren seiner Aktiengewinne und einem Steuersatz von 50 Prozent übrig behielte. Bei Aktienverlusten zieht »TaxedPosition« nichts ab, sondern zeigt den Wert der Depotposition an, der nach dem Verkauf der Rohrkrepiererpapiere bliebe.
Je nach Situation kann nun der Anwender eigene Plugins für weitere Schlüsselwörter schreiben, ins Framework einhängen und das System anpassen. Dass »TaxedPosition.pm« auf die in »Position.pm« definierte Funktion »price()« zurückgreift, legt es nahe, eine Art Vererbung oder Interfacemechanismus zwischen »TaxedPosition.pm« und »Position.pm« einzusetzen. Da im Plugger-Framework jedoch keine Klassen im Spiel sind, definiert »TaxedPosition.pm« in Listing 5, Zeile 9, einfach einen »AUTOLOAD«-Handler, der Aufrufe von unbekannten Funktionen nach »Position.pm« kanalisiert.
So managed das Plugin »Position.pm« der besseren Übersichtlich- und Wartbarkeit wegen alle Bildschirmausgaben. Die Funktion »position()« nimmt die Daten einer Position mit Typ, Tickersymbol, Anzahl, Kaufkurs, aktuellem Kurs, aktuellem Gesamtwert sowie Gewinn und Verlust entgegen und gibt sie schön formatiert aus. Für Bargeldposten entfällt alles außer der linken und der rechten Spalte.
Eigene Plugins sollten wie schon »TaxedPosition.pm« auf die »print«-Funktionen von »Position.pm« zurückgreifen. Auch die Funktion »price()« aus »Position.pm« dürfte für das eine oder andere Plugin von Nutzen sein.
|
Listing 5: |
|---|
01 ###########################################
02 package Plugger::TaxedPosition;
03 # 2004, Mike Schilli <m@perlmeister.com>
04 ###########################################
05 use strict; use warnings;
06 use Log::Log4perl qw(:easy);
07
08 ###########################################
09 sub AUTOLOAD {
10 ###########################################
11 no strict qw(vars refs);
12
13 (my $func = $AUTOLOAD) =~
14 s/.*::/Plugger::Position::/;
15 $func->(@_);
16 }
17
18 ###########################################
19 sub init {
20 ###########################################
21 my($class, $ctx) = @_;
22
23 $ctx->register_cmd("txstock",
24 undef, &process, undef);
25 }
26
27 ###########################################
28 sub process {
29 ###########################################
30 my($ctx, $cmd, @args) = @_;
31
32 my $value = price($args[0]) * $args[1];
33 my $gain = $value -
34 $args[2] * $args[1];
35
36 my $tax = $gain / 2;
37
38 $value -= $tax if $gain > 0;
39 $gain -= $tax if $gain > 0;
40
41 Plugger::Account::position(
42 ucfirst($cmd),
43 @args[0..2], price($args[0]),
44 $value, $gain);
45
46 my $mem = $ctx->mem();
47 $mem->{account_subtotal} += $value;
48 $mem->{account_total} += $value;
49 }
50
51 1;
|
Installation
Sowohl das Skript »dagobert« (Listing 1) als auch der kompilierte C-Wrapper »dago« gehören ins Verzeichnis »/usr /bin« und müssen ausführbar sein. Das Modul »Plugger.pm« (Listing 2) und alle Plugins unter »Plugger/« sollten in einen der »@INC«-Pfade der Perl-Installation. Alternativ kann die Zeile
use lib '/home/mschilli/perl-modules';
im Perl-Skript »dagobert« den Pfad bekannt machen, falls Plugger & Co. unter dem angegebenen Verzeichnis installiert sind. Die Module Module::Pluggable, Finance::YahooQuote und Term::ANSIColor liefert das CPAN. Sie lassen sich am einfachsten mit einer CPAN-Shell installieren. Für einen Tag heißt es: Die Perl-Welt regiert dein Geld! (jk)
|
Infos |
|---|
|
[1] Listings: [ftp://www.linux-magazin.de/pub/listings/magazin/2005/03/Perl] [2] Module::Pluggable-Tutorial: [http://www.perladvent.org/2004/6th] [3] Kürzel bekannter deutscher Aktien: [http://de.biz.yahoo.com/p/de/cpi/index.html] [4] Joel Spolsky, “Don\’t let Architecture Astronauts scare you” in “Joel on Software”: Apress 2004, S. 111 |
|
Der |
|---|
|
|






