Skip to content

$argc 与 $argv — CLI 参数

概述

$argc$argv 是 PHP 中专门用于命令行(CLI)模式的预定义变量。$argv 是一个包含命令行参数的数组,$argc 则是该数组的元素个数(argument count)。这两个变量使得 PHP 脚本能够接收和处理来自终端或 cron 任务的外部参数,是编写 PHP CLI 工具、脚本和定时任务的基础。

前置知识

在阅读本节之前,你需要了解:

  • PHP CLI 模式的基本概念和运行方式
  • 命令行终端的基本操作
  • Shell 脚本的参数传递方式
  • getopt() 函数的基本用法

基础概念

$argv 的结构

$argv 是一个索引数组,包含传递给脚本的所有命令行参数:

bash
php script.php alice bob --verbose --output=result.txt

对应的 $argv 数组:

$argv = [
    0 => 'script.php',         // 脚本名称(始终存在)
    1 => 'alice',              // 第一个参数
    2 => 'bob',                // 第二个参数
    3 => '--verbose',          // 第三个参数
    4 => '--output=result.txt', // 第四个参数
];

$argc = 5; // 数组元素总数

$argc 与 $argv 的关系

变量类型说明
$argvarray命令行参数数组,$argv[0] 始终是脚本名称
$argcint参数个数,等于 count($argv)

Web 模式下的可用性

在 Web 模式(Apache/Nginx + PHP-FPM)下,$argv$argc 的行为取决于 register_argc_argv 配置(默认关闭)。在 CLI 模式下它们始终可用。强烈建议仅在 CLI 脚本中使用这两个变量。

语法与代码

基本参数读取

php
<?php
declare(strict_types=1);

// 脚本: greet.php
// 运行: php greet.php Alice 28

// $argv[0] = 'greet.php'
// $argv[1] = 'Alice'
// $argv[2] = '28'
// $argc = 3

if ($argc < 3) {
    echo "用法: php {$argv[0]} <姓名> <年龄>" . PHP_EOL;
    echo "示例: php {$argv[0]} 张三 25" . PHP_EOL;
    exit(1);
}

$name = $argv[1];
$age  = (int)$argv[2];

echo "你好, {$name}! 你 {$age} 岁了。" . PHP_EOL;

在函数中使用 $argv 和 $argc

php
<?php
declare(strict_types=1);

// $argv 和 $argc 不是超全局变量
// 在函数内需要通过 global 关键字或 $GLOBALS 访问

// 方式一:使用 global 关键字
function getScriptName(): string
{
    global $argv;
    return $argv[0];
}

// 方式二:使用 $GLOBALS
function getArgCount(): int
{
    return $GLOBALS['argc'];
}

// 方式三:作为参数传递(推荐)
function processArgs(array $argv, int $argc): void
{
    if ($argc < 2) {
        echo "请提供参数" . PHP_EOL;
        return;
    }

    for ($i = 1; $i < $argc; $i++) {
        echo "参数 {$i}: {$argv[$i]}" . PHP_EOL;
    }
}

processArgs($argv, $argc);

使用 getopt() 解析选项

php
<?php
declare(strict_types=1);

/**
 * 使用 getopt() 解析命令行选项
 *
 * 用法: php script.php -f value -b -v --name=value --help
 */

// 短选项: 'f:' (f 后跟冒号表示需要值)
//           'b'  (b 不跟冒号表示布尔标志)
// 长选项: ['name:', 'help', 'verbose']
$options = getopt('f:bv', ['name:', 'help', 'verbose']);

// 获取选项值
$file    = $options['f'] ?? $options['name'] ?? 'default.txt';
$bool    = isset($options['b']);
$verbose = isset($options['v']) || isset($options['verbose']);
$help    = isset($options['help']);

if ($help) {
    echo "用法: php {$argv[0]} [-f file] [-b] [-v] [--name=value] [--help]" . PHP_EOL;
    echo "  -f, --name    指定文件名" . PHP_EOL;
    echo "  -b            启用布尔模式" . PHP_EOL;
    echo "  -v, --verbose 启用详细输出" . PHP_EOL;
    echo "  --help        显示帮助信息" . PHP_EOL;
    exit(0);
}

echo "文件: {$file}";
echo "布尔: " . ($bool ? 'true' : 'false');
echo "详细: " . ($verbose ? 'true' : 'false');

getopt() 选项定义规则

格式说明示例
'a'不需要值的短选项-a
'a:'必须有值的短选项-a value-avalue
'a::'可选值的短选项(PHP 5.3+)-a-a value
'abc'多个短选项可组合-abc 等价于 -a -b -c
php
<?php
declare(strict_types=1);

// 完整的选项定义示例
$shortOpts  = '';
$shortOpts .= 'f:';     // -f <value> 必需值
$shortOpts .= 'v::';    // -v [value] 可选值
$shortOpts .= 'abc';    // -a, -b, -c 布尔标志
$shortOpts .= 'p:';     // -p <value> 必需值

$longOpts  = [
    'file:',       // --file <value> 必需值
    'verbose::',   // --verbose [value] 可选值
    'help',        // --help 布尔标志
    'output:',     // --output <value> 必需值
    'dry-run',     // --dry-run 布尔标志(含连字符)
];

$options = getopt($shortOpts, $longOpts);
print_r($options);

详细说明

getopt() 的局限性

  1. 选项后的剩余参数getopt() 只解析选项,不会返回选项之后的非选项参数
php
<?php
declare(strict_types=1);

// 命令行: php script.php --file=test.txt file1.txt file2.txt
// getopt() 只返回 ['file' => 'test.txt']
// file1.txt 和 file2.txt 不会出现在 getopt() 结果中

// 解决方案:手动从 $argv 中提取剩余参数
function getRemainingArgs(array $argv): array
{
    // getopt() 会修改 $argv,移除已解析的选项
    // 但这个行为在 PHP 不同版本中可能不一致
    $remaining = [];

    // 更可靠的方式:跳过脚本名和选项
    $skipNext = false;
    for ($i = 1; $i < count($argv); $i++) {
        if ($skipNext) {
            $skipNext = false;
            continue;
        }
        if (str_starts_with($argv[$i], '-')) {
            // 跳过选项值(如 -f value 或 --file=value)
            if (!str_contains($argv[$i], '=') && isset($argv[$i + 1]) && !str_starts_with($argv[$i + 1], '-')) {
                $skipNext = true;
            }
            continue;
        }
        $remaining[] = $argv[$i];
    }

    return $remaining;
}
  1. 同一选项多次出现getopt() 默认只保留最后一个值
php
<?php
declare(strict_types=1);

// 命令行: php script.php --exclude=old --exclude=temp --exclude=backup
// 默认 getopt() 只返回 ['exclude' => 'backup']

// 解决方案:使用独立函数解析多值选项
function parseMultiValueOption(array $argv, string $option): array
{
    $values = [];
    foreach ($argv as $arg) {
        if (str_starts_with($arg, "--{$option}=")) {
            $values[] = substr($arg, strlen("--{$option}="));
        }
    }
    return $values;
}

$excludes = parseMultiValueOption($argv, 'exclude');
// ['old', 'temp', 'backup']

在 cron 任务中使用 CLI 参数

php
<?php
declare(strict_types=1);

/**
 * 数据库备份脚本
 *
 * Cron 配置示例:
 * 0 2 * * * /usr/bin/php /var/www/scripts/backup.php --db=production --compress
 */

$options = getopt('', ['db:', 'compress', 'output:', 'verbose']);

$db       = $options['db'] ?? 'default';
$compress = isset($options['compress']);
$output   = $options['output'] ?? "/tmp/backup_{$db}_" . date('Ymd') . '.sql';
$verbose  = isset($options['verbose']);

echo "开始备份数据库: {$db}" . PHP_EOL;

if ($verbose) {
    echo "输出文件: {$output}" . PHP_EOL;
    echo "压缩: " . ($compress ? '是' : '否') . PHP_EOL;
}

// 模拟备份逻辑
$sql = "-- Backup of {$db} at " . date('Y-m-d H:i:s') . "\n";
$sql .= "CREATE DATABASE IF NOT EXISTS {$db};\n";

if ($compress && file_put_contents($output . '.gz', gzencode($sql))) {
    echo "备份完成(已压缩): {$output}.gz" . PHP_EOL;
} elseif (file_put_contents($output, $sql)) {
    echo "备份完成: {$output}" . PHP_EOL;
} else {
    echo "备份失败!" . PHP_EOL;
    exit(1);
}

实战示例

完整的 CLI 命令行工具

php
<?php
declare(strict_types=1);

/**
 * 通用 CLI 命令行工具框架
 *
 * 用法:
 *   php tool.php <command> [options] [arguments]
 *
 * 命令:
 *   list      列出所有项目
 *   create    创建新项目
 *   delete    删除项目
 *   export    导出项目数据
 *
 * 选项:
 *   -h, --help       显示帮助
 *   -v, --verbose    详细输出
 *   -f, --force      强制执行
 *   -o, --output     指定输出文件
 */

class CliApp
{
    private array $options;
    private array $arguments;
    private array $commands;
    private bool $verbose = false;

    public function __construct(array $argv, int $argc)
    {
        // 解析选项
        $this->options = getopt('hvf:o:', ['help', 'verbose', 'force', 'output:']);

        // 提取命令和参数
        $this->arguments = $this->extractArguments($argv);

        // 设置标志
        $this->verbose = isset($this->options['v']) || isset($this->options['verbose']);

        // 注册命令
        $this->registerCommands();
    }

    public function run(): int
    {
        // 显示帮助
        if (isset($this->options['h']) || isset($this->options['help']) || empty($this->arguments)) {
            $this->showHelp();
            return 0;
        }

        $command = $this->arguments[0];

        if (!isset($this->commands[$command])) {
            echo "错误: 未知命令 '{$command}'" . PHP_EOL;
            echo "运行 '{$this->getScriptName()} --help' 查看可用命令" . PHP_EOL;
            return 1;
        }

        return ($this->commands[$command])(array_slice($this->arguments, 1));
    }

    private function extractArguments(array $argv): array
    {
        $args = [];
        $skipNext = false;

        for ($i = 1; $i < count($argv); $i++) {
            if ($skipNext) {
                $skipNext = false;
                continue;
            }

            $arg = $argv[$i];

            // 跳过选项及其值
            if (str_starts_with($arg, '-') || str_starts_with($arg, '--')) {
                // 检查选项是否需要值(不含 = 的情况)
                if (!str_contains($arg, '=') && isset($argv[$i + 1]) && !str_starts_with($argv[$i + 1], '-')) {
                    $skipNext = true;
                }
                continue;
            }

            $args[] = $arg;
        }

        return $args;
    }

    private function registerCommands(): void
    {
        $this->commands = [
            'list' => function (array $args): int {
                $this->log("列出所有项目:");
                foreach (['项目A', '项目B', '项目C'] as $i => $project) {
                    echo "  " . ($i + 1) . ". {$project}" . PHP_EOL;
                }
                return 0;
            },
            'create' => function (array $args): int {
                $name = $args[0] ?? null;
                if ($name === null) {
                    echo "错误: 请指定项目名称" . PHP_EOL;
                    return 1;
                }
                $this->log("创建项目: {$name}");
                echo "项目 '{$name}' 创建成功!" . PHP_EOL;
                return 0;
            },
            'delete' => function (array $args): int {
                $name = $args[0] ?? null;
                $force = isset($this->options['f']) || isset($this->options['force']);
                if ($name === null) {
                    echo "错误: 请指定要删除的项目" . PHP_EOL;
                    return 1;
                }
                if (!$force) {
                    echo "确认删除项目 '{$name}'? (y/n): ";
                    $confirm = trim(fgets(STDIN));
                    if (strtolower($confirm) !== 'y') {
                        echo "已取消" . PHP_EOL;
                        return 0;
                    }
                }
                $this->log("删除项目: {$name}");
                return 0;
            },
        ];
    }

    private function showHelp(): void
    {
        $script = $this->getScriptName();
        echo "用法: php {$script} <命令> [选项] [参数]" . PHP_EOL;
        echo PHP_EOL;
        echo "命令:" . PHP_EOL;
        foreach (array_keys($this->commands) as $cmd) {
            echo "  {$cmd}" . PHP_EOL;
        }
        echo PHP_EOL;
        echo "选项:" . PHP_EOL;
        echo "  -h, --help      显示帮助" . PHP_EOL;
        echo "  -v, --verbose   详细输出" . PHP_EOL;
        echo "  -f, --force     强制执行" . PHP_EOL;
        echo "  -o, --output    输出文件" . PHP_EOL;
    }

    private function getScriptName(): string
    {
        global $argv;
        return basename($argv[0]);
    }

    private function log(string $message): void
    {
        if ($this->verbose) {
            echo "[LOG] {$message}" . PHP_EOL;
        }
    }
}

// 运行应用
$app = new CliApp($argv, $argc);
exit($app->run());

注意事项

1. Web 模式下不可用

php
<?php
declare(strict_types=1);

// 检测运行模式
if (PHP_SAPI !== 'cli') {
    die('此脚本只能在 CLI 模式下运行');
}

2. 参数中的特殊字符处理

Shell 会对参数中的特殊字符进行解释,使用引号包裹可避免:

bash
# 不使用引号:Shell 会展开通配符
php script.php *.txt     # 实际传递的是展开后的文件名列表

# 使用引号:原样传递
php script.php "*.txt"   # 传递字面字符串 *.txt

# 传递含空格的参数
php script.php "hello world"    # 正确:传递一个参数 "hello world"
php script.php hello world      # 错误:传递两个参数 hello 和 world

3. 参数安全性

php
<?php
declare(strict_types=1);

// 始终验证和清理 CLI 参数
$fileName = $argv[1] ?? '';
if (!preg_match('/^[a-zA-Z0-9_\-\.]+$/', $fileName)) {
    echo "错误: 文件名包含非法字符" . PHP_EOL;
    exit(1);
}

最佳实践

  1. 检测运行模式:始终检查 PHP_SAPI === 'cli'
  2. 提供帮助信息:支持 -h/--help 显示用法
  3. 使用 getopt():对复杂参数使用 getopt() 解析
  4. 参数验证:对所有输入参数进行验证
  5. 退出码规范:成功返回 0,失败返回非零值
  6. 错误输出到 stderr:使用 fwrite(STDERR, ...) 输出错误信息
  7. 作为参数传递:将 $argv$argc 作为函数参数,不依赖 global
php
<?php
declare(strict_types=1);

// 最佳实践模板:CLI 脚本
if (PHP_SAPI !== 'cli') {
    fwrite(STDERR, "此脚本只能在 CLI 模式下运行\n");
    exit(1);
}

$options = getopt('hvf:o:', ['help', 'verbose', 'force', 'output:']);

if (isset($options['h']) || isset($options['help'])) {
    echo "用法: php {$argv[0]} [选项] <参数>" . PHP_EOL;
    exit(0);
}

$verbose = isset($options['v']) || isset($options['verbose']);
$output  = $options['o'] ?? $options['output'] ?? 'php://stdout';

if ($verbose) {
    fwrite(STDERR, "详细模式已启用\n");
}

下一节

至此,预定义变量部分全部完成。下一节将进入类型系统部分,首先了解 PHP 类型系统的整体架构。

参考链接