Mit der Funktion exec() lässt sich aus PHP heraus ein Befehl auf der Kommandozeile des Servers ausführen. Das ist nützlich, um externe Programme aufzurufen, etwa zum Konvertieren von Bildern, Erstellen von Backups oder Abfragen von Systeminformationen. Weil dabei Systembefehle ausgeführt werden, ist ein sorgfältiger Umgang mit Benutzereingaben besonders wichtig. Dieses Tutorial erklärt die Syntax, zeigt praxisnahe Anwendungsfälle und geht auf die Sicherheitsaspekte im Detail ein.
Syntax und Parameter
Die Funktion erwartet einen Befehl als String und bietet zwei optionale Referenz-Parameter für die Ausgabe und den Exit-Code.
exec(
string $command,
array &$output = null,
int &$result_code = null
): string|false
Der Rückgabewert von exec() ist die letzte Zeile der Befehlsausgabe, nicht der Exit-Code. Die vollständige Ausgabe wird zeilenweise im Array $output gespeichert. Der Exit-Code steht im dritten Parameter $result_code. Ein Wert von 0 bedeutet in der Regel, dass der Befehl erfolgreich war.
Grundlegendes Beispiel
Im folgenden Beispiel wird mit ls der Inhalt des aktuellen Verzeichnisses abgefragt. Die gesamte Ausgabe landet im Array $output.
<?php
$output = [];
$exitCode = 0;
exec('ls -l', $output, $exitCode);
if ($exitCode === 0) {
foreach ($output as $zeile) {
echo $zeile . "<br>";
}
} else {
echo "Fehler, Exit-Code: $exitCode";
}
?>
Jede Zeile der Befehlsausgabe wird als eigenes Element im Array gespeichert. So lässt sich die Ausgabe anschließend bequem mit echo oder print_r() weiterverarbeiten.
Exit-Code richtig auswerten
Viele Entwickler ignorieren den Exit-Code und prüfen nur, ob $output gefüllt ist. Das führt zu stillen Fehlern, denn ein Befehl kann Ausgabe erzeugen und trotzdem fehlschlagen. Die Konvention bei Shell-Befehlen lautet: 0 bedeutet Erfolg, jeder andere Wert ist ein Fehler. Manche Programme verwenden verschiedene Codes für unterschiedliche Fehlerarten.
<?php
$output = [];
$code = 0;
exec('grep -r "TODO" /var/www/projekt/', $output, $code);
switch ($code) {
case 0:
echo count($output) . " Treffer gefunden";
break;
case 1:
echo "Keine Treffer gefunden";
break;
default:
echo "Fehler bei der Suche (Code: $code)";
}
?>
Bei grep bedeutet Exit-Code 1 beispielsweise, dass keine Treffer gefunden wurden. Das ist kein Fehler im eigentlichen Sinne, sondern ein erwartetes Ergebnis. Solche Feinheiten lassen sich nur über den Exit-Code unterscheiden.
Fehlerausgabe abfangen mit 2>&1
Standardmäßig liefert exec() nur die Standardausgabe (stdout). Fehlermeldungen landen in stderr und gehen komplett verloren. Bei fehlgeschlagenen Befehlen steht dann ein leeres Array in $output, und die Fehlersuche wird schwierig. Mit dem Zusatz 2>&1 am Ende des Befehls werden Fehlermeldungen in die normale Ausgabe umgeleitet.
<?php
$output = [];
$code = 0;
/* Ohne 2>&1: Fehler geht verloren */
exec('convert bild.jpg bild.png', $output, $code);
/* $output ist leer, $code ist 1 */
/* Mit 2>&1: Fehler wird sichtbar */
exec('convert bild.jpg bild.png 2>&1', $output, $code);
/* $output enthält z.B.:
"convert: unable to open image 'bild.jpg'" */
?>
Gerade beim Aufruf externer Programme wie ImageMagick oder ffmpeg ist 2>&1 unverzichtbar, um Fehlermeldungen mitzubekommen.
Praxisbeispiele
Ohne konkrete Anwendungsfälle bleibt exec() abstrakt. Die folgenden Beispiele zeigen typische Aufgaben, die sich mit externen Programmen lösen lassen.
Bilder konvertieren mit ImageMagick
ImageMagick ist auf vielen Servern installiert und eignet sich hervorragend zum Konvertieren und Komprimieren von Bildern.
<?php
$quelle = escapeshellarg('/var/www/uploads/foto.png');
$ziel = escapeshellarg('/var/www/uploads/foto.webp');
exec("convert $quelle -quality 80 $ziel 2>&1",
$output, $code);
if ($code !== 0) {
error_log('ImageMagick-Fehler: ' .
implode("\n", $output));
}
?>
ZIP-Archiv erstellen
Für serverseitige Backups oder Download-Pakete lässt sich zip direkt aufrufen. Das ist oft schneller als die PHP-eigene ZipArchive-Klasse bei vielen Dateien.
<?php
$ordner = escapeshellarg('/var/www/backup/daten');
$archiv = escapeshellarg('/var/www/backup/archiv.zip');
exec("zip -r $archiv $ordner 2>&1", $output, $code);
if ($code === 0) {
echo "Backup erstellt";
} else {
echo "Fehler: " . implode(", ", $output);
}
?>
Git-Status abfragen
Für Deployment-Tools oder Admin-Panels kann es sinnvoll sein, den Git-Status direkt aus PHP abzufragen.
<?php
$repoDir = '/var/www/projekt';
exec("cd $repoDir && git log --oneline -5 2>&1",
$output, $code);
if ($code === 0) {
echo "Letzte 5 Commits:<br>";
foreach ($output as $commit) {
echo htmlspecialchars($commit) . "<br>";
}
}
?>
Sicherheit: escapeshellarg() vs. escapeshellcmd()
Werden Benutzereingaben in einen Shell-Befehl eingebaut, besteht die Gefahr von Command Injection. Ein Angreifer könnte über manipulierte Eingaben beliebige Befehle auf dem Server ausführen.
<?php
/* FALSCH: Eingabe direkt einsetzen */
$datei = $_GET['datei'];
exec('wc -l ' . $datei, $output);
/* Angreifer sendet: ?datei=test.txt;rm -rf / */
/* RICHTIG: Argument escapen */
$datei = escapeshellarg($_GET['datei']);
exec('wc -l ' . $datei, $output, $code);
if ($code === 0) {
echo "Zeilenanzahl: " . $output[0];
}
?>
PHP bietet zwei verschiedene Escape-Funktionen, die sich in ihrer Wirkung deutlich unterscheiden.
escapeshellarg() umschließt den Wert mit einfachen Anführungszeichen und escaped vorhandene Anführungszeichen. So wird sichergestellt, dass der Wert als ein einzelnes Argument behandelt wird. Diese Funktion ist die erste Wahl für Parameter, die von außen kommen.
escapeshellcmd() escaped Shell-Metazeichen (&, ;, |, * usw.) im gesamten Befehlsstring. Das verhindert, dass zusätzliche Befehle angehängt werden, erlaubt aber weiterhin mehrere Argumente. Damit ist escapeshellcmd() weniger restriktiv und nur sinnvoll, wenn der gesamte Befehl aus einer Benutzereingabe stammt.
Empfehlung: Verwende immer escapeshellarg() für einzelne Parameter. escapeshellcmd() ist nur als Notfallabsicherung für den gesamten Befehlsstring gedacht und sollte in neuem Code vermieden werden.
Verwandte Funktionen im Vergleich
PHP bietet mehrere Funktionen für die Ausführung von Systembefehlen. Je nach Anwendungsfall eignet sich eine andere besser.
shell_exec()
Gibt die gesamte Ausgabe als einzelnen String zurück. Praktisch, wenn die Ausgabe nicht zeilenweise verarbeitet werden muss.
<?php
$ergebnis = shell_exec('whoami');
echo "Aktueller Benutzer: " . trim($ergebnis);
?>
system()
Gibt die Ausgabe direkt an den Browser weiter, statt sie in einer Variable zu speichern. Nützlich für Befehle, deren Ausgabe sofort angezeigt werden soll.
<?php
$exitCode = 0;
system('df -h', $exitCode);
?>
passthru()
Leitet die rohe Ausgabe direkt durch, ohne sie zu verändern. Geeignet für Binärdaten, zum Beispiel wenn ein Bild über ein externes Programm generiert wird.
<?php
header('Content-Type: image/png');
passthru('convert input.jpg png:-');
?>
Fortgeschritten: proc_open() für volle Prozesskontrolle
Für einfache Befehle reicht exec() aus. Wenn man aber Daten per stdin an einen Prozess senden, stdout und stderr getrennt lesen oder den Prozess kontrolliert beenden will, ist proc_open() das richtige Werkzeug.
<?php
$descriptors = [
0 => ['pipe', 'r'], // stdin
1 => ['pipe', 'w'], // stdout
2 => ['pipe', 'w'], // stderr
];
$process = proc_open('php -r "echo strtoupper(fgets(STDIN));"',
$descriptors, $pipes);
if (is_resource($process)) {
/* Daten an stdin senden */
fwrite($pipes[0], "hallo welt");
fclose($pipes[0]);
/* stdout lesen */
$stdout = stream_get_contents($pipes[1]);
fclose($pipes[1]);
/* stderr lesen */
$stderr = stream_get_contents($pipes[2]);
fclose($pipes[2]);
$code = proc_close($process);
echo $stdout; // "HALLO WELT"
}
?>
Mit proc_open() hat man die volle Kontrolle über die Kommunikation mit dem Prozess. Das ist besonders nützlich bei Programmen, die Eingabedaten über stdin erwarten, beispielsweise bei der Konvertierung von Daten oder beim Aufruf von Compilern.
Timeout-Handling
Lang laufende Befehle können PHP blockieren und den Webserver belasten. Standardmäßig bricht PHP ein Script nach der in max_execution_time konfigurierten Zeit ab. Für einzelne Befehle lässt sich unter Linux mit dem timeout-Befehl ein gezieltes Zeitlimit setzen.
<?php
/* Befehl nach 30 Sekunden abbrechen */
exec('timeout 30 ffmpeg -i video.mp4 video.webm 2>&1',
$output, $code);
if ($code === 124) {
echo "Konvertierung abgebrochen: Zeitlimit erreicht";
} elseif ($code === 0) {
echo "Konvertierung erfolgreich";
} else {
echo "Fehler: " . implode(", ", $output);
}
?>
Der timeout-Befehl gibt Exit-Code 124 zurück, wenn die Zeit abgelaufen ist. So lässt sich ein Timeout sauber erkennen und behandeln.
exec() auf dem Server deaktivieren
Auf Shared-Hosting-Servern ist exec() oft aus Sicherheitsgründen deaktiviert. Das geschieht über die php.ini-Direktive disable_functions. Ob die Funktion verfügbar ist, lässt sich vorab prüfen.
<?php
if (!function_exists('exec')) {
echo "exec() ist auf diesem Server deaktiviert.";
} else {
exec('php -v', $output);
echo $output[0];
}
?>
Asynchrone Ausführung
Manchmal soll ein Befehl im Hintergrund laufen, ohne dass PHP auf das Ergebnis wartet. Das ist praktisch für lang laufende Aufgaben wie Video-Encoding oder Report-Generierung. Unter Linux leitet man die Ausgabe nach /dev/null um und hängt & an den Befehl an.
<?php
/* Hintergrundprozess starten */
exec('php /var/www/worker/report.php > /dev/null 2>&1 &');
echo "Report-Generierung gestartet";
?>
Der PHP-Prozess wartet dann nicht auf das Ende des Befehls und kann sofort eine Antwort an den Browser senden.
Zusammenfassung
exec() führt Systembefehle aus und liefert die Ausgabe zeilenweise in einem Array. Der Rückgabewert der Funktion ist die letzte Ausgabezeile, der Exit-Code steht im dritten Parameter. Benutzereingaben müssen vor der Übergabe immer mit escapeshellarg() abgesichert werden. Mit 2>&1 lassen sich Fehlermeldungen einfangen, und proc_open() bietet bei komplexen Anforderungen die volle Prozesskontrolle. Für einfachere Anwendungsfälle bieten sich shell_exec(), system() oder passthru() an.