命令行参数解析:从入门到实战

说实话,命令行参数解析这事儿,看着简单,但坑不少。我刚开始写PHP命令行工具时,就吃过不少亏。今天咱们把这部分彻底讲透。

$argv 和 $argc:最基础的参数获取

这两个变量是PHP内置的,不用任何配置就能用。说白了,$argc 是参数个数,$argv 是参数数组。

<?php
// demo.php
echo "参数个数: " . $argc . PHP_EOL;
print_r($argv);

运行一下看看:

$ php demo.php foo bar --name=zhangsan
参数个数: 4
Array
(
    [0] => demo.php
    [1] => foo
    [2] => bar
    [3] => --name=zhangsan
)
小提示:$argv[0] 永远是脚本文件名。我见过新手直接拿 $argv[1] 当第一个参数,结果发现不对——其实第一个参数是从 $argv[1] 开始的。

嗯,这里要注意:$argv$argc 只在 CLI 模式下可用。你在 Web 环境下用,会报错。

getopt() 函数详解

光用 $argv 手动解析太累了。PHP 提供了 getopt(),这才是正经的解析方式。

基本用法

<?php
$options = getopt("a:b:c");
// -a 后面跟值,-b 后面跟值,-c 是开关选项(无值)
print_r($options);

运行:

$ php test.php -a hello -b world -c
Array
(
    [a] => hello
    [b] => world
    [c] => false  // 注意:开关选项返回 false
)
避坑指南:我曾经被这个坑过——开关选项(不带值的)返回的是 false 而不是 true。你判断的时候要用 isset() 或者 === false,别用 if($options['c']) 这种写法。

长选项支持

从 PHP 5.3 开始,getopt() 支持长选项了:

<?php
$shortopts = "f:v";
$longopts = [
    "file:",    // --file 后面跟值
    "verbose",  // --verbose 开关
    "output:",  // --output 后面跟值
];
$options = getopt($shortopts, $longopts);
print_r($options);

运行:

$ php test.php -f data.txt --verbose --output=result.log
Array
(
    [f] => data.txt
    [verbose] => false
    [output] => result.log
)
语法 含义 示例
单个字符 短选项 a, b, c
字符后跟 : 该选项必须带值 a:, b:
字符后跟 :: 选项可选值(可省略) a::
长选项 -- 开头的选项 --file, --verbose
个人经验:我习惯把短选项和长选项都定义上。比如 -f 和 --file 同时支持,用户用哪个都行。这样工具用起来更顺手。

自定义参数解析器

getopt() 虽然好用,但有些场景不够灵活。比如你想支持 --name=value--name value 两种写法,或者想处理子命令。这时候就得自己写了。

我自己写过一个简单的解析器,核心思路就是遍历 $argv

<?php
function parseArgs($args) {
    $parsed = [];
    $count = count($args);
    
    for ($i = 1; $i < $count; $i++) {
        $arg = $args[$i];
        
        // 处理 --name=value 格式
        if (strpos($arg, '=') !== false) {
            list($key, $value) = explode('=', $arg, 2);
            $key = ltrim($key, '-');
            $parsed[$key] = $value;
            continue;
        }
        
        // 处理 --name value 格式
        if (substr($arg, 0, 2) === '--') {
            $key = substr($arg, 2);
            // 看下一个参数是不是值
            if ($i + 1 < $count && substr($args[$i + 1], 0, 1) !== '-') {
                $i++;
                $parsed[$key] = $args[$i];
            } else {
                $parsed[$key] = true; // 开关选项
            }
            continue;
        }
        
        // 处理 -v 短选项
        if (substr($arg, 0, 1) === '-') {
            $key = substr($arg, 1);
            if ($i + 1 < $count && substr($args[$i + 1], 0, 1) !== '-') {
                $i++;
                $parsed[$key] = $args[$i];
            } else {
                $parsed[$key] = true;
            }
            continue;
        }
        
        // 普通参数(没有 - 前缀的)
        $parsed['_arguments'][] = $arg;
    }
    
    return $parsed;
}

// 使用示例
$args = parseArgs($argv);
print_r($args);
核心思路:遍历参数数组,根据前缀判断是短选项、长选项还是普通参数。注意处理边界情况,比如最后一个参数是选项时不要越界。

实战:构建一个简单的参数解析工具

好了,理论讲完了。咱们动手写个完整的工具。这个工具要支持:

  • 短选项(-v, -f file)
  • 长选项(--verbose, --file=path)
  • 子命令(command subcommand)
  • 帮助信息(--help 或 -h)
<?php
class ArgParser {
    private $commands = [];
    private $options = [];
    private $helpText = '';
    
    public function addCommand($name, $callback, $description = '') {
        $this->commands[$name] = [
            'callback' => $callback,
            'description' => $description
        ];
    }
    
    public function addOption($short, $long, $hasValue = false, $description = '') {
        $this->options[] = [
            'short' => $short,
            'long' => $long,
            'hasValue' => $hasValue,
            'description' => $description
        ];
    }
    
    public function parse($argv) {
        $args = array_slice($argv, 1);
        $parsed = [
            'command' => null,
            'options' => [],
            'arguments' => []
        ];
        
        $count = count($args);
        for ($i = 0; $i < $count; $i++) {
            $arg = $args[$i];
            
            // 处理 --help 或 -h
            if ($arg === '--help' || $arg === '-h') {
                $this->showHelp();
                exit(0);
            }
            
            // 处理长选项
            if (substr($arg, 0, 2) === '--') {
                $this->parseLongOption($arg, $args, $i, $parsed);
                continue;
            }
            
            // 处理短选项
            if (substr($arg, 0, 1) === '-') {
                $this->parseShortOption($arg, $args, $i, $parsed);
                continue;
            }
            
            // 第一个非选项参数是命令
            if ($parsed['command'] === null && isset($this->commands[$arg])) {
                $parsed['command'] = $arg;
            } else {
                $parsed['arguments'][] = $arg;
            }
        }
        
        // 执行命令
        if ($parsed['command'] && isset($this->commands[$parsed['command']])) {
            $callback = $this->commands[$parsed['command']]['callback'];
            $callback($parsed['options'], $parsed['arguments']);
        } else {
            echo "未知命令: " . $parsed['command'] . PHP_EOL;
            $this->showHelp();
        }
    }
    
    private function parseLongOption($arg, $args, &$i, &$parsed) {
        $key = substr($arg, 2);
        if (strpos($key, '=') !== false) {
            list($key, $value) = explode('=', $key, 2);
            $parsed['options'][$key] = $value;
        } else {
            // 检查是否需要值
            foreach ($this->options as $opt) {
                if ($opt['long'] === $key && $opt['hasValue']) {
                    if ($i + 1 < $count) {
                        $i++;
                        $parsed['options'][$key] = $args[$i];
                    }
                    return;
                }
            }
            $parsed['options'][$key] = true;
        }
    }
    
    private function parseShortOption($arg, $args, &$i, &$parsed) {
        $key = substr($arg, 1);
        foreach ($this->options as $opt) {
            if ($opt['short'] === $key && $opt['hasValue']) {
                if ($i + 1 < $count) {
                    $i++;
                    $parsed['options'][$key] = $args[$i];
                }
                return;
            }
        }
        $parsed['options'][$key] = true;
    }
    
    public function showHelp() {
        echo $this->helpText . PHP_EOL;
        echo "可用命令:" . PHP_EOL;
        foreach ($this->commands as $name => $cmd) {
            echo "  " . str_pad($name, 15) . $cmd['description'] . PHP_EOL;
        }
        echo PHP_EOL . "可用选项:" . PHP_EOL;
        foreach ($this->options as $opt) {
            $short = $opt['short'] ? "-" . $opt['short'] : "";
            $long = $opt['long'] ? "--" . $opt['long'] : "";
            echo "  " . str_pad($short . ", " . $long, 25) . $opt['description'] . PHP_EOL;
        }
    }
    
    public function setHelpText($text) {
        $this->helpText = $text;
    }
}

// 使用示例
$parser = new ArgParser();
$parser->setHelpText("我的命令行工具 v1.0");

$parser->addCommand('init', function($options, $args) {
    echo "执行 init 命令..." . PHP_EOL;
    echo "选项: " . json_encode($options) . PHP_EOL;
    echo "参数: " . json_encode($args) . PHP_EOL;
}, '初始化项目');

$parser->addCommand('build', function($options, $args) {
    echo "执行 build 命令..." . PHP_EOL;
    if (isset($options['output'])) {
        echo "输出到: " . $options['output'] . PHP_EOL;
    }
}, '构建项目');

$parser->addOption('v', 'verbose', false, '显示详细信息');
$parser->addOption('o', 'output', true, '指定输出文件');

$parser->parse($argv);
避坑指南:我曾经在解析参数时忘了处理 -- 分隔符。按照惯例,-- 后面的所有内容都应该当作普通参数,即使它们以 - 开头。这个细节很容易被忽略。

运行效果:

$ php tool.php init -v --output=result.log
执行 init 命令...
选项: {"v":true,"output":"result.log"}
参数: []

$ php tool.php build --help
我的命令行工具 v1.0
可用命令:
  init            初始化项目
  build           构建项目

可用选项:
  -v, --verbose   显示详细信息
  -o, --output    指定输出文件

你想想看,有了这个工具,以后写命令行程序就轻松多了。只需要定义命令和选项,解析逻辑完全复用。我个人习惯把这种基础工具类放在项目的 src/ 目录下,所有命令行工具都继承或使用它。

总结一下:
  • $argv$argc 是最基础的,但手动解析太累
  • getopt() 适合简单场景,但不够灵活
  • 自定义解析器可以完全控制,适合复杂需求
  • 实战中建议封装成类,方便复用

嗯,这一章就到这儿。下一章咱们聊聊如何让命令行工具的输出更漂亮——颜色、进度条、表格格式化,这些实用技巧一个都不能少。