标准输入输出
概述
PHP CLI 提供了标准输入(STDIN)、标准输出(STDOUT)和标准错误(STDERR)三个预定义常量,分别对应文件描述符 0、1、2。它们是命令行程序与用户和操作系统交互的核心通道。
三个标准流
| 常量 | 描述 | 用途 |
|---|---|---|
STDIN | 标准输入(fd:0) | 读取用户输入、管道数据 |
STDOUT | 标准输出(fd:1) | 正常输出结果 |
STDERR | 标准错误(fd:2) | 输出错误/警告信息 |
基础概念
I/O 模型
php
<?php
// STDIN — 可读资源(用户输入/管道/文件重定向)
// STDOUT — 可写资源(正常输出)
// STDERR — 可写资源(错误输出,可重定向到日志文件)
// 管道示例: echo "hello" | php script.php
// $data = fgets(STDIN); // 读取管道数据: "hello"
// 输出重定向: php script.php > output.log 2> error.log
// echo "正常输出" → output.log
// fwrite(STDERR, "错误") → error.log语法与代码
读取用户输入
php
<?php
declare(strict_types=1);
// fgets(STDIN) — 读取一行(包含换行符)
echo "请输入你的名字: ";
$name = fgets(STDIN);
$name = trim($name); // 移除换行符
echo "你好, {$name}!\n";
// fread(STDIN, length) — 读取指定长度
$buffer = fread(STDIN, 1024);
// stream_get_line(STDIN) — 读取一行(不含换行符)
// 支持自定义分隔符
$input = stream_get_line(STDIN, 1024, ':'); // 以冒号为分隔符
// readline() — 交互式输入(需要 readline 扩展)
// 支持: 历史记录、光标移动、Tab 补全
$input = readline("请输入命令: ");
readline_add_history($input); // 添加到历史写入输出
php
<?php
declare(strict_types=1);
// echo — 输出到 STDOUT(最常用)
echo "Hello World\n";
echo "多个", "参数", "可以", "拼接\n";
// print — 输出单个值(返回1)
print "Hello\n";
// printf — 格式化输出
printf("姓名: %s, 年龄: %d, 身高: %.1fcm\n", '张三', 28, 175.5);
// vprintf — 数组格式化
vprintf("%s-%s-%s\n", ['2024', '01', '15']);
// fwrite(STDOUT) — 写入标准输出
fwrite(STDOUT, "写入 STDOUT\n");
// fwrite(STDERR) — 写入标准错误
fwrite(STDERR, "[ERROR] 发生错误\n");
fwrite(STDERR, "[WARNING] 这是一条警告\n");流式处理大数据
php
<?php
declare(strict_types=1);
// 逐行处理大文件 — 内存友好
$handle = fopen('php://stdin', 'r');
$lineNumber = 0;
while (($line = fgets($handle)) !== false) {
$lineNumber++;
$line = trim($line);
if (empty($line)) {
continue;
}
// 处理每行数据
echo "Line {$lineNumber}: {$line}\n";
}
fclose($handle);
// 使用 STDIN 常量(等同 fopen('php://stdin', 'r'))
$lineNumber = 0;
while (($line = fgets(STDIN)) !== false) {
$lineNumber++;
if (str_contains($line, 'ERROR')) {
fwrite(STDERR, "错误行 #{$lineNumber}: {$line}");
}
}实战示例
交互式输入工具
php
<?php
declare(strict_types=1);
class Prompt
{
/**
* 提示输入文本
*/
public static function ask(string $question, string $default = ''): string
{
$prompt = $default
? "{$question} [{$default}]: "
: "{$question}: ";
fwrite(STDOUT, $prompt);
$input = trim(fgets(STDIN) ?: '');
return $input !== '' ? $input : $default;
}
/**
* 确认提示(y/n)
*/
public static function confirm(string $question, bool $default = true): bool
{
$hint = $default ? '[Y/n]' : '[y/N]';
fwrite(STDOUT, "{$question} {$hint}: ");
$input = strtolower(trim(fgets(STDIN) ?: ''));
if ($input === '') {
return $default;
}
return in_array($input, ['y', 'yes', 'true', '1']);
}
/**
* 选择菜单
*/
public static function select(string $question, array $options, mixed $default = null): mixed
{
fwrite(STDOUT, "{$question}:\n");
$keys = array_keys($options);
foreach ($options as $key => $label) {
$marker = ($key === $default) ? ' (default)' : '';
fwrite(STDOUT, " {$key}. {$label}{$marker}\n");
}
fwrite(STDOUT, "请选择: ");
$input = trim(fgets(STDIN) ?: '');
if ($input === '' && $default !== null) {
return $default;
}
return in_array($input, $keys) ? $input : $default;
}
/**
* 密码输入(隐藏输入内容)
*/
public static function password(string $question = '密码: '): string
{
if (DIRECTORY_SEPARATOR === '\\') {
// Windows: 使用 PowerShell 隐藏输入
$command = 'powershell -Command "$p = Read-Host -AsSecureString; [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($p))"';
fwrite(STDOUT, $question);
$password = trim(shell_exec($command) ?: '');
} else {
// Unix: 使用 stty -echo 隐藏输入
fwrite(STDOUT, $question);
system('stty -echo');
$password = trim(fgets(STDIN) ?: '');
system('stty echo');
fwrite(STDOUT, "\n");
}
return $password;
}
}
// 使用示例
$name = Prompt::ask('你的名字', 'World');
$confirmed = Prompt::confirm('确认删除?', false);
$env = Prompt::select('选择环境', ['dev' => '开发', 'test' => '测试', 'prod' => '生产'], 'dev');
$pass = Prompt::password('请输入密码: ');
echo "名字: {$name}, 确认: " . ($confirmed ? '是' : '否') . ", 环境: {$env}\n";ANSI 终端颜色
php
<?php
declare(strict_types=1);
class ConsoleColor
{
// 前景色
public const BLACK = '30';
public const RED = '31';
public const GREEN = '32';
public const YELLOW = '33';
public const BLUE = '34';
public const MAGENTA = '35';
public const CYAN = '36';
public const WHITE = '37';
// 背景色
public const BG_BLACK = '40';
public const BG_RED = '41';
public const BG_GREEN = '42';
public const BG_YELLOW = '43';
public const BG_BLUE = '44';
// 样式
public const RESET = '0';
public const BOLD = '1';
public const DIM = '2';
public const UNDERLINE = '4';
public const BLINK = '5';
public const REVERSE = '7';
public static function colorize(string $text, string $fg = '', string $bg = '', string $style = ''): string
{
$codes = [];
if ($style) $codes[] = $style;
if ($fg) $codes[] = $fg;
if ($bg) $codes[] = $bg;
if (empty($codes)) {
return $text;
}
return "\033[" . implode(';', $codes) . "m{$text}\033[0m";
}
// 便捷方法
public static function red(string $text): string { return self::colorize($text, self::RED); }
public static function green(string $text): string { return self::colorize($text, self::GREEN); }
public static function yellow(string $text): string { return self::colorize($text, self::YELLOW); }
public static function blue(string $text): string { return self::colorize($text, self::BLUE); }
public static function bold(string $text): string { return self::colorize($text, '', '', self::BOLD); }
public static function dim(string $text): string { return self::colorize($text, '', '', self::DIM); }
public static function isTerminal(): bool
{
// 检测是否在真实终端中运行(非管道)
return (function_exists('posix_isatty') && posix_isatty(STDOUT))
|| (getenv('TERM') !== false);
}
}
// 使用示例
fwrite(STDOUT, ConsoleColor::green("成功: ") . "操作完成\n");
fwrite(STDOUT, ConsoleColor::red("失败: ") . "连接超时\n");
fwrite(STDOUT, ConsoleColor::yellow("警告: ") . "配置缺失\n");
fwrite(STDOUT, ConsoleColor::bold("标题: ") . "用户列表\n");
fwrite(STDERR, ConsoleColor::red("[ERROR]") . " 文件不存在\n");进度条
php
<?php
declare(strict_types=1);
class ProgressBar
{
private int $total;
private int $current = 0;
private int $width = 50;
private string $charDone = '=';
private string $charTodo = '-';
private ?int $startTime = null;
public function __construct(int $total, int $width = 50)
{
$this->total = $total;
$this->width = $width;
$this->startTime = time();
}
public function advance(int $step = 1): void
{
$this->current = min($this->current + $step, $this->total);
$this->render();
}
public function setMessage(string $message): void
{
$this->render($message);
}
private function render(?string $message = null): void
{
$percent = $this->total > 0 ? $this->current / $this->total : 0;
$done = (int) ($percent * $this->width);
$todo = $this->width - $done;
$bar = str_repeat($this->charDone, $done) . str_repeat($this->charTodo, $todo);
$percentStr = sprintf('%5.1f%%', $percent * 100);
// 预估剩余时间
$elapsed = time() - ($this->startTime ?? time());
$eta = $this->current > 0 ? ($elapsed / $this->current) * ($this->total - $this->current) : 0;
$etaStr = $eta > 0 ? gmdate('H:i:s', (int) $eta) : '--:--:--';
$suffix = $message ? " {$message}" : '';
$output = sprintf(" [%s] %s %d/%d ETA: %s%s", $bar, $percentStr, $this->current, $this->total, $etaStr, $suffix);
// 回到行首并清除到行尾
fwrite(STDOUT, "\r\033[K{$output}");
if ($this->current >= $this->total) {
fwrite(STDOUT, "\n");
}
}
public function finish(string $message = ' 完成'): void
{
$this->current = $this->total;
$this->render($message);
}
}
// 使用示例
$total = 100;
$bar = new ProgressBar($total);
for ($i = 0; $i <= $total; $i++) {
$bar->advance();
usleep(20000); // 模拟工作
}
// 输出:
// [==================================================] 100.0% 100/100 ETA: 00:00:00注意事项
输出缓冲
php
<?php
// CLI 模式下默认无输出缓冲
// echo/printf 立即输出到终端
// 但某些场景可能有缓冲:
// 1. 管道到其他程序时
// 2. php://output 包装器
// 3. ob_start() 手动开启
// 确保立即输出
echo "正在处理...\n";
ob_flush(); // 刷新 PHP 输出缓冲
flush(); // 刷新 Web 服务器缓冲(CLI 中无效果,但无害)
// 关闭所有输出缓冲
while (ob_get_level()) {
ob_end_clean();
}
// 设置无缓冲
ini_set('output_buffering', 'off');
ini_set('zlib.output_compression', false);非交互模式检测
php
<?php
// 检测是否在交互式终端中运行
if (function_exists('posix_isatty')) {
$isTerminal = posix_isatty(STDIN);
if (!$isTerminal) {
// 非交互模式 — 来自管道或重定向
// 不应该提示用户输入
fwrite(STDERR, "非交互模式,跳过用户输入\n");
}
}
// 通过环境变量检测
$isInteractive = (getenv('TERM') !== false) && (getenv('CI') === false);
// 不支持 posix 扩展时的替代方案
if (!function_exists('posix_isatty')) {
$isTerminal = (stream_isatty(STDIN));
}最佳实践
1. 日志输出规范
php
<?php
// CLI 日志格式规范
class CliLogger
{
public static function info(string $message): void
{
$time = date('H:i:s');
fwrite(STDOUT, " [{$time}] [INFO] {$message}\n");
}
public static function success(string $message): void
{
$time = date('H:i:s');
fwrite(STDOUT, ConsoleColor::green(" [{$time}] [OK] {$message}") . "\n");
}
public static function warning(string $message): void
{
$time = date('H:i:s');
fwrite(STDERR, ConsoleColor::yellow(" [{$time}] [WARN] {$message}") . "\n");
}
public static function error(string $message): void
{
$time = date('H:i:s');
fwrite(STDERR, ConsoleColor::red(" [{$time}] [ERROR] {$message}") . "\n");
}
}2. 信号处理与退出
php
<?php
// 优雅退出 — 处理 Ctrl+C
$shouldExit = false;
pcntl_signal(SIGINT, function () use (&$shouldExit) {
echo "\n收到退出信号,正在清理...\n";
$shouldExit = true;
});
pcntl_signal(SIGTERM, function () use (&$shouldExit) {
echo "\n收到终止信号,正在清理...\n";
$shouldExit = true;
});
while (!$shouldExit) {
// 执行任务
sleep(1);
}
echo "清理完成,退出\n";
exit(0);