Auch in den letzten beiden Monaten dieses Jahres waren wir nicht ganz untätig – teilweise mit Hilfe anderer Perl-Programmierer*innen.
In den folgenden Abschnitten stelle ich unsere neuen bzw. aktualisierten CPAN-Pakete vor:
Ein neues Modul, das ein statisches Parsen von *cpanfile*s ermöglicht. Ich habe schon in einem Blogpost erwähnt, warum wir nicht Module::CPANfile
nutzen. Da ich noch mehre dieser Anwendungsfälle habe, habe ich ein Modul daraus gebaut: CPANfile::Parse::PPI
.
use v5.24;
use CPANfile::Parse::PPI;
my $path = '/path/to/cpanfile';
my $cpanfile = CPANfile::Parse::PPI->new( $path );
# or
# my $cpanfile = CPANfile::Parse::PPI->new( \$content );
for my $module ( $cpanfile->modules->@* ) {
my $stage = "";
$stage = "on $module->{stage}" if $module->{stage};
say sprintf "%s is %s", $module->{name}, $module->{type};
}
In dieser Distribution habe ich zwei neue Regeln hinzugefügt:
Zum einen eine Regel, mit der die Nutzung von *List::Util*s first
gefordert wird, wenn im Code ein grep
genutzt aber nur das erste Element weiterverwendet wird, wie in diesem Beispiel:
my ($first_even) = grep{
$_ % 2 == 0
} @array;
Die zweite Regel fordert die Nutzung des postderef-Features. Das heißt, dass dieser Code
my @array = @{ $arrayref };
umgeschrieben werden soll als
my @array = $arrayref->@*;
Außerdem gab es kleine Änderungen an den Metadaten. Danke an Gabor Szabo für den Pull Request.
Hier haben wir keine Arbeit aufwenden müssen, sondern durften auf die Fähigkeiten in der Perl-Community zurückgreifen. Andrew Fresh hat einen Pull Request eingereicht, der die Tests die dieses Modul für das generierte Plugin erstellt, lauffähig macht. Vielen Dank dafür!
Auch hier haben wir Pull Requests aus der Perl-Community erhalten:
Vor kurzem kam die erste Anforderung, eines unserer Module für OTOBO zu portieren. Da wir die Spezifikationsdateien unserer Erweiterungen generieren lassen, musste das Kommando entsprechend angepasst werden.
In der Metadaten-Datei (z.B. die für DashboardMyTickets) muss nur noch als Produkt OTOBO
angegeben werden, dann wird die .sopm-Datei richtig generiert.
Das ist das zweite Modul, das für die Nutzung mit OTOBO ertüchtigt wurde. Mit der neuesten Version können Addons für OTOBO geparst werden. Das wurde notwendig, um einen OPAR-Klon für OTOBO aufsetzen zu können.
Zusätzlich wurde die Synopsis durch Håkon Hægland verbessert. Danke für den entsprechenden Pull Request.
Permalink: /2020-12-29-cpan-update-november-dezember
Gregor hat vor längerer Zeit das Modul Test::Perl::Critic::Progressive
und ich vor kurzem das Modul PPI
vorgestellt. In diesem Blogpost zeige ich, wie man mit PPI
Perl::Critic-Regeln umsetzen kann, die dann in den Tests verwendet werden.
Nachdem ich eine Anwendung programmiert hatte, hat Gregor den Code angeschaut. Dabei ist er über dieses Stück Code gestolpert und meinte, es sähe schräg
aus:
my ($doc) = grep{
$_->{virtual_path} eq $virt_path
}@{ $app_config->{docs} || [] };
Für mich ist das völlig normal, ich nutze das häufiger. Aber ich gebe zu, dass man das schöner schreiben könnte.
Da man in diesem Beispiel nur an dem ersten Treffer
interessiert ist, könnte man statt des grep
die Funktion first
aus dem Modul List::Util
nutzen. Damit würde es dann schon so aussehen:
my $doc = first {
$_->{virtual_path} eq $virt_path
}@{ $app_config->{docs} || [] };
Natürlich muss man am Anfang noch das Modul laden. Der Unterschied ist nur klein, aber für Einsteiger vielleicht doch einfacher, weil man hier nicht auf den Kontext achten muss. Beim grep
gibt es unterschiedliche Ergebnisse, je nachdem ob man das Ergebnis in Skalar- oder im Listenkontext nutzt – einfach mal dieses Skript bei perlbanjo.com ausführen...
Das nächste was man schöner schreiben könnte, ist die Dereferenzierung der Arrayreferenz:
@{ $app_config->{docs} || [] }
Seit der Version 5.20 gibt es das (bis 5.24 experimentelle) Feature postderef . Da sieht die Schreibweise dann etwas schöner aus:
$app_config->{docs}->@*
Auch hier muss man am Anfang des Codes noch etwas einfügen, aber der Lesefluss ist in meinen Augen deutlich besser. Zum Schluss noch ein Leerzeichen zwischen das schließende
des }
grep
s und der Dereferenzierung. Insgesamt wäre es dann
my $doc = first {
$_->{virtual_path} eq $virt_path
} $app_config->{docs}->@*;
Nachdem wir geklärt haben, welchen Code wir beanstanden wollen, und wie er schlussendlich aussehen soll, können wir uns an die Umsetzung der Regeln machen.
In meinen Augen liefert PPI::Dumper
einen sehr guten Einstiegspunkt. Einfach den unerwünschten
Code in allen möglichen Fassungen in den __DATA__
-Bereich des Skriptes packen und ausführen. Damit sieht man, welche PPI-Knoten es gibt und an welchen man ansetzen muss. Der Dump des Perl Document Object Model (POM) in unserem Beispiel sieht auszugsweise folgendermaßen aus:
PPI::Document
PPI::Token::Whitespace '\n'
PPI::Statement::Variable
PPI::Token::Word 'my'
PPI::Token::Whitespace ' '
PPI::Token::Symbol '@list'
PPI::Token::Whitespace ' '
PPI::Token::Operator '='
PPI::Token::Whitespace ' '
PPI::Token::Word 'grep'
PPI::Token::Whitespace ' '
PPI::Structure::Block { ... }
Bei jeder Perl::Critic-Regel muss man angeben, auf welchen Knoten die Regel angewandt werden soll. In diesem Fall ist das immer ein Knoten vom Typ PPI::Token::Word.
Gehen wir einen Teil nach dem anderen durch. Zuerst wollen wir first
statt grep
nutzen, wenn auf der linken Seite nur eine einelementige Liste angegeben ist.
Die passenden Beispiele dazu sind:
my @list = grep { ausdruck() } @liste; # grep ok
my $list = grep { ausdruck() } @liste; # grep ok
my ($list) = grep { ausdruck() } @liste; # grep not ok
my ($list, $second) = grep { ausdruck() } @liste; # grep ok
if( grep{ ausdruck() }@liste ) { # grep ok
}
# ggf. noch weitere Beispiele
Wenn wir uns den Dump anschauen, merken wir folgendes:
Vergleiche Varianten der Variablenliste:
PPI::Token::Whitespace '\n'
PPI::Statement::Variable
PPI::Token::Word 'my'
PPI::Token::Whitespace ' '
PPI::Structure::List ( ... )
PPI::Statement::Expression
PPI::Token::Symbol '$list'
# vs
PPI::Statement::Variable
PPI::Token::Word 'my'
PPI::Token::Whitespace ' '
PPI::Structure::List ( ... )
PPI::Statement::Expression
PPI::Token::Symbol '$list'
PPI::Token::Operator ','
PPI::Token::Whitespace ' '
PPI::Token::Symbol '$second'
Es muss also eine Liste mit genau einem PPI::Token::Symbol sein, dessen Sigil ein $
ist.
Schauen wir uns das Grundgerüst für eine Perl::Critic-Regel an:
package Perl::Critic::Policy::PerlAcademy::ProhibitGrepToGetFirstFoundElement;
# ABSTRACT: Use List::Utils 'first' instead of grep if you want to get the first found element
use 5.006001;
use strict;
use warnings;
use Readonly;
use Perl::Critic::Utils qw{ :severities };
use base 'Perl::Critic::Policy';
our $VERSION = '2.02';
#-----------------------------------------------------------------------------
Readonly::Scalar my $DESC => q{Use List::Utils 'first' instead of grep if you want to get the first found element};
Readonly::Scalar my $EXPL => [ ];
#-----------------------------------------------------------------------------
sub default_severity { return $SEVERITY_MEDIUM }
sub default_themes { return qw<perl_academy> }
sub applies_to {
return qw<
PPI::Token::Word
>;
}
Das kann so 1:1 kopiert werden. Der Paketnamen muss angepasst werden sowie die Beschreibungen.
Bei der default_severity
muss man sich überlegen, wie wichtig einem diese Regel ist. Regeln können in sogenannte Themes eingeteilt werden.
Mit applies_to
legt man eine Liste von Klassennamen fest, auf deren Objekte die Regel angewandt wird. Wir haben oben festgestellt, dass wir auf PPI::Token::Word-Objekte reagieren müssen.
Ob die Regel eingehalten wird, wird dann in der Methode violates geprüft. Ist alles ok, wird undef zurückgegeben, ansonsten ein Objekt, das die Regelmissachtung beschreibt:
sub violates {
my ( $self, $elem, $doc ) = @_;
# ... code zur pruefung der regel ...
return $self->violation( $DESC, $EXPL, $elem );
}
Neben dem Objekt der Regel selbst bekommt man das Element (hier ein Objekt von PPI::Token::Word) und das PPI::Document-Objekt übergeben.
Als erstes prüfen wir, ob es ein grep-Befehl ist:
# other statements than grep aren't catched
return if $elem->content ne 'grep';
Dann interessieren wir uns nur für das grep, wenn das Ergebnis nicht in einer Variablen gespeichert wird. Dazu müssen wir in der Hierarchie des POM eine Stufe nach oben gehen:
my $parent = $elem->parent;
# grep in boolean or void context isn't checked
return if !$parent->isa('PPI::Statement::Variable');
Und bei den Variablen muss eine Liste gegeben sein
my $list = first{ $_->isa('PPI::Structure::List') } $parent->schildren;
return if !$list;
Die Liste darf nur ein Element haben und das muss ein Scalar sein.
my $symbols = $list->find('PPI::Token::Symbol');
return if !$symbols;
return if 1 != @{ $symbols };
return if '$' ne substr $symbols->[0], 0, 1;
Sollte das alles zutreffen, ist das ein Regelverstoß:
return $self->violation( $DESC, $EXPL, $elem );
Diese gesamte Regel ist mittlerweile auch auf CPAN bei meinen Regeln – und zwar als Perl::Critic::Policy::Reneeb::ProhibitGrepToGetFirstFoundElement – zu finden.
Die beiden anderen Punkte zum schöneren
Code umzusetzen bleibt dem geneigten Leser überlassen.
Permalink: /2020-12-01-perl-critic-regeln-erstellen
Only perl can parse Perl
Dieses Zitat von Tom Christiansen gilt in Teilen immer noch. Perl (die Sprache) ist eigentlich alles das, was perl
(der Interpreter) sauber ausführen kann. Dass das nicht nur der Code mit der Syntax ist, die man so hinlänglich sieht, kann man an etlichen Acme::
-Modulen sehen. So kann man wirklich sauberen Code mit Acme::Bleach
schreiben. Das Modul wandelt normalen
Perl-Code in Whitespaces um – und das Programm läuft immer noch. Ein weiteres Beispiel: Zu meinen Ausbildungszeiten in der Bioinformatik habe ich (nicht produktiven) Code mit Acme::DoubleHelix
in eine Doppelhelix-Darstellung umgewandelt.
Um diese Darstellung von Code geht es aber nicht, wenn wir etwas mit PPI
machen. Das Modul dient der statischen Analyse von Perl-Code. Zum Beispiel als Backend
für Perl::Critic
und das von Gregor letztens vorgestellte Test::Perl::Critic::Progressive
. Da PPI
eine statische Analyse durchführt, wird der Code nicht ausgeführt. Deshalb können manche Konstrukte aber nicht tiefergehend betrachtet werden:
require $module;
Welches Modul hier geladen wird, kann PPI
nicht feststellen. Für viele Anwendungsbereiche ist das aber auch irrelevant. Es ermöglicht aber viele Anwendungen, ohne dass man Perl-Code ausführen muss oder selbst mit mehr oder weniger guten regulären Ausdrücken im Code etwas suchen muss.
Aber nicht nur wegen Perl::Critic ist PPI unser Modul des Monats
. Es vereinfacht auch unsere Arbeit bei einigen Anwendungen.
Zum einen bei der Bereitstellung der Perl-API-Dokumentation der ((OTRS)) Community Edition unter https://otrs.perl-services.de aber auch bei der Anwendung zur Messung der Modul-Verbreitung unter https://usage.perl-academy.de .
Bei der OTRS-Dokumentation schauen wir im Code nach, ob es überhaupt POD in der Datei gibt und schmeißt den Rest quasi weg. Damit wird es einfacher, Teile der Dokumentation auseinanderzunehmen, ohne dass der restliche Code in die Quere kommt.
So sieht der entsprechende Code aus:
my @perl_files = $archive->membersMatching('\.(?:pm|pod)$');
FILE:
for my $file ( @perl_files ) {
my $path = $file->fileName;
next FILE if $path =~ m{cpan-lib};
next FILE if $path !~ m{Kernel};
my $code = $file->contents;
my $doc = PPI::Document->new( \$code );
my $pod = $doc->find( 'PPI::Token::Pod' );
next FILE if !$pod;
my $pod_string = join "\n\n", map{ $_->content }@{ $pod };
my $package = $doc->find_first('PPI::Statement::Package');
$pod_string =~ m{
^=head\d+ \s+ DESCRIPTION \s+
(?<description>.*?) \s+
^=(?:head|cut)
}xms;
my $description = $+{description} || '';
}
In $archive
steckt ein Objekt von Archive::Zip
und über das holen wir alle Perl-Module und POD-Dateien, die in der Zip-Datei stecken. Wir holen immer die Zip-Datei, damit wir nicht zig Git-Klone bei uns rumfliegen haben und wir die vielleicht gar nicht mehr brauchen.
Von jeder dieser Dateien holen wir mit ->contents
den Inhalt der Datei. Mit PPI::Document->new( \\$code )
erzeugen wir dann das Objekt von PPI::Document
und durch die Skalarreferenz weiß PPI, dass nicht eine Datei geparst werden soll, sondern der Inhalt der Skalarvariablen. Soll eine Datei geparst werden, nimmt man einfach PPI::Document->new( $path_to_file )
.
Anschließend suchen wir alle POD-Teile in der Datei (find
), und wenn gar kein POD enthalten ist, brauchen wir gar nicht weiterzumachen. Der Methode find
muss man den Paketnamen übergeben, nach dessen Objekten man sucht. Welche Klassen es gibt ist, wird in der PPI-Dokumentation gezeigt.
Da wir auch noch den Paketnamen brauchen, der im Perl-Code zu finden ist, suchen wir das erste Element der Klasse PPI::Statement::Package
.
Somit haben wir dann alles, um das HTML aus POD zu generieren.
Die zweite Anwendung nutzt PPI,
um cpanfiles
zu parsen. Warum wir nicht das Modul Module::CPANfile
nutzen? Wenn man sich das anschaut, lädt es den Perl-Code mit eval
. Und da wir keine Kontrolle darüber haben, was die Besucher in die Dateien schreiben, die hochgeladen werden, nehmen wir lieber einen Parser zur statischen Analyse.
Schauen wir uns also mal an, was PPI
aus einem cpanfile macht. Das cpanfile hat diesen Inhalt:
on 'test' => sub {
requires 'Perl::Critic::RENEEB' => '2.01';
requires "Proc::Background" => 0;
requires 'Test::BDD::Cucumber';
};
on 'runtime' => sub {
requires 'Class::Unload';
};
on 'develop' => sub {
requires 'CryptX' => '0.64';
requires 'DBIx::Class::DeploymentHandler';
requires q~Dist::Zilla~;
requires MySQL::Workbench::DBIC => '1.13';
};
Um uns anzuschauen, wie das Perl Document Object Tree dazu aussieht, nutzen wir das folgende Skript (Perl::Critic
hat ein etwas mächtigeres Skript im Repository, das aber leider nicht installiert wird):
#!/usr/bin/perl
use strict;
use warnings;
use PPI;
use PPI::Dumper;
use Mojo::File qw(curfile);
curfile->sibling('examples')->list->first( sub {
my $code = $_->slurp;
my $doc = PPI::Document->new( \$code );
PPI::Dumper->new( $doc )->print and return;
});
Das erzeugt dann folgende Ausgabe:
PPI::Document
PPI::Statement
PPI::Token::Word 'on'
PPI::Token::Whitespace ' '
PPI::Token::Quote::Single ''test''
PPI::Token::Whitespace ' '
PPI::Token::Operator '=>'
PPI::Token::Whitespace ' '
PPI::Token::Word 'sub'
PPI::Token::Whitespace ' '
PPI::Structure::Block { ... }
PPI::Token::Whitespace '\n'
PPI::Token::Whitespace ' '
PPI::Statement
PPI::Token::Word 'requires'
PPI::Token::Whitespace ' '
PPI::Token::Quote::Single ''Perl::Critic::RENEEB''
PPI::Token::Whitespace ' '
PPI::Token::Operator '=>'
PPI::Token::Whitespace ' '
PPI::Token::Quote::Single ''2.01''
PPI::Token::Structure ';'
PPI::Token::Whitespace '\n'
PPI::Token::Whitespace ' '
PPI::Statement
PPI::Token::Word 'requires'
PPI::Token::Whitespace ' '
PPI::Token::Quote::Double '"Proc::Background"'
PPI::Token::Whitespace ' '
PPI::Token::Operator '=>'
PPI::Token::Whitespace ' '
PPI::Token::Number '0'
PPI::Token::Structure ';'
PPI::Token::Whitespace '\n'
[...]
Wir wollen einfach die Modulnamen daraus haben, also der erste String nach dem Literal requires. An dem Dump kann man gut sehen, dass das Literal immer ein Objekt der Klasse PPI::Token::Word
ist. Aber nicht jedes Objekt der Klasse enthält das Literal requires. Wir sehen zum Beispiel auch sub. Das müssen wir berücksichtigen. Nach dem Literal kommen beliebige Whitespaces, die uns aber nicht interessieren. PPI kennt da das Prinzip der significant siblings/children
. Da in dem Baum die Elemente von requires und der Modulname auf einer Ebene sind, interessieren wir uns für das nächste signifikate Geschwister.
So sieht der Code dann in der Anwendung aus:
my $code = $file->slurp;
my $doc = PPI::Document->new( \$code );
my $requires = $doc->find(
sub { $_[1]->isa('PPI::Token::Word') and $_[1]->content eq 'requires' }
);
if ( !$requires ) {
$file->remove;
return;
}
REQUIRED:
for my $required ( @{ $requires || [] } ) {
my $value = $required->snext_sibling;
my $can_string = $value->can('string') ? 1 : 0;
my $prereq = $can_string ?
$value->string :
$value->content;
save_prereq( $prereq );
}
Der Anfang ist ähnlich zu oben. Wir lesen die Datei ein und erzeugen ein Objekt von PPI::Document
. Während wir oben der Methode find
nur die Klasse angegeben haben, deren Objekte wir alle haben wollen, möchten wir hier die Suche etwas einschränken. Nämlich alle PPI::Token::Word
-Objekte, deren Inhalt required
ist. Übergibt man eine anonyme Subroutine an find
, wird für jedes Element unterhalb des zu durchsuchenden Knoten diese Subroutine aufgerufen.
Als Parameter werden zwei Parameter übergeben. Zum einen das Element auf dem die Suche ausgeführt wird und zum anderen das Element, das geprüft wird.
Die Methode content
liefert den Code 1:1.
Für alle gefundenen requires bekommen wir die Objekte der Klasse PPI::Token::Word
. Für jedes dieser Objekte wollen wir das erste signifikante Geschwister. Mit next_sibling
würde man die Whitespaces bekommen, die interessieren uns aber nicht. Aus diesem Grund müssen wir snext_sibling
verwenden.
Schauen wir noch mal kurz auf die verschiedenen Varianten der Modulangaben:
requires 'Perl::Critic::RENEEB' => '2.01';
requires "Proc::Background" => 0;
requires q~Dist::Zilla~;
requires MySQL::Workbench::DBIC => '1.13';
... und die dazugehörigen PPI-Elemente:
PPI::Token::Quote::Single ''Perl::Critic::RENEEB''
PPI::Token::Quote::Double '"Proc::Background"'
PPI::Token::Quote::Literal 'q~Dist::Zilla~'
PPI::Token::Word 'MySQL::Workbench::DBIC'
Da die Whitespace
-Knoten übergangen werden bekommen wir einen der hier gezeigten Knoten. Mit der Methode content
bekommen wir den Wert wie er hier zu sehen ist, also inklusive der Anführungszeichen bzw. des q\~\~
.
Wir müssen uns also auf die unterschiedlichen Methoden stützen, die die Objekte mitliefern. Bei den ganzen Quote::\*
-Klassen kann die Methode string
genutzt werden, bei PPI::Token::Word
die Methode content
.
Noch kurz zu den s...
-Methoden: Als significant werden alle Teile bezeichnet, die für die Ausführung des Codes wichtig sind. Kommentare, POD-Dokumenation, __END__
-Bereiche und Whitespaces gehören nicht dazu.
Die Skripte und Beispiele habe ich auch wieder ins Git-Repository gepackt.
Permalink: /2020-10-21-ppi
Mich selbst interessiert auch die Frage, welches Modul vom CPAN wird eigentlich besonders häufig genutzt? Da CPAN keine Daten dazu speichert, wie oft ein Modul heruntergeladen wird ist das nicht so einfach zu beantworten. Ein Tweet hat mich dazu gebracht eine kleine Anwendung zu schreiben, bei der jeder seine/ihre cpanfiles hochladen kann: https://usage.perl-academy.de.
Momentan werden dabei nur die requires ausgewertet.
Ja, so lange es nicht besonders weit verbreitet ist, ist die Datenlage sehr dünn und natürlich kann man das leicht manipulieren in dem man die eigenen cpanfiles mehrfach hochlädt. Um eine Idee von häufig genutzten Modulen zu bekommen, ist die Anwendung ganz nützlich.
Ich plane mit der Zeit noch einige statistische Auswertungen hinzuzufügen. Ich werde dann hier im Blog davon berichten. Bis dahin hoffe ich, dass noch einige cpanfiles hochgeladen werden. Jede/r kann mithelfen!
Mit was haben wir die Anwendung umgesetzt? Natürlich mit Mojolicious. Weiterhin kommen noch Minion als JobQueue und PPI zum Parsen der cpanfiles zum Einsatz. Diese beiden Module werden wir hier im Blog noch genauer vorstellen.
Permalink: /2020-08-17-module-usage