Skip to content

系统程序执行

概述

PHP 提供了多个函数用于执行系统命令和外部程序:exec()system()passthru()shell_exec()proc_open()。每个函数有不同的输出处理方式和返回值。使用这些函数时必须注意安全性,防止命令注入攻击。

适用场景

  • 调用系统工具(如 git、rsync)
  • 执行编译/构建命令
  • 图片处理(ImageMagick、FFmpeg)
  • 系统管理脚本

基础概念

执行函数对比

函数输出处理返回值终止符
exec()可选输出数组最后一行
system()直接输出最后一行
passthru()直接输出(二进制安全)状态码
shell_exec()返回完整输出string|null反引号
proc_open()流式读写resource
popen()流式读写resource

安全警告

永远不要将用户输入直接传递给执行函数。使用 escapeshellarg()escapeshellcmd() 进行转义。

语法与代码示例

exec()

php
<?php

// exec - 执行命令,返回最后一行
$output = exec('ls -la /tmp');
echo "最后一行: {$output}\n";

// exec 获取完整输出
exec('ls -la /tmp', $outputLines, $returnCode);
foreach ($outputLines as $line) {
    echo $line . PHP_EOL;
}
echo "返回码: {$returnCode}\n";

// 使用附加输出参数
exec('git status --short', $files, $code);
echo "变更文件: " . count($files) . "\n";

system()

php
<?php

// system - 执行命令并直接输出到 STDOUT
$lastLine = system('echo "Hello from system()"');
echo "最后一行: {$lastLine}\n";

// 常用于交互式命令
system('clear'); // 清屏

passthru()

php
<?php

// passthru - 适合输出二进制数据(如图片)
header('Content-Type: image/png');
passthru('cat /tmp/screenshot.png');

// 带返回值
passthru('ffmpeg -i /tmp/input.mp4 /tmp/output.mp3', $code);
echo "FFmpeg 返回码: {$code}\n";

shell_exec()

php
<?php

// shell_exec - 返回完整输出
$output = shell_exec('ls -la /tmp | wc -l');
echo "文件数: " . trim($output) . "\n";

// 反引号是 shell_exec 的简写
$output = `ls -la /tmp | wc -l`;
echo "文件数: " . trim($output) . "\n";

// 注意:shell_exec 在命令失败时返回 null
$result = shell_exec('ls /nonexistent');
var_dump($result); // null 或空字符串

proc_open()

php
<?php

// proc_open - 完全控制输入输出流
$descriptors = [
    0 => ['pipe', 'r'],  // stdin
    1 => ['pipe', 'w'],  // stdout
    2 => ['pipe', 'w'],  // stderr
];

$process = proc_open('php -r "while($line=fgets(STDIN)){echo strtoupper($line);}"', $descriptors, $pipes);

if (is_resource($process)) {
    // 写入 stdin
    fwrite($pipes[0], "hello\n");
    fwrite($pipes[0], "world\n");
    fclose($pipes[0]);

    // 读取 stdout
    echo "输出: " . stream_get_contents($pipes[1]);
    fclose($pipes[1]);

    // 读取 stderr
    $error = stream_get_contents($pipes[2]);
    fclose($pipes[2]);

    // 等待进程结束
    $returnCode = proc_close($process);
    echo "返回码: {$returnCode}\n";
}

实战示例

安全的命令执行封装

php
<?php

declare(strict_types=1);

class CommandExecutor
{
    /**
     * 安全执行命令
     */
    public static function exec(string $command, array $args = []): CommandResult
    {
        // 转义参数
        $escapedArgs = array_map('escapeshellarg', $args);
        $fullCommand = vsprintf($command, $escapedArgs);

        $output = [];
        $returnCode = 0;
        $startTime = microtime(true);

        exec($fullCommand . ' 2>&1', $output, $returnCode);

        $elapsed = round(microtime(true) - $startTime, 3);

        return new CommandResult(
            command: $fullCommand,
            output: $output,
            returnCode: $returnCode,
            elapsed: $elapsed
        );
    }

    /**
     * 交互式执行
     */
    public static function execInteractive(string $command, string $stdin = ''): CommandResult
    {
        $descriptors = [
            0 => ['pipe', 'r'],
            1 => ['pipe', 'w'],
            2 => ['pipe', 'w'],
        ];

        $process = proc_open($command, $descriptors, $pipes);
        if (!is_resource($process)) {
            return new CommandResult($command, [], 255, 0);
        }

        fwrite($pipes[0], $stdin);
        fclose($pipes[0]);

        $stdout = stream_get_contents($pipes[1]);
        $stderr = stream_get_contents($pipes[2]);
        fclose($pipes[1]);
        fclose($pipes[2]);

        $returnCode = proc_close($process);

        return new CommandResult($command, explode("\n", trim($stdout)), $returnCode, 0);
    }
}

class CommandResult
{
    public function __construct(
        public readonly string $command,
        public readonly array $output,
        public readonly int $returnCode,
        public readonly float $elapsed
    ) {}

    public function isSuccessful(): bool
    {
        return $this->returnCode === 0;
    }

    public function getOutput(): string
    {
        return implode("\n", $this->output);
    }
}

// 使用
$result = CommandExecutor::exec('git status --short', []);
if ($result->isSuccessful()) {
    echo "变更文件: " . trim($result->getOutput()) . "\n";
} else {
    fwrite(STDERR, "命令失败: {$result->command}\n");
}

注意事项

命令注入防护

php
<?php

// 危险:直接拼接用户输入
$file = $_GET['file'];
exec("cat {$file}"); // 用户可以传入 "file; rm -rf /"

// 安全:使用 escapeshellarg
$file = escapeshellarg($_GET['file']);
exec("cat {$file}");

// 更安全:白名单验证
$allowedFiles = ['config.json', 'README.md'];
$requestedFile = basename($_GET['file']);
if (!in_array($requestedFile, $allowedFiles, true)) {
    die("不允许的文件");
}
exec("cat " . escapeshellarg($requestedFile));

禁用函数

bash
# php.ini 安全配置:禁用危险的执行函数
```ini
disable_functions = exec,passthru,shell_exec,system,proc_open,popen

最佳实践

1. 记录命令执行日志

php
<?php

function safeExec(string $command, array $args = []): array
{
    $escapedArgs = array_map('escapeshellarg', $args);
    $fullCommand = vsprintf($command, $escapedArgs);

    error_log("Executing: {$fullCommand}");

    $output = [];
    $code = 0;
    exec($fullCommand . ' 2>&1', $output, $code);

    if ($code !== 0) {
        error_log("Command failed ({$code}): {$fullCommand}");
    }

    return ['output' => $output, 'code' => $code];
}

2. 使用 proc_open 处理大输出

php
<?php

// 对于输出量大的命令,使用 proc_open 流式处理
function streamExec(string $command): Generator
{
    $descriptors = [
        0 => ['pipe', 'r'],
        1 => ['pipe', 'w'],
        2 => ['pipe', 'w'],
    ];

    $process = proc_open($command, $descriptors, $pipes);
    if (!is_resource($process)) return;

    fclose($pipes[0]);

    while (($line = fgets($pipes[1])) !== false) {
        yield trim($line);
    }

    fclose($pipes[1]);
    fclose($pipes[2]);
    proc_close($process);
}

进阶用法

调试与测试技巧

php
<?php
declare(strict_types=1);

// 单元测试辅助函数
function createTestResource(): mixed
{
    return match (true) {
        default => new stdClass(),
    };
}

// 调试输出函数
function debugOutput(mixed , string  = ''): void
{
     =  ? ": " : '';
     .= print_r(, true);
    fwrite(STDERR,  . "\n");
}

// 性能基准测试
function benchmark(callable , int  = 1000): float
{
     = hrtime(true);
    for ($i = 0; $i < $iterations; $i++) {
        $fn();
    }
    return (hrtime(true) - $start) / 1e9;
}

日志记录实践

php
<?php
declare(strict_types=1);

/**
 * 简易日志记录器
 */
class SimpleLogger
{
    private string $logFile;
    private string $level = 'INFO';

    public function __construct(string $logFile)
    {
        $this->logFile = $logFile;
    }

    public function info(string $message, array $context = []): void
    {
        $this->log('INFO', $message, $context);
    }

    public function warning(string $message, array $context = []): void
    {
        $this->log('WARNING', $message, $context);
    }

    public function error(string $message, array $context = []): void
    {
        $this->log('ERROR', $message, $context);
    }

    private function log(string $level, string $message, array $context): void
    {
        $timestamp = date('Y-m-d H:i:s');
        $contextStr = $context ? ' ' . json_encode($context, JSON_UNESCAPED_UNICODE) : '';
        $line = "[{$timestamp}] [{$level}] {$message}{$contextStr}\n";
        file_put_contents($this->logFile, $line, FILE_APPEND | LOCK_EX);
    }
}

配置与环境检测

php
<?php
declare(strict_types=1);

// 环境检测工具
class EnvironmentChecker
{
    public static function checkRequirements(array $requirements): array
    {
        $results = [];
        foreach ($requirements as $name => $check) {
            $results[$name] = is_callable($check) ? $check() : false;
        }
        return $results;
    }

    public static function getSystemInfo(): array
    {
        return [
            'php_version' => PHP_VERSION,
            'os' => PHP_OS,
            'sapi' => PHP_SAPI,
            'memory_limit' => ini_get('memory_limit'),
            'max_execution_time' => ini_get('max_execution_time'),
            'loaded_extensions' => get_loaded_extensions(),
        ];
    }
}

常见问题排查

问题可能原因解决方案
连接超时网络问题/配置错误检查配置,增加超时时间
权限不足文件/目录权限使用 chmod/chown 修正
性能下降索引缺失/数据量大添加索引,优化查询
数据不一致并发冲突/事务残留使用锁机制和事务
内存溢出大数据集/未释放资源增大内存限制,分批处理

故障排除步骤

  1. 检查错误日志和异常信息
  2. 确认配置和环境是否正确
  3. 使用调试工具逐步排查
  4. 参考官方文档查找已知问题

版本兼容性说明

功能最低版本说明
基础功能PHP 8.1本文档基准版本
只读属性PHP 8.1public readonly 修饰符
枚举类型PHP 8.1enum 类型和 match 表达式
FiberPHP 8.1协程/轻量级并发
命名参数PHP 8.0foo(arg_name: value)
联合类型PHP 8.0`int
Null 安全运算符PHP 8.0$obj?->method()
析构器 promotionPHP 8.0__construct(public $x)
php
<?php
declare(strict_types=1);

// 版本兼容性检测
function ensureVersion(string $minVersion): void
{
    if (version_compare(PHP_VERSION, $minVersion, '<')) {
        throw new RuntimeException(
            sprintf('需要 PHP %s+, 当前版本: %s', $minVersion, PHP_VERSION)
        );
    }
}

ensureVersion('8.1.0');

参考链接