Procesos externos en PHP: exec(), shell_exec(), proc_open() y passthru()

PHP puede ejecutar comandos del sistema operativo a través de varias funciones: exec(), shell_exec(), passthru(), system() y proc_open(). Cada una tiene un comportamiento distinto respecto a la captura de salida y la comunicación con el proceso externo. Usarlas de forma segura exige escapar siempre los argumentos con escapeshellarg().

exec(): ejecutar y capturar líneas

exec() ejecuta el comando, devuelve la última línea de salida y, opcionalmente, llena un array con todas las líneas de salida y el código de salida del proceso.

<?php
// Forma básica: devuelve solo la última línea
$ultimaLinea = exec('ls /var/www');

// Forma completa: capturar toda la salida y el código de salida
$salida      = [];
$codigoSalida = 0;
exec('git log --oneline -5 2>&1', $salida, $codigoSalida);

if ($codigoSalida !== 0) {
    throw new RuntimeException('git log falló con código: ' . $codigoSalida);
}

foreach ($salida as $linea) {
    echo $linea . "n";
}

// SIEMPRE escapar argumentos que provienen del usuario
$nombreFichero = $_POST['nombre'] ?? '';
$rutaSegura    = '/var/datos/' . escapeshellarg(basename($nombreFichero));
exec('cat ' . $rutaSegura . ' 2>&1', $contenido, $codigo);
?>

shell_exec(): capturar toda la salida

shell_exec() devuelve toda la salida del comando como un único string. Devuelve null si el comando no produce salida o si ocurre un error.

<?php
// Obtener información del sistema
$infoMemoria = shell_exec('free -h');
$diskUsage   = shell_exec('df -h /');

echo $infoMemoria;

// Procesar salida de un comando externo
$json = shell_exec('curl -s https://api.github.com/repos/php/php-src');
if ($json === null) {
    throw new RuntimeException('No se pudo ejecutar curl.');
}

$datos = json_decode($json, true);
echo "PHP tiene " . $datos['stargazers_count'] . " estrellas en GitHub.n";

// Obtener el número de líneas de un fichero de log
$ruta      = escapeshellarg('/var/log/app.log');
$numLineas = trim(shell_exec("wc -l < $ruta") ?? '0');
echo "El fichero tiene $numLineas líneas.n";
?>

passthru() y system(): salida directa

passthru() envía la salida del comando directamente al navegador sin capturarla, ideal para datos binarios. system() hace lo mismo pero imprime la salida y también devuelve la última línea.

<?php
// passthru(): útil para datos binarios (imagen, zip, etc.)
// La salida va directamente al cliente sin pasar por PHP
header('Content-Type: image/png');
passthru('convert entrada.jpg -resize 800x600 png:-');

// system(): imprime cada línea según llega (útil para feedback en tiempo real)
echo "<pre>";
system('composer install --no-dev 2>&1', $codigo);
echo "</pre>";
echo "Código de salida: $codigon";
?>

proc_open(): comunicación bidireccional

proc_open() ofrece control total sobre los descriptores de fichero del proceso: se puede escribir en su STDIN, leer su STDOUT y STDERR por separado, y conocer el código de salida. Es la opción adecuada para procesos interactivos o cuando se necesita pasar datos al proceso.

<?php
function ejecutarConEntrada(string $comando, string $entrada): array
{
    $descriptores = [
        0 => ['pipe', 'r'],  // STDIN del proceso
        1 => ['pipe', 'w'],  // STDOUT del proceso
        2 => ['pipe', 'w'],  // STDERR del proceso
    ];

    $proceso = proc_open($comando, $descriptores, $pipes);

    if (!is_resource($proceso)) {
        throw new RuntimeException("No se pudo iniciar el proceso: $comando");
    }

    // Escribir en STDIN del proceso
    fwrite($pipes[0], $entrada);
    fclose($pipes[0]);

    // Leer STDOUT y STDERR
    $stdout = stream_get_contents($pipes[1]);
    $stderr = stream_get_contents($pipes[2]);
    fclose($pipes[1]);
    fclose($pipes[2]);

    $codigo = proc_close($proceso);

    return ['stdout' => $stdout, 'stderr' => $stderr, 'codigo' => $codigo];
}

// Pasar texto a un validador de JSON externo
$json      = '{"nombre": "Ana", "edad": 30}';
$resultado = ejecutarConEntrada('python3 -m json.tool', $json);

if ($resultado['codigo'] === 0) {
    echo "JSON válido:n" . $resultado['stdout'];
} else {
    echo "Error: " . $resultado['stderr'];
}
?>

escapeshellarg() y escapeshellcmd()

Nunca se deben concatenar valores externos en un comando de shell sin escaparlos. escapeshellarg() envuelve el valor entre comillas simples y escapa las que pueda contener. escapeshellcmd() escapa los metacaracteres del propio comando (menos habitual).

<?php
// SIN escapar: vulnerable a command injection
// Si $fichero = 'foto.jpg; rm -rf /', el comando eliminaría el sistema
$ficheroMalicioso = 'foto.jpg; rm -rf /';
exec("convert $ficheroMalicioso output.png"); // ¡PELIGROSO!

// CON escapeshellarg: el valor queda aislado entre comillas
$ficheroSeguro = escapeshellarg($ficheroMalicioso);
// $ficheroSeguro = "'foto.jpg; rm -rf /'"  (tratado como un único argumento)
exec("convert $ficheroSeguro output.png");   // seguro

// Para múltiples argumentos, escapar cada uno por separado
function convertirImagen(string $origen, string $destino, int $ancho, int $alto): void
{
    $cmd = sprintf(
        'convert %s -resize %dx%d %s',
        escapeshellarg($origen),
        (int) $ancho,
        (int) $alto,
        escapeshellarg($destino)
    );
    exec($cmd, $salida, $codigo);
    if ($codigo !== 0) {
        throw new RuntimeException('Error al convertir imagen.');
    }
}
?>

La documentación oficial de exec() y la de proc_open() detallan todos los tipos de descriptores disponibles (pipes, ficheros, null device), el manejo de señales y las restricciones del safe mode en entornos de alojamiento compartido.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP