命令行参数解析:从入门到实战
说实话,命令行参数解析这事儿,看着简单,但坑不少。我刚开始写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()适合简单场景,但不够灵活- 自定义解析器可以完全控制,适合复杂需求
- 实战中建议封装成类,方便复用
嗯,这一章就到这儿。下一章咱们聊聊如何让命令行工具的输出更漂亮——颜色、进度条、表格格式化,这些实用技巧一个都不能少。