php中分离捕获stdout/stderr的可靠方式是proc_open():需定义三元pipe描述符数组,分别读取stdout与stderr管道,用proc_get_status()获取退出码,最后proc_close()清理资源。

执行 shell 命令后返回码非 0,不代表一定失败;关键是把 stdout 和 stderr 分开捕获,再结合退出码做语义判断。直接忽略 stderr 或混在一起,容易掩盖真实问题(比如 rsync 报“有差异”却不是错误)。
PHP 中分离捕获 stdout/stderr 的可靠方式
shell_exec() 默认只取 stdout,stderr 被丢弃。要真正分离,必须用 proc_open() —— 这是 PHP 唯一能同时控制三个标准流的函数。
- 定义描述符数组:指定 stdin(可设为 pipe)、stdout(pipe)、stderr(pipe)
- 调用 proc_open() 启动进程,获取资源句柄和管道数组
- 用 stream_get_contents() 分别读取 stdout 和 stderr 管道(注意关闭写端,避免阻塞)
- 用 proc_get_status() 获取退出码,而不是依赖返回值是否为 null
- 最后用 proc_close() 清理资源,它会返回实际退出码
Go 中安全分离 stdout/stderr 的惯用写法
别用 cmd.StdoutPipe() + cmd.Start() 再手动读——一旦输出超 pipe buffer(通常 64KiB),进程就卡死。正确做法是启动 goroutine 异步读两个 pipe:
- 分别调用 cmd.StdoutPipe() 和 cmd.StderrPipe() 获取 reader
- 用两个 goroutine,各自把内容读进 bytes.Buffer 或直接写入 io.Writer
- 务必在 cmd.Start() 之后、cmd.Wait() 之前启动读取 goroutine(顺序不能错)
- Wait() 返回后,再从 buffer 提取字符串,同时检查 err 类型:*exec.ExitError 表示非零退出,可调 ExitCode()
- 如果只是调试,CombinedOutput() 更省事,但无法区分哪部分是 error
Shell 脚本里主动捕获并响应非零退出码
靠 $? 检查上一条命令退出码太被动。用 trap 可以全局监听 ERR 信号,适合封装健壮脚本:
-
trap 'echo "error at line $LINENO: $?" >&2; exit $?' ERR—— 出错立刻打印位置和码,并终止 - 若需继续执行但记录错误,把 exit 换成日志写入或变量赋值(如
last_err=$?) - 注意 trap ERR 不触发于管道中的中间命令(如
cmd1 | cmd2中 cmd1 失败),如需严格控制,改用set -o pipefail
为什么不能只看返回值是否为 null?
很多函数对“失败”的定义模糊:
- PHP 的 shell_exec() 返回 null,可能是命令不存在、权限不足,也可能是 stderr 输出了错误但 stdout 为空
- Go 的 cmd.Output() 返回 error,可能是 exec.ExitError(子进程挂了)、exec.Error(找不到命令)、甚至 io.ErrUnexpectedEOF(管道异常中断)
- 某些工具(如 make、grep、rsync)约定非零退出码表示“状态变化”,不是故障。得结合业务逻辑判断











