Wie führe ich externe Programme aus oder fange ihre Ausgabe ab?
Inhalt:
In kurz
# Ausführen eines Befehls ("ls -l")
system('ls -l') and die $?;
# Besser:
system('ls', '-l') and die $?;
# Abfangen der Ausgabe:
my $output = `ls -l`; # Diese Zeichen sind als "Backticks" bekannt
# Oder (gleichbedeutend):
my $output = qx(ls -l);
# Besser: Siehe unten bei SafeBackticks oder bei IPC::System::Simple
Warum es mehrere Argumente sein sollten (wichtig)
Ein Parameter
Wenn system genau einen Parameter bekommt, wird dieser geparst. Wenn er Shell-Sonderzeichen enthält (Dollar, Ampersand, etc.), wird eine externe Shell aufgerufen (üblicherweise /bin/sh unter Unix, aber etwas anderes auf z. B. Windows; daher sollte man sich in portablen Skripten nie auf die Funktion dieser Shell verlassen).
Wenn der Parameter keine Sonderzeichen enthält, wird er nach Leerzeichen gesplittet, wobei das erste Element (im obigen Beispiel "ls") als Programm ausgeführt wird und die restlichen Parameter (bspw. "-l") als Argumente bekommt. Das hat prinzipiell den gleichen Effekt, wie wenn man "ls -l" in einer Shell ausführt, ist aber effizienter.
Um noch einmal zusammenzufassen: Wenn system mit einem einzigen String als Parameter gestartet wird, wird dieser String auf spezielle Zeichen untersucht, die einen
großen Einfluss darauf haben, welcher Befehl ausgeführt wird. Solche Sachen wie
system("ls $verzeichnis") sollten also
absolut tabu sein, insbesondere, wenn man nicht 100%ig sicher ist, dass $verzeichnis in keinem Fall so etwas wie "; rm *" ist (Achtung vor Benutzereingaben). Um dies zu vermeiden, fangt bitte auf keinen Fall an, $verzeichnis zu escapen. Korrektes Escapen ist extrem mühsam und fehleranfällig. (Escapen bedeutet, dass man ";" durch "\;" ersetzt. Je nach Shell und Plattform müssen jedoch unterschiedliche Zeichen auf unterschiedliche Art und Weise escapet werden.)
Mehrere Parameter
Zu bevorzugen ist
system('ls', $verzeichnis)Durch diese Aufrufvariante wird Escapen einfach nicht gebraucht.
Es gibt nur noch das Problem, dass $verzeichnis mit "-" anfangen könnte. Wie man das vermeidet, ist hier beschrieben:
http://wooledge.org/mywiki/BashPitfalls#head-78c4f21bc34ad48964e11f5dc6ec92f84d622b67
Der Fall, dass der Befehl selbst Leerzeichen enthält (oder gar eine Benutzereingabe ist), und es nicht notwendig Argumente für den Befehl gibt, sollte im normalen Leben nie eintreten, doch auch dafür gibt es eine Lösung:
my @befehl = ('Eigene Dateien/Script.pl');
system {$befehl[0]} @befehl; # Ohne diese seltsame Schreibweise wuerde das Leerzeichen als Befehlstrenner behandelt werden
Unglückseligerweise gibt es in Perl ein Problem: Backticks (bzw. qx bzw. readpipe) kennen keine Mehr-Parameter-Form analog zu system. Es gibt dafür aber ein weniger bekanntes Feature der Funktion "open", welches weitaus mächtiger ist als Backticks, allerdings auch etwas "cumbersome" ist. Daher bietet es sich an, folgende kleine Helferfunktion zu benutzen:
sub safe_backticks
{
my ($cmd, @args) = @_;
open (my $pipe, '-|', $cmd, @args)
or die "Could not run $cmd @args: $!\n";
my $output = do {local $/; <$pipe>};
defined ($output) or die "read: $!";
unless( close($pipe) )
{
my $error = ($? & 0x7f)
? 'Signal ' . ($? & 0x7f)
: 'Exit status ' . ($? >> 8);
die "$error: $cmd @args";
}
return $output;
}
Diese Subroutine lässt sich ungefähr so benutzen wie Backticks auch, nur dass sie den Aufruf mit mehreren Parametern unterstützt und damit vor den oben erwähnten Verwundbarkeiten geschützt ist. So könnte man sie aufrufen:
my $output = safe_backticks('ls', $verzeichnis);
(Der Aufruf im Listenkontext wird noch nicht unterstützt, das kann aber leicht eingebaut werden.)
Der geneigte Leser wird wissen, dass dies nur ein langweiliges Beispiel ist und nicht etwa die empfohlene Variante, an eine Liste von Dateien zu kommen. Siehe dazu
EinUndAusgabeDateinamenImVerzeichnisAuslesen.
Fehlerabfrage
Wenn man eine korrekte Fehlermeldung geben möchte, muss man korrekterweise sowohl $! als auch $? in Betracht ziehen.
$! gibt (wie bei anderen Systemaufrufen auch) an, warum der Aufruf nicht möglich war. Das kann zum Beispiel daran liegen, dass das aufzurufende Programm nicht existierte oder nicht ausführbar war.
$? hingegen gibt Fehler an, die aufgetreten sind, nachdem das Programm selbst schon erfolgreich gestartet wurde. Das kann entweder ein Programmabbruch mit Status ungleich 0 sein, oder Programmabbruch durch ein Signal (also durch kill).
$? auszuwerten ist recht involviert, da es ein Bitarray ist. Ein Beispiel dafür ist oben in
SafeBackticks? zu sehen. Noch komplizierter ist es, eine korrekte Fehlermeldung unter Berücksichtigung von $!
und $? auszugeben, wenn system() fehlschlägt. In "perldoc -f system" steht dazu folgender Code:
if ($? == -1) {
print "failed to execute: $!\n";
}
elsif ($? & 127) {
printf "child died with signal %d, %s coredump\n",
($? & 127), ($? & 128) ? 'with' : 'without';
}
else {
printf "child exited with value %d\n", $? >> 8;
}
Einfacher geht es durch ein Modul:
IPC::System::Simple
Das CPAN-Modul IPC::System::Simple vereinfacht die umständliche Benutzung von system sowie von Backticks.
* Es gibt einfache Fehlermeldungen
* Es erlaubt die Angabe von getrennten Parametern wie bei
SafeBackticks?
Die SYNOPSIS aus der Perldoc sagt das Wichtigste (hier übersetzt ins Deutsche):
use IPC::System::Simple qw(system systemx capture capturex);
system("some_command"); # Befehl ist erfolgreich oder stirbt (wirft Exception)!
system("some_command",@args); # Erfolgreich oder stirbt, benutzt nur die Shell, wenn @args leer ist
systemx("some_command",@args); # Erfolgreich oder stirbt, benutzt NIE die Shell
# Fängt die Ausgabe des Befehls ab (wie Backticks). Stirbt bei Fehlern.
my $output = capture("some_command");
# Wie Backticks im Listenkontext. Stirbt bei Fehlern.
my @output = capture("some_command");
# Ebenso, aber kein Shell-Aufruf, wenn @args nichtleer ist
my $output = capture("some_command", @args);
# Ebenso, ruft NIE die Shell auf
my $output = capturex("some_command", @args);
my @output = capturex("some_command", @args);
Wie man sieht, wird das Builtin "system" von dem Modul überschrieben, wenn man es importiert. Somit funktioniert es ähnlich wie autodie.
Eine ähnliche Funktionalität lässt sich auch mit der Subroutine capture_exec aus dem Modul IO::CaptureOutput erreichen. Hierbei muss noch ein Parameter "\undef" übergeben werden, um zu sagen, dass STDERR unberührt bleiben soll. IO::CaptureOutput benutzt allerdings intern temporäre Dateien, wohingegen "open" und Backticks über Pipes implementiert sind.
Ergänzungen, Kommentare, Autoren