Выполнение внешних процессов из PHP: функции, примеры и рекомендации

Раздел: Администрирование и конфигурация PHP -> Выполнение системных команд из PHP

Выполнение внешних команд в PHP: обзор и практические решения

PHP предоставляет несколько способов вызова системных команд. Выбор конкретного метода зависит от требуемой степени контроля над процессом, объема выводимых данных и необходимости обработки кода возврата. Рассмотрим основные функции, их особенности и случаи применения.

Как выполнить команду и получить весь вывод в виде строки?

Наиболее эффективное решение для получения полного текстового вывода команды - функция shell_exec. Она выполняет команду через оболочку и возвращает строку со всем выводом. Если команда ничего не вывела, возвращается null.

<?php
$output = shell_exec('ls -la');
echo $output;
?>

Php выполнение команды (выполнение внешних команд в php)

В данном примере команда ls -la выводит содержимое текущего каталога. Функция удобна для быстрого получения результата, когда не требуется обрабатывать отдельные строки или код возврата.

Типичные проблемы:

  • Небезопасное экранирование. Если в команде используются пользовательские данные, обязательно применяйте escapeshellarg() или escapeshellcmd().
  • Команда может быть заблокирована в безопасном режиме (safe_mode). В современных версиях PHP эта опция удалена, но на старых конфигурациях может вызывать ошибки.
  • Вывод может быть очень большим. shell_exec загружает весь вывод в память, что для потоковых данных неэффективно.
  • При ошибках выполнения функция может вернуть null, что не позволяет отличить пустой вывод от сбоя.

Рекомендуется проверять код возврата с помощью exec или system, если требуется надежное обнаружение ошибок.

Как получить код возврата и построчный вывод?

Для выполнения команды с возможностью получить код возврата и массив строк вывода используется exec. Первый аргумент - команда, второй (опционально) - массив, в который будет помещён построчный вывод. Третий аргумент - переменная для кода возврата.

<?php
$output = [];
$returnCode = 0;
exec('ls -la', $output, $returnCode);
if ($returnCode === 0) {
    foreach ($output as $line) {
        echo $line . "\n";
    }
} else {
    echo "Ошибка выполнения. Код: $returnCode";
}
?>

Цель использования: когда необходим анализ каждой строки вывода и проверка успешности выполнения. Например, при запуске скриптов резервного копирования или проверке наличия файлов.

Проблема: массив $output может быть большим, что потребляет память. Также exec не возвращает содержимое потока ошибок (stderr). Для захвата stderr нужно перенаправлять в stdout (2>&1) или использовать proc_open.

Как выполнить команду и вывести результат напрямую в поток вывода?

Функция system выполняет команду и сразу выводит результат в стандартный поток вывода (обычно в браузер). Она возвращает последнюю строку вывода.

<?php
$lastLine = system('ping -c 1 8.8.8.8', $returnCode);
echo "Последняя строка: $lastLine";
?>

Подходит для команд, вывод которых нужно немедленно отправлять клиенту, например, при длительных операциях (потоковый вывод).

Недостаток: вывод не может быть легко захвачен в переменную для дальнейшей обработки. Также функция блокирует выполнение скрипта до завершения команды.

Как выполнить команду и передать бинарные данные в вывод?

passthru аналогична system, но не преобразует вывод. Она подходит для передачи бинарных данных, например, изображений или архивов. Возвращает код возврата.

<?php
header('Content-Type: image/png');
passthru('cat /path/to/image.png', $returnCode);
?>

Используется, когда нужно напрямую выдать результат работы команды (например, программу convert из ImageMagick) без буферизации.

Ошибки: если команда не найдена или нет прав, в вывод может попасть сообщение об ошибке, которое испортит бинарные данные. Следует проверять код возврата.

Как выполнить команду, записав её в обратных кавычках?

В PHP, как и в оболочке, можно использовать оператор обратные кавычки (``). Он эквивалентен shell_exec. Выражение в кавычках выполняется как команда.

<?php
$result = `whoami`;
echo "Текущий пользователь: $result";
?>

Синтаксически лаконично, но снижает читаемость и не даёт возможности передать код возврата. Подходит для однострочных команд в сценариях.

Проблемы безопасности: легко забыть экранирование; плохо поддается отладке. Использовать с осторожностью.

Как управлять потоками stdin, stdout, stderr процесса?

Для полного контроля над вводом/выводом процесса используется proc_open. Она открывает дескрипторы для обмена данными с выполняемой программой.

<?php
$descriptorspec = [
    0 => ['pipe', 'r'], // stdin
    1 => ['pipe', 'w'], // stdout
    2 => ['pipe', 'w']  // stderr
];
$process = proc_open('grep something', $descriptorspec, $pipes);
if (is_resource($process)) {
    fwrite($pipes[0], 'some input text');
    fclose($pipes[0]);
    $stdout = stream_get_contents($pipes[1]);
    fclose($pipes[1]);
    $stderr = stream_get_contents($pipes[2]);
    fclose($pipes[2]);
    $returnCode = proc_close($process);
    echo "Stdout: $stdout\nStderr: $stderr\nКод возврата: $returnCode";
}
?>

Цель: интерактивное взаимодействие с программой, работа с большими потоками данных без загрузки в память, захват stderr отдельно.

Сложность реализации, риск зависания при неполном чтении/записи. Требуется аккуратно закрывать дескрипторы и вызывать proc_close.

Как выполнить команду и читать/писать данные через однонаправленный поток?

popen открывает однонаправленный канал (только чтение или только запись) к процессу. Возвращает файловый указатель.

<?php
$handle = popen('ls -la', 'r');
if ($handle) {
    while (($line = fgets($handle)) !== false) {
        echo $line;
    }
    pclose($handle);
}
?>

Удобно, когда нужно обрабатывать вывод построчно, не загружая его целиком.

Нельзя одновременно читать и писать, нет доступа к stderr. Для более сложных сценариев лучше proc_open.

Расширенные примеры выполнения внешних команд

Экранирование аргументов с escapeshellarg

При подстановке пользовательских данных в команду обязательно экранировать каждый аргумент, чтобы предотвратить внедрение кода.

Пример
<?php
$filename = $_GET['file'] ?? 'test.txt';
$safeFilename = escapeshellarg($filename);
$command = "wc -l $safeFilename";
$output = shell_exec($command);
echo "Количество строк: $output";
?>

Результат (если передан file=test.txt):

Количество строк: 10 test.txt

Если бы экранирование отсутствовало, злоумышленник мог бы передать test.txt; rm -rf /.

Выполнение команды с таймаутом через proc_open

Некоторые команды могут зависнуть. Реализация таймаута с помощью proc_get_status и usleep:

Пример
<?php
$descriptorspec = [0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w']];
$process = proc_open('sleep 10', $descriptorspec, $pipes);
$timeout = 5; // секунд
$start = time();
while (true) {
    $status = proc_get_status($process);
    if (!$status['running']) break;
    if (time() - $start > $timeout) {
        // принудительно завершить
        proc_terminate($process, 9);
        echo "Команда прервана по таймауту";
        break;
    }
    usleep(100000); // 100 ms
}
fclose($pipes[0]); fclose($pipes[1]); fclose($pipes[2]);
proc_close($process);
?>

Результат: через 5 секунд появится сообщение о прерывании, процесс будет убит.

Запуск фоновой команды (без ожидания завершения)

Чтобы выполнить команду в фоне, можно перенаправить вывод и использовать exec с амперсандом в конце команды:

Пример
<?php
// Запуск в фоне: вывод в /dev/null, команда завершается
$command = '/usr/bin/php /path/to/background.php > /dev/null 2>&1 &';
exec($command, $output, $returnCode);
echo "Команда запущена в фоне, код возврата: $returnCode";
?>

Важно: PHP продолжит выполнение, не дожидаясь завершения команды.

Получение stderr вместе с stdout с помощью proc_open

Пример
<?php
$descriptorspec = [
    0 => ['pipe', 'r'],
    1 => ['pipe', 'w'],
    2 => ['pipe', 'w']
];
$process = proc_open('ls /nonexistent', $descriptorspec, $pipes);
if (is_resource($process)) {
    fclose($pipes[0]);
    $stdout = stream_get_contents($pipes[1]);
    fclose($pipes[1]);
    $stderr = stream_get_contents($pipes[2]);
    fclose($pipes[2]);
    proc_close($process);
    echo "Stdout: '$stdout'\n";
    echo "Stderr: '$stderr'\n";
}
?>

Результат:

Stdout: ''
Stderr: 'ls: cannot access '/nonexistent': No such file or directory
'

Использование passthru для выдачи изображения

Пример
<?php
$imagePath = '/var/www/image.png';
if (file_exists($imagePath)) {
    header('Content-Type: image/png');
    passthru('cat ' . escapeshellarg($imagePath), $returnCode);
    if ($returnCode !== 0) {
        // обработка ошибки
    }
} else {
    echo 'Файл не найден';
}
?>

Комбинирование exec и escapeshellcmd

escapeshellcmd экранирует всю команду целиком (в отличие от escapeshellarg, который экранирует только один аргумент). Рекомендуется использовать для простых команд без аргументов.

Пример
<?php
$cmd = escapeshellcmd('ls -la /tmp'); // не экранирует аргументы, только метасимволы
$output = shell_exec($cmd);
echo $output;
?>

Внимание: escapeshellcmd не защищает от инъекций, если в команде есть непроверенные параметры. Лучше использовать escapeshellarg для каждого аргумента.

Неблокирующее выполнение с proc_open и stream_select

Позволяет взаимодействовать с процессом, не блокируя выполнение скрипта.

Пример
<?php
$descriptorspec = [0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w']];
$process = proc_open('cat', $descriptorspec, $pipes);
stream_set_blocking($pipes[1], 0); // неблокирующий режим
fwrite($pipes[0], "Hello\n");
sleep(1); // имитация работы
fwrite($pipes[0], "World\n");
fclose($pipes[0]);
$output = '';
while ($line = fgets($pipes[1])) {
    $output .= $line;
}
echo $output;
fclose($pipes[1]); fclose($pipes[2]);
proc_close($process);
?>

Результат:

Hello
World

При таком подходе скрипт может продолжать обработку, пока процесс выполняется.

Выполнение внешних команд в PHP - comments

En
Php выполнение команды (php)