| 函數 | 輸出方式 | 返回值 | 獲取完整輸出 | 獲取退出碼 | 適用場景 |
|---|---|---|---|---|---|
system() | 直接輸出到標準輸出 | 最後一行 (字符串) | 困難 (需輸出緩衝捕獲) | 第二參數 &$retval | 簡單命令、直接顯示結果 |
exec() | 不輸出 | 最後一行 (字符串) | 第二參數 &$output 數組 (每行一元素) | 第三參數 &$retval | **最常用**,需程序化處理輸出 |
passthru() | 直接輸出原始二進制流 | 無 (void) | 困難 | 第二參數 &$retval | 圖片/PDF/二進制流直接輸出瀏覽器 |
shell_exec() | 不輸出 | 完整輸出 (字符串,含換行) | 直接返回 | 無 (需自行解析) | 需完整輸出字符串、反引號別名 |
popen() | 管道流 (資源句柄) | 文件指針 (resource) | fread/fgets 讀取 / fwrite 寫入 | pclose() 返回退出碼 | **雙向交互**、長時間運行進程、流式處理 |
反引號 | 不輸出 | 完整輸出 (字符串) | 直接返回 | 無 | shell_exec 別名,簡潔但可讀性差 |
<?php
// 1. system
$lastLine = system('ls -la', $retval);
echo "退出碼: $retval\n";
// 2. exec (推薦)
$output = [];
$lastLine = exec('ls -la', $output, $retval);
print_r($output); // 每行一個元素
echo "最後一行: $lastLine, 退出碼: $retval\n";
// 3. passthru (輸出圖片示例)
header('Content-Type: image/png');
passthru('cat /path/to/image.png'); // 直接輸出二進制
// 4. shell_exec / 反引號
$fullOutput = shell_exec('ls -la');
// 等價
$fullOutput = `ls -la`;
echo $fullOutput;
// 5. popen (雙向交互:修改密碼示例)
$descriptors = [
0 => ['pipe', 'r'], // stdin
1 => ['pipe', 'w'], // stdout
2 => ['pipe', 'w'], // stderr
];
$proc = proc_open('passwd ' . escapeshellarg($username), $descriptors, $pipes);
if (is_resource($proc)) {
fwrite($pipes[0], $newPassword . "\n" . $newPassword . "\n");
fclose($pipes[0]);
$stdout = stream_get_contents($pipes[1]);
$stderr = stream_get_contents($pipes[2]);
fclose($pipes[1]); fclose($pipes[2]);
$exitCode = proc_close($proc);
// 判斷 $exitCode, $stderr
}
**所有接收用戶輸入構建命令的場景,必須雙重轉義:**
$userInput = $_POST['filename']; // 惡意: "test.txt; rm -rf /"
// 錯誤示範
exec("cat $userInput"); // 執行了 rm -rf /
// 正確:escapeshellarg (參數級) + escapeshellcmd (命令級)
$safeArg = escapeshellarg($userInput); // 'test.txt; rm -rf /' (單引號包裹,內部單引號轉義)
exec("cat $safeArg"); // 僅嘗試讀取名為 "test.txt; rm -rf /" 的文件
// 更安全:完全避免 Shell,使用 PHP 內置函數
$content = file_get_contents($userInput); // 無命令注入風險
escapeshellarg($str):用單引號包裹,內部單引號轉義為 '\'',確保作為**單個參數**傳遞。escapeshellcmd($str):轉義 Shell 元字符 (; & | > < ( ) $ ` \ 等),確保作為**命令本身**安全。// 1. 設置腳本最大執行時間 (影響 system/exec/shell_exec 等待上限)
set_time_limit(300); // 5 分鐘
// 2. 命令級超時 (需外部工具)
exec("timeout 10s long_running_command", $out, $ret); // Linux timeout 命令
// 3. 後台運行 (避免阻塞 PHP 請求)
// 方法 A: 輸出重定向到 /dev/null + &
exec("nohup php /path/to/task.php > /dev/null 2>&1 &", $out, $ret);
// 方法 B: popen 寫入管道後立即關閉 (不等待)
$fp = popen("php /path/to/task.php > /dev/null 2>&1 &", 'r');
pclose($fp); // 立即返回,進程在後台運行
// 方法 C: 系統級服務 (systemd, Supervisor, Gearman, RabbitMQ Worker) —— **生產首選**
exec/system 調用 cmd.exe /c,路徑分隔符、引號規則不同。passthru 輸出編碼可能為 CP936/CP65001,需 iconv 轉 UTF-8。popen 模式 r/w 在 Windows 僅支持單向 (PHP 7.1+ 改進)。exec('"C:\Program Files\Tool\tool.exe" arg')。disable_functions = exec,system,passthru,shell_exec,proc_open,proc_get_status,proc_close,proc_nice,proc_terminate,popen,pclose,pcntl_exec,pcntl_fork,pcntl_signal,pcntl_alarm,pcntl_wait,pcntl_waitpid,pcntl_wexitstatus,pcntl_wifsignaled,pcntl_wifexited,pcntl_wifstopped,pcntl_wstopsig,pcntl_wtermsig
# 視業務需要保留極少數 (如.exec 用於 Composer 安裝部署腳本),其餘嚴禁
exec() (首選)。passthru()。shell_exec() 或反引號。proc_open() / popen()。system()。