clue / commander
终于有了在PHP中注册可用命令和参数以及匹配命令行的一种合理方式。
Requires
- php: >=5.3
Requires (Dev)
- clue/arguments: ^1.0
- phpunit/phpunit: ^9.3 || ^5.7 || ^4.8.35
README
终于有了在PHP中注册可用命令和参数以及匹配命令行的一种合理方式。
您想在PHP中构建一个命令行界面(CLI)工具,该工具接受额外的参数,并希望将这些参数路由到单个函数?那么这个库就是为您准备的!
这对于交互式CLI工具或任何可以将命令行字符串拆分为命令行参数数组的地方也非常有用,您现在想根据给定的参数执行单个函数。
目录
支持我们
我们在开发、维护和更新我们出色的开源项目上投入了大量时间。您可以通过在GitHub上成为赞助者来帮助我们保持高质量的工作。赞助者将获得许多回报,有关详细信息,请参阅我们的赞助页面。
让我们将这些项目提升到下一个层次! 🚀
快速入门示例
以下示例代码演示了如何使用此库构建一个简单的命令行界面(CLI)工具,该工具接受传递给此程序的命令行参数
$router = new Clue\Commander\Router(); $router->add('exit [<code:uint>]', function (array $args) { exit(isset($args['code']) ? $args['code'] : 0); }); $router->add('sleep <seconds:uint>', function (array $args) { sleep($args['seconds']); }); $router->add('echo <words>...', function (array $args) { echo join(' ', $args['words']) . PHP_EOL; }); $router->add('[--help | -h]', function () use ($router) { echo 'Usage:' . PHP_EOL; foreach ($router->getRoutes() as $route) { echo ' ' .$route . PHP_EOL; } }); $router->execArgv();
请参阅示例。
用法
路由器
Router
是此包中的主要类。
它负责注册新的路由,将给定的参数与这些路由进行匹配,然后执行已注册的路由回调。
$router = new Router();
高级用法:
Router
接受一个可选的Tokenizer
实例作为构造函数的第一个参数。
add()
可以使用 add(string $route, callable $handler): Route
方法向此 Router 注册一个新的 Route
。
它接受一个用于匹配的路由表达式和一个当此路由表达式匹配时将执行的路由回调。
这非常类似于常见的PHP(微)框架提供的“HTTP路由器”,用于将传入的HTTP请求路由到相应的“控制器函数”。
$route = $router->add($path, $fn);
路由表达式使用一种自定义的领域特定语言(DSL),旨在使其尽可能简单,以便此库的消费者(即开发人员)和您工具的用户都能理解它们。
请注意,这是一个左结合语法(LAG),并且所有标记都是贪婪的。这意味着标记将从左到右处理,每个标记都将尽可能多地匹配输入参数。这意味着某些路由表达式几乎没有意义,例如,在省略号后的参数后有可选参数。有关更多详细信息,请参阅下面。
您可以使用空字符串来匹配未给出任何参数的情况
$router->add('', function() { echo 'No arguments given. Need help?' . PHP_EOL; }); // matches: (empty string) // does not match: hello (too many arguments)
您可以使用任何数量的静态关键字,如下所示
$router->add('user list', function () { echo 'Here are all our users…' . PHP_EOL; }); // matches: user list // does not match: user (missing required keyword) // does not match: user list hello (too many arguments)
您可以使用替代块来支持任何静态关键字,如下所示
$router->add('user (list | listing | ls)', function () { echo 'Here are all our users…' . PHP_EOL; }); // matches: user list // matches: user listing // matches: user ls // does not match: user (missing required keyword) // does not match: user list hello (too many arguments)
请注意,您可以在路由表达式的几乎任何标记处添加替代块。请注意,替代块不需要括号,并且替代标记(|
)始终在当前块级别上工作,这可能并不总是显而易见。除非您添加括号,否则默认情况下,a b | c d
将被解释为(a b) | (c d)
。括号可以用来将其解释为a (b | c) d
。特别是,您还可以将替代块与可选块(见下文)结合使用,以有条件地仅接受一个替代选项,但不能接受多个。
您可以使用任意数量的占位符来标记必需的参数,如下所示
$router->add('user add <name>', function (array $args) { assert(is_string($args['name'])); var_dump($args['name']); }); // matches: user add clue // does not match: user add (missing required argument) // does not match: user add hello world (too many arguments) // does not match: user add --test (argument looks like an option) // matches: user add -- clue (value: clue) // matches: user add -- --test (value: --test) // matches: user add -- -nobody- (value: -nobody-) // matches: user add -- -- (value: --)
请注意,以破折号(-
)开头的参数在用户输入中不会简单地接受,因为它们可能被误解为(可选的)选项(见下文)。如果用户希望处理以破折号(-
)开头的参数,他们必须使用过滤器(见下文)或可以使用双破折号分隔符(--
),因为在此分隔符之后的所有内容都将按原样处理。请参阅上面演示此行为的最后几个示例。
您可以使用预定义的过滤器来限制接受哪些值,如下所示
$router->add('user ban <id:int> <force:bool>', function (array $args) { assert(is_int($args['id'])); assert(is_bool($args['force'])); }); // matches: user ban 10 true // matches: user ban 10 0 // matches: user ban -10 yes // matches: user ban -- -10 no // does not match: user ban 10 (missing required argument) // does not match: user ban hello true (invalid value does not validate)
请注意,过滤器还会返回转换为正确数据类型的值。请注意,使用双破折号分隔符(--
)在匹配过滤值时是可选的。目前可用的预定义过滤器如下
int
接受任何正整数或负整数,例如10
或-4
uint
接受任何正整数(无符号),例如10
或0
float
接受任何正浮点数或负浮点数,例如1.5
或-2.3
ufloat
接受任何正浮点数(无符号),例如1.5
或0
bool
接受任何布尔值,例如yes/true/1
或no/false/0
如果您想添加自定义过滤器函数,请参阅下文的
Tokenizer
以获取高级用法。
您可以通过将参数括在方括号中来标记它们为可选的,如下所示
$router->add('user search [<query>]', function (array $args) { assert(!isset($args['query']) || is_string($args['query'])); var_dump(isset($args['query']); }); // matches: user search // matches: user search clue // does not match: user search hello world (too many arguments)
请注意,您可以在路由表达式的几乎任何标记处添加方括号,尽管它们最常用于上述参数或下文的可选选项。可选标记可以出现在路由表达式的任何位置,但请记住,标记将从左到右匹配,因此如果可选标记匹配,则其余部分将由后续标记处理。作为一个经验法则,请确保可选标记靠近路由表达式的末尾,这样您就不会注意到这种细微的影响。可选块接受替代组,所以[a | b]
实际上等同于较长的形式[(a | b)]
。特别是,这通常用于以下替代选项。
您可以通过追加省略号来接受任意数量的参数,如下所示
$router->add('user delete <names>...', function (array $args) { assert(is_array($args); assert(count($args) > 0); var_dump($args['names']); }); // matches: user delete clue // matches: user delete hello world // does not match: user delete (missing required argument)
请注意,您可以在路由表达式的任何参数、单词或选项标记后添加尾随省略号。它们最常用于上述参数。上述要求至少有一个参数,如果您想使其完全可选,请参阅以下内容。技术上,省略号标记可以出现在路由表达式的任何位置,但请记住,标记将从左到右匹配,因此如果省略号匹配,它将消耗所有输入参数,不会为后续标记留下任何内容。作为一个经验法则,请确保省略号标记靠近路由表达式的末尾,这样您就不会注意到这种细微的影响。
您可以通过在方括号内追加省略号来接受任意数量的可选参数,如下所示
$router->add('user dump [<names>...]', function (array $args) { if (isset($args['names'])) { assert(is_array($args); assert(count($args) > 0); var_dump($args['names']); } else { var_dump('no names'); } }); // matches: user dump // matches: user dump clue // matches: user dump hello world
以上无需任何参数,它可以与零个或多个参数一起工作。
您可以通过这种方式添加任意数量的可选短或长选项
$router->add('user list [--json] [-f]', function (array $args) { assert(!isset($args['json']) || $args['json'] === false); assert(!isset($args['f']) || $args['f'] === false); }); // matches: user list // matches: user list --json // matches: user list -f // matches: user list -f --json // matches: user -f list // matches: --json user list
如示例所示,在 $args
数组中的选项要么在用户输入未传递时未设置,要么在传递时设置为 false
(这与其他解析器例如 getopt()
的工作方式一致)。请注意,无论选项在哪里定义,选项都可以在任何用户输入参数中接受。请注意,在路由表达式中,方括号是必需的,用于标记此选项为可选,如果您确实想要一个必需的选项,也可以省略这些方括号。
您可以通过这种方式将短选项和长选项组合在可选块中
$router->add('user setup [--help | -h]', function (array $args) { assert(!isset($args['help']) || $args['help'] === false); assert(!isset($args['h']) || $args['h'] === false); assert(!isset($args['help'], $args['h']); }); // matches: user setup // matches: user setup --help // matches: user setup -h // does not match: user setup --help -h (only accept eithers, not both)
如示例所示,此选项可以选择性地在任何用户输入位置接受短选项或长选项,但永远不会同时接受两者。
您可以通过这种方式选择性地接受或要求短选项和长选项的值
$router->add('[--sort[=<param>]] [-i=<start:int>] user list', function (array $args) { assert(!isset($args['sort']) || $args['sort'] === false || is_string($args['sort'])); assert(!isset($args['i']) || is_int($args['i'])); }); // matches: user list // matches: user list --sort // matches: user list --sort=size // matches: user list --sort size // matches: user list -i=10 // matches: user list -i 10 // matches: user list -i10 // matches: user list -i=-10 // matches: user list -i -10 // matches: user list -i-10 // matches: user -i=10 list // matches: --sort -- user list // matches: --sort size user list // matches: user list --sort -i=10 // does not match: user list -i (missing option value) // does not match: user list -i --sort (missing option value) // does not match: user list -i=a (invalid value does not validate) // does not match: --sort user list (user will be interpreted as option value) // does not match: user list --sort -2 (value looks like an option)
如示例所示,在 $args
数组中的选项值将作为字符串或其过滤和转换的值给出,如果它们在用户输入中传递。短选项和长选项都可以接受带有推荐等号符号语法(分别使用 -i=10
和 --sort=size
)的用户输入值。短选项和长选项也可以接受带有常见空格分隔语法的值(分别使用 -i 10
和 --sort size
)。短选项还可以接受带有无分隔符的常见连接语法(例如 -i10
)的用户输入值。请注意,强烈建议始终确保接受值的任何选项都靠近您的路由表达式左侧。这是为了确保空格分隔的值被视为选项值,而不是被误解释为关键字或参数。
您可以通过这种方式限制短选项和长选项的值为给定的预设
$router->add('[--ask=(yes | no)] [-l[=0]] user purge', function (array $args) { assert(!isset($args['ask']) || $args['sort'] === 'yes' || $args['sort'] === 'no'); assert(!isset($args['l']) || $args['l'] === '0'); }); // matches: user purge // matches: user purge --ask=yes // matches: user purge --ask=no // matches: user purge -l // matches: user purge -l=0 // matches: user purge -l 0 // matches: user purge -l0 // matches: user purge -l --ask=no // does not match: user purge --ask (missing option value) // does not match: user purge --ask=maybe (invalid option value) // does not match: user purge -l4 (invalid option value)
如示例所示,可以通过使用上述任何标记将选项值限制为给定的预设值。技术上,使用上述任何标记来限制选项值是有效的。在实际应用中,这主要用于静态关键字标记或其替代组。建议始终使用括号表示可选组,尽管它们在具有可选值的选项中不是严格必需的。这也使得 [--ask=(yes | no)]
接受任一选项值更为明显,而(不太有用)的表达式 [--ask=yes | no]
会接受选项 --ask=yes
或静态关键字 no
。
remove()
remove(Route $route): void
方法可以用来从注册的路由中移除指定的 Route
对象。
$route = $router->add('hello <name>', $fn); $router->remove($route);
如果给定的路由不存在,它将抛出 UnderflowException
。
getRoutes()
getRoutes(): Route[]
方法可以用来返回所有注册的 Route
对象的数组。
echo 'Usage help:' . PHP_EOL; foreach ($router->getRoutes() as $route) { echo $route . PHP_EOL; }
如果您尚未添加任何路由,此数组将为空。
execArgv()
execArgv(array $argv = null): void
方法可以通过匹配 argv
与所有注册的路由并退出来实现执行。
您可以显式传入您的 $argv
或它将自动使用来自 $_SERVER
超全局的值。该 argv
是一个数组,它始终以调用程序作为第一个元素开始。我们简单地忽略这个第一个元素,然后根据注册的路由处理剩余的元素。
这是一个便利的方法,它将匹配并执行一个路由,然后退出程序而不会返回。
如果找不到路由或者路由回调抛出异常,它将在STDERR输出错误信息并设置适当的非零退出码。
请注意,这只是为了方便,并且仅适用于所有程序中最简单的情况。如果您需要更多控制,请考虑使用底层的 handleArgv()
方法并自行处理任何错误情况。
handleArgv()
handleArgv(array $argv = null): mixed
方法可以通过匹配 argv
与所有已注册的路由来执行,然后返回。
您可以显式传入您的 $argv
或它将自动使用来自 $_SERVER
超全局的值。该 argv
是一个数组,它始终以调用程序作为第一个元素开始。我们简单地忽略这个第一个元素,然后根据注册的路由处理剩余的元素。
与 execArgv()
不同,此方法将尝试执行路由回调,并返回路由回调返回的内容。
$router->add('hello <name>', function (array $args) { return strlen($args[$name]); }); $length = $router->handleArgv(array('program', 'hello', 'test')); assert($length === 4);
如果找不到路由,它将抛出 NoRouteFoundException
。
// throws NoRouteFoundException $router->handleArgv(array('program', 'invalid'));
如果路由回调抛出 Exception
,它将传递此 Exception
。
$router->add('hello <name>', function (array $args) { if ($args['name'] === 'admin') { throw new InvalidArgumentException(); } return strlen($args['name']); }); // throws InvalidArgumentException $router->handleArgv(array('program', 'hello', 'admin'));
handleArgs()
handleArgs(array $args): mixed
方法可以通过将给定的参数与所有已注册的路由进行匹配来执行,然后返回。
与 handleArgv()
不同,此方法将使用完整的 $args
数组来匹配已注册的路由(即它不会忽略第一个元素)。如果您自己构建此数组或使用交互式命令行界面(CLI)并要求用户提供参数,这将特别有用。
$router->add('hello <name>', function (array $args) { return strlen($args[$name]); }); $length = $router->handleArgs(array('hello', 'test')); assert($length === 4);
参数必须作为单个元素的数组给出。如果您只有一个要拆分为单个命令行参数数组的命令行字符串,请考虑使用 clue/arguments。
$line = fgets(STDIN, 2048); assert($line === 'hello "Christian Lück"'); $args = Clue\Arguments\split($line); assert($args === array('hello', 'Christian Lück')); $router->handleArgs($args);
如果找不到路由,它将抛出 NoRouteFoundException
。
// throws NoRouteFoundException $router->handleArgs(array('invalid'));
如果路由回调抛出 Exception
,它将传递此 Exception
。
$router->add('hello <name>', function (array $args) { if ($args['name'] === 'admin') { throw new InvalidArgumentException(); } return strlen($args['name']); }); // throws InvalidArgumentException $router->handleArgs(array('hello', 'admin'));
路由
Route
表示 Router 中的单个已注册路由。
它包含匹配所需的路线令牌和如果此路线匹配则要执行的路线回调。
请参阅 Router
。
NoRouteFoundException
NoRouteFoundException
会在 handleArgv()
或 handleArgs()
中找不到匹配的路由时引发。它扩展了 PHP 的内置 RuntimeException
。
令牌化器
Tokenizer
类负责将路由表达式解析为有效的令牌实例。此类主要用于内部,您在大多数情况下不必担心。
如果您需要为您的路由表达式自定义逻辑,您可以将您的 Tokenizer
实例显式传递给 Router
构造函数。
$tokenizer = new Tokenizer(); $router = new Router($tokenizer);
addFilter()
可以使用 addFilter(string $name, callable $filter): void
方法添加自定义过滤器函数。
然后可以使用过滤器名称在参数或选项表达式中,例如 add <name:lower>
或 --search=<address:ip>
。
过滤器函数将以过滤器值调用,如果此过滤器接受给定值,则必须返回布尔成功值。过滤器值将通过引用传递,因此如果过滤成功,可以更新它。
$tokenizer = new Tokenizer(); $tokenizer->addFilter('ip', function ($value) { return filter_var($ip, FILTER_VALIDATE_IP); }); $tokenizer->addFilter('lower', function (&$value) { $value = strtolower($value); return true; }); $router = new Router($tokenizer); $router->add('add <name:lower>', function ($args) { }); $router->add('--search=<address:ip>', function ($args) { });
安装
安装此库的推荐方式是通过 Composer。 Composer 是什么?
此项目遵循 SemVer。这将安装最新支持的版本。
$ composer require clue/commander:^1.4
有关版本升级的详细信息,请参阅 变更日志。
此项目旨在在任何平台上运行,因此不需要任何 PHP 扩展,并支持在从 PHP 5.3 到当前 PHP 8+ 和 HHVM 的旧版 PHP 上运行。强烈建议为此项目使用 PHP 7+。
测试
要运行测试套件,首先需要克隆此仓库,然后通过Composer安装所有依赖项(Composer)
$ composer install
要运行测试套件,请转到项目根目录并运行
$ php vendor/bin/phpunit
许可
本项目采用宽松的MIT许可协议发布。
你知道吗?我提供定制开发服务,并可以为发布和贡献发放发票。有关详情请联系我(@clue)。
更多
- 如果你想要构建一个交互式命令行工具,你可能需要考虑使用clue/reactphp-stdio来响应用户从标准输入(STDIN)发出的命令。
- 如果你构建了一个从标准输入(STDIN)读取命令行的交互式命令行工具,你可能需要使用clue/arguments将此字符串分割成单独的参数。