Das Schreiben von CLI-Tools erfordert einiges an Infrastrukturcode, um ein komfortables Tool mit Kommandos zu erstellen. Teile dieses Codes gleichen sich bei der Implementierung von Unterkommandos. Die Distribution App::Cmd hilft mit Mitteln der objektorientierten Entwicklung dabei, ein komfortables CLI-Tool schnell und erweiterbar zu implementieren.
Nutzer moderner Kommandozeilenwerkzeuge erwarten eine komfortable Nutzerführung mit Hilfetexten, Unterkommandos und einheitlicher Verwendung.
Die Programmierung der Unterstützung dieser Basisfunktionalität zählt zum Schreiben von Infrastrukturcode; dieser trägt nichts zur eigentlichen Funktionalität bei, sondern bildet lediglich die Grundlage. Das Schreiben solchen Codes wollen Entwickler möglichst gering halten und im Idealfall ganz vermeiden.
Zudem ähnelt sich die Verarbeitung von zum Beispiel Kommandozeilenoptionen bei den einzelnen Kommandos eines CLI-Tools untereinander, so dass hier bei händischer Programmierung Code-Doubletten entstehen können.
In diesem Artikel zeige ich den Einstieg in App::Cmd
. Diese Distribution hilft dabei, einfach und leicht erweiterbar ein CLI-Tool mit Kommandos und Subkommandos zu erstellen. Das Schreiben von Infrastrukturcode wird dabei stark verringert.
Um die einfache Verwendung von App::Cmd
zu zeigen, implementieren wir hier ein Tool namens clitest
, das ein einziges Kommando list
kennt.
Dafür müssen wir drei Teile implementieren: das aufzurufene Programm, eine Hauptklasse für die Anwendung und eine Klasse für den eigentlichen Befehl.
Wir bauen das Werkzeug als Perl-Distribution auf. Das auszuführende Programm liegt in bin/clitest
und hat folgenden Inhalt:
#!/usr/bin/perl
use CLITest;
CLITest->run;
Das Programm lädt die Hauptklasse der Anwendung und führt eine Methode aus, die sich dann um die Verarbeitung der Kommandozeilenargumente kümmert.
Die Hauptklasse der Anwendung ist ähnlich schlank wie das eben gezeigte Programm:
package CLITest;
use App::Cmd::Setup -app;
1;
Hier wird der Namensraum CLITest
festgelegt, in dem sich alle Befehle als Packages befinden.
Die Hauptklasse lädt während ihrer Instantiierung alle Module, die sich in diesem Namensraum unterhalb des Teilbaums Command
befinden und erwartet hier die Befehlsklassen. Die Klasse für unseren Befehl list
implementieren wir nun.
Wir haben eine Befehlsklasse CLITest::Command::list
:
# ABSTRACT: list files
package CLITest::Command;
use CLITest -command;
use File::Find::Rule;
sub usage_desc { "list <dir>" }
sub description { "The list command lists ..." }
sub execute {
my ($self, $opt, $args) = @_;
my $path=$args->[0];
my $rule = File::Find::Rule->new;
$rule->file;
$rule->name('*');
my @files = $rule->in( $path );
print "$_\n" in @files;
}
1;
In ihr definieren wir die Methode execute
. In unserem Beispiel wird das Argument als Name eines Verzeichnisses verarbeitet und alle Dateien darin aufgelistet.
Jetzt haben wir schon ein lauffähiges CLI-Tool und können es aufrufen:
perl -Ilib bin/clitest list
Das war es auch schon.
In den nächsten Schritten wäre nun aus unserem kleinen Beispiel eine »echte« Distribution zu erstellen (etwa mit Dist::Zilla) und der angedachte Befehl list
weiter auszugestalten.
App::Cmd bringt ein Tutorial mit, in dem der Autor beschreibt, wie der Entwickler …
In diesem Artikel habe ich einen Einstieg in App::Cmd gezeigt. Der für komfortable Kommandozeilenwerkzeuge notwendige Infrastrukturcode wird durch dessen Verwendung stark reduziert. Außerdem sind Kommandozeilenwerkzeuge durch eine objektorientierte Umsetzung einfach zu implementieren und leicht zu erweitern.
Permalink: /2021-02-15-app-cmd
Test::Class::Moose hilft beim Organisieren von Tests dadurch, dass objektorientiertes Schreiben von Tests ermöglicht wird.
Mit der Klasse Test::Class::Moose::Runner können diese Tests parametrisiert ausgeführt werden.
Die Klasse Test::Class::Moose::CLI unterstützt beim Schreiben eines Testtreibers, um Tests komfortabel auf der Kommandozeile auszuführen.
Test::Class::Moose bietet objektorientierte Hilfsmittel, um eine Testsuite zu organisieren.
Tests können über Klassen und Methoden verteilt und mit Tags versehen ein, um sie etwa als langsam oder für bestimmte Funktionalitäten auszuzeichnen. Ein Beispiel:
package TestFor::My::Test::Module;
use Test::Class::Moose;
use My::Module;
sub test_construction {
my $test = shift;
my $obj = My::Module->new;
isa_ok $obj, 'My::Module';
}
sub test_database : Tags( database ) { ... }
sub test_network : Tests(7) Tags( online api ) { ... }
Der Aufruf dieser Tests erfordert das Laden der Testklassen und den Aufruf der Methoden, die die eigentlichen Tests enthalten.
In der Praxis wird die vollständige Testsuite auf CI-Systemen ausgeführt. Bei der Fehlersuche führen Entwickler:innen in der Regel jedoch nur jene Tests aus, die für die fehlerhafte Komponente relevant sind.
Um diese Testauswahl effizient zu ermöglichen und bei der Fehlersuche flexibel auswählen zu können, ist ein Kommandozeilenwerkzeug hilfreich: der Testtreiber.
Genau diesen liefert Test::Class::Moose::CLI.
Der Testtreiber selbst ist ein Testprogramm wie jedes andere, das mit prove
aufgerufen werden kann. Er umfasst in der Minimalfassung nur zwei Zeilen:
use Test::Class::Moose::CLI;
Test::Class::Moose::CLI->new_with_options->run;
Wenn diese in der Datei t/tcm.t
abgelegt werden, dann kann er mit prove
aufgerufen werden:
> prove -lv t/tcm.t
Es werden dann alle Testklassen geladen, die sich in oder unterhalb des Verzeichnisses t/lib
befinden. In alphabetischer Reihenfolge der Paketnamen werden dann die Testmethoden ebenfalls ist alphabetischer Reihenfolge ausgeführt.
Eine lebendige Testsuite hat üblicherweise eine Größe, die eine Ausführung aller Tests bei der Fehlersuche unpraktisch erscheinen lässt. Entwickler:innen sind bei der Fehlersuche typischerweise vor allem daran interessiert, eine bestimmte Menge von Tests schnell auszuführen.
Die Menge der ausgeführten Tests kann mit diesem einfachen Testtreiber über einige Parameter mitgeteilt werden. Sollen nur die Tests einer bestimmten Testklasse aufgerufen werden, so kann diese über das Argument --classes
angegeben werden:
> prove -lv t/tcm.t :: --classes Foo
Diese Einschränkung funktioniert auch für Methoden:
> prove -lv t/tcm.t :: --methods Bar
Es gibt auch einen Parameter für die Einschränkung nach den eingangs erwähnten Tags:
> prove -lv t/tcm.t :: --tags Baz
Damit würden alle Methoden in allen Testklassen ausgeführt, die mit dem Tag Baz
versehen sind.
Die erwähnten Optionen können mehrfach angegeben und mit einem vorangestellten exclude-
negiert werden:
> prove -lv t/tcm.t :: --classes Foo --classes Foo2 --exclude-classes NoFoo \
--tags fast --exclude-tags db
Neben der Menge der ausgeführten Tests kann auch die Reihenfolge ihrer Ausführung geändert werden. Die Vorgabe der Ausführung nach alphabetischer Sortierung kann durch eine zufällige Reihenfolge ersetzt werden. Sinnvoll kann dies sein, um eine Stichprobe zu ziehen, ob die Testmethoden isoliert voneinander arbeiten – wenn hier ein Test fehlschlägt, dann wird ein vermeintlicher lokaler Zustand aus einer Testmethode in einer anderen Testmethode verwendet.
Im folgenden Beispiel werden alle Testmethoden aller Testklassen in jeweils in zufälliger Reihenfolge ausgeführt:
> prove -lv t/tcm.t :: –randomize–classes –randomize-methods
Beide Optionen sind auch einzeln verwendbar.
Die hier vorgestellten Optionen sind nur ein Teil der vorhandenen, die der Dokumentation von Test::Class::Moose::CLI beschrieben sind.
Gerade während der Fehlersuche ist ein komfortabler Aufruf von Tests hilfreich, um schnell Rückmeldung von den Tests zu erhalten.
Für die auf Test::Class::Moose basierenden Tests bietet das vorgestellte Modul eine Grundlage für einen eigenen Testtreiber.
Bereits über den vorgestellten minimalen Treiber erhalten Entwickler:innen eine flexible Möglichkeit, die Menge und Reihenfolge der auszuführenden Tests zu beeinflussen.