Skip to content

标准输入输出

概述

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

参考链接