Skip to content

执行运算符

概述

PHP 支持执行运算符(反引号 `)和 shell_exec() 函数,用于在 PHP 脚本中执行系统命令并获取输出。这提供了与操作系统交互的能力,但也带来了严重的安全风险。

安全风险

执行系统命令存在命令注入的严重安全风险。在处理用户输入时,必须严格过滤和转义。建议优先使用 PHP 内置函数替代系统命令调用。

基础概念

两种调用方式

php
// 方式1:反引号运算符
$output = `ls -la`;

// 方式2:shell_exec() 函数(等价)
$output = shell_exec('ls -la');

注意

反引号运算符在 PHP 的运算符优先级中低于字符串连接 .、加减 + - 等。如果需要在反引号中使用变量,建议使用 shell_exec() 更清晰。

语法与代码示例

基本命令执行

php
<?php

declare(strict_types=1);

// 执行简单命令
$output = shell_exec('ls -la');
echo $output . "\n";

// 带参数的命令
$filename = 'example.txt';
$content = shell_exec("cat {$filename}");
echo $content . "\n";

// 使用反引号
$host = shell_exec('hostname');
$currentUser = shell_exec('whoami');

echo "Host: " . trim($host) . "\n";
echo "User: " . trim($currentUser) . "\n";

// 获取系统信息
$phpVersion = shell_exec('php -v');
echo $phpVersion . "\n";

反引号中的变量解析

php
<?php

declare(strict_types=1);

// 反引号支持双引号风格的变量插值
$file = 'test.txt';
$result = `cat $file`; // 变量会被解析

// 复杂变量使用花括号
$dir = '/tmp';
$result = `ls {$dir}`;

// 但 shell_exec() 更清晰
$result = shell_exec("cat {$file}");
$result = shell_exec('cat ' . escapeshellarg($file));

反引号中的变量插值

反引号内的变量插值发生在 PHP 层面,然后才传递给 shell。这意味着如果 $file 包含 shell 特殊字符(如 ; rm -rf /),会构成命令注入风险。必须使用 escapeshellarg() 转义。

获取命令执行状态

php
<?php

declare(strict_types=1);

// shell_exec 只返回输出,不返回状态码
// 如需状态码,使用 exec()
$output = [];
$returnVar = 0;
exec('ls -la', $output, $returnVar);

echo "Return code: {$returnVar}\n"; // 0 表示成功
print_r($output);

// 使用 system() —— 直接输出并返回最后一行
$lastLine = system('ls -la', $returnVar);
echo "Last line: {$lastLine}\n";

// 使用 passthru() —— 直接输出原始结果
passthru('ls -la', $returnVar);

// 使用 proc_open() —— 更精细的控制(推荐)
$descriptorSpec = [
    0 => ['pipe', 'r'],  // stdin
    1 => ['pipe', 'w'],  // stdout
    2 => ['pipe', 'w'],  // stderr
];

$process = proc_open('ls -la', $descriptorSpec, $pipes);

if (is_resource($process)) {
    $stdout = stream_get_contents($pipes[1]);
    $stderr = stream_get_contents($pipes[2]);
    fclose($pipes[0]);
    fclose($pipes[1]);
    fclose($pipes[2]);

    $returnCode = proc_close($process);

    echo "STDOUT:\n{$stdout}\n";
    if ($returnCode !== 0) {
        echo "STDERR:\n{$stderr}\n";
    }
}

安全:escapeshellarg 和 escapeshellcmd

php
<?php

declare(strict_types=1);

// escapeshellarg:转义单个参数,添加单引号包裹
$file = 'my file.txt';
$safeArg = escapeshellarg($file);
// $safeArg = "'my file.txt'"
$result = shell_exec("cat " . $safeArg);

// escapeshellcmd:转义整个命令中的特殊字符
$command = 'cat ' . escapeshellarg($file);
$result = shell_exec($command);

// 安全的处理用户输入
function safeExecute(string $command, array $args): string
{
    foreach ($args as $key => $value) {
        $args[$key] = escapeshellarg($value);
    }
    $safeCommand = vsprintf($command, $args);
    return shell_exec($safeCommand) ?: '';
}

// 安全使用
$filename = $_GET['file'] ?? 'default.txt';
$content = shell_exec('cat ' . escapeshellarg($filename));

// 危险!不要这样做
// $filename = $_GET['file'] ?? 'default.txt';
// $content = shell_exec("cat {$filename}"); // 命令注入!

命令注入示例

如果 $filename = "test.txt; rm -rf /",直接拼接到命令中会执行 cat test.txt; rm -rf /,造成灾难性后果。始终使用 escapeshellarg() 转义用户提供的参数。

详细说明

各命令执行函数对比

函数返回值输出状态码适用场景
`cmd`输出字符串或 null不直接输出简单命令
shell_exec()输出字符串或 null不直接输出简单命令
exec()最后一行输出不直接输出获取输出数组和状态码
system()最后一行输出直接输出需要直接显示输出
passthru()直接输出原始数据二进制输出(如图像)
popen()文件指针不直接输出流式读取
proc_open()资源不直接输出精细控制(推荐)

PHP 中的替代方案

许多系统命令可以用 PHP 内置函数替代,更安全且跨平台:

php
<?php

declare(strict_types=1);

// 不推荐:shell_exec('ls -la')
// 推荐:scandir() + stat()
$files = scandir('/tmp');
foreach ($files as $file) {
    $path = '/tmp/' . $file;
    $stat = stat($path);
    echo "{$stat['mode']} {$stat['size']} {$file}\n";
}

// 不推荐:shell_exec('mkdir /tmp/mydir')
// 推荐:mkdir()
mkdir('/tmp/mydir', 0755, true); // true 启用递归创建

// 不推荐:shell_exec('cp file1 file2')
// 推荐:copy()
copy('/tmp/source.txt', '/tmp/dest.txt');

// 不推荐:shell_exec('chmod 755 file')
// 推荐:chmod()
chmod('/tmp/file.txt', 0755);

// 不推荐:shell_exec('wc -l file')
// 推荐:file() + count()
$lines = count(file('/tmp/file.txt'));

实战示例

安全的文件压缩工具

php
<?php

declare(strict_types=1);

class ArchiveManager
{
    /**
     * 使用 zip 命令安全地压缩目录
     */
    public function zipDirectory(string $sourceDir, string $outputFile): bool
    {
        if (!is_dir($sourceDir)) {
            throw new InvalidArgumentException("Source directory not found: {$sourceDir}");
        }

        // 使用 escapeshellarg 转义参数
        $safeSource = escapeshellarg($sourceDir);
        $safeOutput = escapeshellarg($outputFile);

        $command = "cd {$safeSource} && zip -r {$safeOutput} .";
        $output = shell_exec($command);

        return file_exists($outputFile);
    }

    /**
     * 使用 PHP ZipArchive 替代 shell 命令(更安全)
     */
    public function zipDirectoryNative(string $sourceDir, string $outputFile): bool
    {
        if (!class_exists('ZipArchive')) {
            throw new RuntimeException('ZipArchive extension not available');
        }

        $zip = new ZipArchive();
        if ($zip->open($outputFile, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
            return false;
        }

        $files = new RecursiveIteratorIterator(
            new RecursiveDirectoryIterator($sourceDir, RecursiveDirectoryIterator::SKIP_DOTS),
            RecursiveIteratorIterator::SELF_FIRST
        );

        foreach ($files as $file) {
            $filePath = $file->getRealPath();
            $relativePath = substr($filePath, strlen($sourceDir) + 1);
            if ($file->isDir()) {
                $zip->addEmptyDir($relativePath);
            } else {
                $zip->addFile($filePath, $relativePath);
            }
        }

        return $zip->close();
    }
}

系统信息获取

php
<?php

declare(strict_types=1);

class SystemInfo
{
    public function getDiskUsage(string $path = '/'): array
    {
        // 使用 PHP 内置函数
        $freeSpace = disk_free_space($path);
        $totalSpace = disk_total_space($path);

        return [
            'total' => $totalSpace,
            'free' => $freeSpace,
            'used' => $totalSpace - $freeSpace,
            'percentage' => round((($totalSpace - $freeSpace) / $totalSpace) * 100, 2),
        ];
    }

    public function getMemoryUsage(): array
    {
        // 使用 PHP 内置函数
        return [
            'current' => memory_get_usage(true),
            'peak' => memory_get_peak_usage(true),
            'limit' => ini_get('memory_limit'),
        ];
    }
}

$info = new SystemInfo();
print_r($info->getDiskUsage());
print_r($info->getMemoryUsage());

注意事项

  • 命令注入风险:永远不要将未经处理的用户输入传递给 shell 命令
  • 跨平台兼容性lscat 等 Unix 命令在 Windows 上可能不可用
  • 超时问题:长时间运行的命令可能需要设置 set_time_limit()
  • 权限问题:PHP 进程的运行用户可能没有执行某些命令的权限
  • 输出缓冲:大量输出可能导致内存耗尽,使用 proc_open() 流式处理

最佳实践

  1. 优先使用 PHP 内置函数:文件操作、目录操作等应使用 PHP 函数而非 shell 命令
  2. 必须使用 shell 命令时,转义所有参数escapeshellarg() 是必不可少的
  3. 使用 proc_open() 替代 shell_exec():更精细的控制,可以分离 stdout 和 stderr
  4. 限制可执行的命令白名单:如果必须接受用户输入,使用白名单匹配
  5. 记录所有命令执行:审计日志有助于追踪问题

进阶用法

调试与测试技巧

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');

参考链接