最直接可靠的判断 CLI 环境的方式是 php_sapi_name() === 'cli',它稳定返回 "cli" 字符串;而 $_SERVER['argv'] 存在或 !isset($_SERVER['HTTP_HOST']) 等方法均不可靠。

php_sapi_name() 返回 cli 就是 CLI 环境
最直接可靠的判断方式是调用 php_sapi_name(),它返回当前 PHP 的 SAPI(Server API)类型。在命令行下执行时,该函数稳定返回字符串 "cli",而非 "apache2handler"、"fpm-fcgi" 或 "litespeed" 等 Web 模式标识。
注意:不能只检查 PHP_SAPI 常量——它只是 php_sapi_name() 的别名,本质相同;也不能依赖 $_SERVER['SHELL'] 或 getenv('TERM'),这些在某些容器或精简环境里可能为空或不可靠。
示例判断逻辑:
if (php_sapi_name() === 'cli') {
echo "正在 CLI 下运行\n";
} else {
echo "不是 CLI 环境\n";
}
$_SERVER['argv'] 存在且为数组,但不足够可靠
$_SERVER['argv'] 在 CLI 下通常存在且是数组(含脚本路径和参数),但在某些 Web SAPI(如 CGI 模式)中也可能被填充,尤其当服务器配置了 variables_order = "EGPCS" 且 CGI 参数被注入时,容易误判。
立即学习“PHP免费学习笔记(深入)”;
更关键的是:CLI 脚本若被 include 或 require 到 Web 脚本中,$_SERVER['argv'] 可能仍存在,但此时实际运行上下文已是 Web 环境。
- 仅用
isset($_SERVER['argv'])判断会出错 - 即使
is_array($_SERVER['argv'])成立,也不代表当前是 CLI 入口 - 真正安全的做法是结合
php_sapi_name()+ 是否为脚本主入口(比如检查__FILE__ === $_SERVER['SCRIPT_FILENAME'])
避免用 getallheaders() 或 $_SERVER['HTTP_HOST'] 做反向推断
有人想“既然 Web 环境有 HTTP 头,那没头就是 CLI”,于是写 !isset($_SERVER['HTTP_HOST'])。这很危险:
- CLI 脚本若通过
curl或file_get_contents("http://...")发起请求,$_SERVER里依然不会有HTTP_HOST,但脚本本身仍是 CLI - 某些 FastCGI 配置下,Web 请求的
$_SERVER可能缺失部分 HTTP 变量 -
getallheaders()在 CLI 下会警告Warning: getallheaders(): Not valid in CLI mode,但触发警告本身已破坏静默运行需求
这类“排除法”在边界场景下极易翻车,不如直取 SAPI 类型。
在 Composer 脚本或 PHPUnit 测试中要特别注意
Composer 的 scripts 和 PHPUnit 的测试用例,虽然最终由 CLI 启动,但它们的执行上下文可能被封装在闭包或自定义 runner 中,php_sapi_name() 依然返回 "cli",这点没问题;但要注意:
- PHPUnit 默认禁用输出缓冲,而某些 CLI 工具依赖
ob_get_level()判断是否可安全输出,需单独处理 - Composer 脚本若调用
exec('php some-script.php'),子进程仍是 CLI,但父进程的STDIN/STDOUT可能被重定向,影响交互逻辑 - 使用
readline()或fgets(STDIN)前,务必先确认php_sapi_name() === 'cli'且stream_isatty(STDIN)为 true,否则会阻塞或报错
CLI 判断本身很简单,难的是后续行为是否适配——比如日志格式要不要加颜色、错误要不要显示堆栈、输入要不要等待用户键入。这些细节一旦漏掉,脚本在 cron 或 Docker 容器里就容易静默失败。



















