vanilla/garden-cli

一个功能全面但极其简单的命令行解析器,适用于您下一个PHP CLI脚本。停止与getopt()战斗。

v4.0 2023-01-24 20:33 UTC

README

Build Status Packagist Version MIT License CLA

介绍

Garden CLI是一个PHP命令行界面库,旨在提供一个干净简单的API,提供全面的功能。

为什么使用Garden CLI?

PHP的getopt()提供的功能很少,并且容易出错,如果命令行选项中有一个打字错误,可能会导致整个命令调用失败。Garden CLI解决了这个问题,并提供了额外的功能。

  • 您的命令将自动支持--help以打印命令的帮助信息。
  • 支持单个命令或多条命令。(例如,git pull,git push等)
  • 命令选项将自动解析和验证,并自动打印错误信息。
  • 简单优雅的语法,即使是最基础的命令行脚本也只需少许努力即可实现健壮的解析。

安装

Garden CLI需要PHP 8.1或更高版本

Garden CLI符合PSR-4规范,并可以使用composer安装。只需将vanilla/garden-cli添加到您的composer.json文件中。

"require": {
    "vanilla/garden-cli": "~4.0"
}

定义CLI

Cli类提供了一个流畅的接口来定义命令、选项和参数。

基本示例

以下是一个基本示例,说明如何使用Garden CLI解析其选项的命令行脚本。假设您正在编写一个名为dbdump.php的脚本,用于从数据库中导出一些数据。

<?php
// All of the command line classes are in the Garden\Cli namespace.
use Garden\Cli\Cli;

// Require composer's autoloader.
require_once 'vendor/autoload.php';

// Define the cli options.
$cli = new Cli();

$cli->description('Dump some information from your database.')
    ->opt('host:h', 'Connect to host.', true)
    ->opt('port:P', 'Port number to use.', false, 'integer')
    ->opt('user:u', 'User for login if not current user.', true)
    ->opt('password:p', 'Password to use when connecting to server.')
    ->opt('database:d', 'The name of the database to dump.', true);

// Parse and return cli args.
$args = $cli->parse($argv, true);

此示例返回一个Args对象,或退出以显示帮助或错误消息。以下是关于示例的一些注意事项。

  • 您可以通过将false作为parse()的第二个参数传递来抛出异常而不是退出。
  • opt方法有以下参数:namedescriptionrequiredtype。大多数参数都有合理的默认值。
  • 如果您想为选项指定简写代码,请使用冒号将其与name参数分开。
  • 如果您为选项指定了简写代码,这将仅作为参数名称在$argv中的别名。您在解析后始终通过其完整名称访问选项。

选项类型

opt方法有一个$type参数,您可以使用它来指定选项的类型。有效类型是integerstringboolean,默认为string

您还可以在类型名称后添加[]来指定数组。要在命令行上提供数组,您需要多次指定选项,如下所示

command --header="line1" --header="line2"

显示帮助

如果您使用--help选项调用基本示例,则会看到以下帮助信息打印出来

usage: dbdump.php [<options>]

Dump some information from your database.

OPTIONS
  --database, -d   The name of the database to dump.
  --help, -?       Display this help.
  --host, -h       Connect to host.
  --password, -p   Password to use when connecting to server.
  --port, -P       Port number to use.
  --user, -u       User for login if not current user.

所有选项都打印在一个紧凑的表格中,且必需选项以粗体显示。表格会自动扩展以适应较长的选项名称,并在提供额外的长描述时自动换行。

显示错误

假设您只使用-P foo调用基本示例,您会看到以下错误消息

The value of --port (-P) is not a valid integer.
Missing required option: database
Missing required option: user

使用解析后的选项

一旦您使用Cli->parse($argv)成功解析了$argv,您就可以使用返回的Args对象上的各种方法。

$args = $cli->parse($argv);

$host = $args->getOpt('host', '127.0.0.1'); // get host with default 127.0.0.1
$user = $args->getOpt('user'); // get user
$database = $args['database']; // use the args like an array too
$port = $args->getOpt('port', 123); // get port with default 123

多条命令示例

假设您正在编写一个类似于git的命令行工具,名为nit.php,该工具可以从远程仓库推送和拉取信息。

// Define a cli with commands.
$cli = Cli::create()
    // Define the first command: push.
    ->command('push')
    ->description('Push data to a remote server.')
    ->opt('force:f', 'Force an overwrite.', false, 'boolean')
    ->opt('set-upstream:u', 'Add a reference to the upstream repo.', false, 'boolean')
    // Define the second command: pull.
    ->command('pull')
    ->description('Pull data from a remote server.')
    ->opt('commit', 'Perform the merge and commit the result.', false, 'boolean')
    // Set some global options.
    ->command('*')
    ->opt('verbose:v', 'Output verbose information.', false, 'integer')
    ->arg('repo', 'The repository to sync with.', true);

$args = $cli->parse($argv);

像基本示例一样,parse()在成功解析后会返回一个Args对象。以下是一些关于此示例的注意事项。

  • 如果想要在定义命令架构时拥有100%流畅的接口,则提供了Cli::create()方法。
  • 调用command()方法来定义一个新的命令。
  • 如果调用command('*'),则可以定义适用于所有命令的全局选项。
  • 如果opt()的类型是integer,则可以统计一个选项被提供的次数。在此示例中,这允许您通过添加多个-v来指定多个级别的详细程度。
  • arg()方法允许您定义位于命令行选项之后的参数。下面将详细介绍。

列出命令

调用没有选项或仅包含--help选项的脚本将显示命令列表。以下是上述多个命令示例的输出。

usage: nit.php <command> [<options>] [<args>]

COMMANDS
  push   Push data to a remote server.
  pull   Pull data from a remote server.

Args和Opts

Args类区分了args和opts。在该类上提供了访问opts和args的方法。

  • Opts通过--name全名或-s短代码传递。它们是有名称的,并且可以有类型。
  • Args作为选项之后传递,作为由空格分隔的字符串。
  • 在从命令行调用脚本时,如果您有歧义,可以使用--来分隔opts和args。

CliApplication类

Cli类的基本功能对于定义和记录opts和args非常有效。但是,您仍然需要将解析后的命令行args连接到自己的代码。如果您想减少这种样板代码,可以使用CliApplication类。

注意:为了使用CliApplication功能,您将需要引入一些额外的依赖。有关更多信息,请参阅composer.json中建议的包。

定义CliApplication的子类

要使用CliApplication,通常您会继承它并重写configureContainer()configureCli()方法以定义您应用中的命令。

class App extends Garden\Cli\Application\CliApplication {
    protected function configureCli(): void {
        parent::configureCli();

        // Add methods with addMethod().
        $this->addMethod('SomeClassName', 'someMethod');
        $this->addMethod('SomeClassName', 'someOtherMethod', [CliApplication::OPT_SETTERS => false]);
        $this->addMethod('SomeOtherClassName', 'someMethod', [CliApplication::OPT_COMMAND => 'command-name']);

        // Add command classes with addCommandClass().
        $this->addCommandClass('ExampleCommand', 'run');

        // Add ad-hoc closures with addCallable().
        $this->addCallable('foo', function (int $count) { });

        // Wire up dependencies with addConstructor() or addFactory().
        $this->addFactory(\PDO::class, [\Garden\Cli\Utility\DbUtils::class, 'createMySQL']);
    }

    protected function configureContainer(Container $container): void {
        parent::configureContainer($container);

        // Configure the container here.
    }
}

此示例连接了三个方法。

使用addMethod()方法

您可以通过使用addMethod()将类方法连接到命令行。这将执行以下操作:

  1. 它将创建一个从方法名派生的命令。使用OPT_COMMAND选项覆盖命令名称。
  2. 它将可选地创建用于对象设置器的opts。对象设置器是以下一个参数的以单词set开头的函数。您可以使用OPT_SETTERS选项取消设置器连接。
  3. 它将创建用于方法参数的opts。如果方法有类类型提示类型,则它们不会连接到opts,而是由容器满足。
  4. 它将使用方法文档块来添加命令和opts的描述。请确保您使用PHPDoc语法。

您可以使用静态或实例方法调用addMethod()。如果您传递静态方法,则它将只连接静态设置器。实例方法将连接静态和实例方法。

使用addCommandClass()方法

addCommandClass()方法与addMethod()非常相似,但有以下不同之处:

  1. 命令名称将从类名推断。您可以使用OPT_COMMAND_REGEX来删除前缀或后缀。默认情况下,正则表达式将删除以"Job"或"Command"结尾的类的后缀。
  2. 它将从类描述中推断命令描述。
  3. 默认情况下,从设置器创建opts。

使用addCallable()方法

您可以通过使用addCallable()将一个临时的闭包连接到命令行。这与addMethod()非常相似,但它只会反映可调用的参数。

尽管在行内闭包中添加文档块不是常见的做法,但你仍然可以这样做,并且它将被用来记录命令。如果你不这样做,但至少想提供一个描述,请使用OPT_DESCRIPTION选项来提供。

使用addConstructor()addFactory()方法

你可以通过将构造函数参数或工厂方法连接到opts来添加依赖。最常见的情况是指定数据库的连接参数或API客户端的访问令牌。

如果类的构造函数参数来自命令行有意义,请使用addConstructor()

如果你想要清理参数的名称或以某种方式执行一些额外的属性,请使用addFactory()

如果构造函数或工厂具有类类型提示,则无需担心。它们将通过容器自动注入。然后,你可以通过容器目录进行配置,甚至可以通过调用addConstructor()addFactory()来将它们连接到opts。

class App extends Garden\Cli\Application\CliApplication {
    protected function configureCli(): void {
        parent::configureCli();

        // This will make the database connection get created by the DbUtils::createMySQL() method with command line opts for the same.
        $this->addFactory(\PDO::class, [\Garden\Cli\Utility\DbUtils::class, 'createMySQL']);
        $this->getContainer()->setShared(true);

        // This will wire up the constructor parameters for the the StreamLogger to the command line and set is as the logger for the app.
        $this->addConstructor(\Garden\Cli\StreamLogger::class);
        $this->getContainer()->setShared(true);
        $this->getContainer()->rule(\Psr\Log\LoggerInterface::class)->setAliasOf(\Garden\Cli\StreamLogger::class);
    }
}

使用addCall()方法

你可以使用addCall()方法将一个类方法连接起来。用于setter注入。在类实例化时应用此调用。

class App extends Garden\Cli\Application\CliApplication {
    protected function configureCli(): void {
        parent::configureCli();

        // Wire up your github client's API key to the command line.
        $this->addCall(GithubClient::class, 'setAPIKey', [\Garden\Cli\Application\CliApplication::OPT_PREFIX => 'git-']);
    }
}

运行您的应用程序

要使用您的应用程序,只需调用main()方法。

$app = new App();
$app->main($argv);

主方法执行以下操作

  1. 解析$argv参数以确定命令。
  2. 如果命令映射到实例方法,则从容器中获取实例。
  3. 应用从opts中设置setter。
  4. 通过容器调用该方法,满足任何未指定为opts的参数。

从Garden CLI应用程序迁移到CliApplication

如果您想将旧的Garden CLI应用程序迁移到CliApplication,则需要执行以下操作

  1. CliApplication替换您对Cli类的使用。CliApplication是主Cli类的子类。因此,如果您有一个使用Cli类的应用程序,则只需将您的实例替换为CliApplication并使用您的旧代码。

  2. 重写CliApplication::dispatchInternal()方法,并将您的switch语句或相关内容移到那里。确保在您的代码之后调用parent::dispatchInternal(),通常作为switch的默认值。

  3. 将您的$cli->parse($argv)调用替换为$cli->main($argv)调用。这将解析参数并将调度到您的dispatchInternal()方法。

  4. 现在您可以开始用CliApplication特定方法的调用替换一些样板代码。如果您愿意,可以保留旧样板代码不变,只是为新代码使用CliApplication辅助程序。

日志记录

许多CLI应用程序需要某种形式的日志记录。Garden CLI可以满足这一需求。

使用TaskLogger格式化输出

TaskLogger是一个PSR-3日志装饰器,它可以帮助您以优雅、紧凑的样式将基于任务的信息输出到控制台。它适用于像安装脚本、耗时脚本或放入cron作业中的脚本这样的场景。

日志记录任务

当使用TaskLogger时,您需要从消息和任务的角度思考。消息是要输出给用户的一个单独的日志项。任务有一个开始和一个结束,并且可以无限嵌套。使用各种PSR-3方法输出消息,而使用begin()end()输出任务。以下是您可以用于记录任务的全部方法。

任务嵌套和持续时间

您可以通过在调用 end* 方法之前调用 begin* 方法来无限嵌套任务。每次嵌套任务时,它将输出其消息,并缩进一个级别。任务还会计算其持续时间,并在调用 end 后输出。

抑制消息

默认情况下,TaskLogger 只会输出 LogLevel::INFO 级别或更高级别的消息。您可以使用 setMinLevel 方法来更改此设置。如果在一个被抑制的级别开始任务,但子消息在或高于最小级别,则开始任务的消息将被回溯输出。这允许您看到哪个任务触发了日志消息。

示例

$log = new TaskLogger();

$log->info('This is a message.');
$log->error('This is an error.'); // outputs in red

$log->beginInfo('Begin a task');
// code task code goes here...
$log->end('done.');

$log->beginDebug('Make an API call');
$log->endHttpStatus(200); // treated as error or success depending on code

$log->begin(LogLevel::NOTICE, 'Multi-step task');
$log->info('Step 1');
$log->info('Step 2');
$log->beginDebug('Step 3');
$log->debug('Step 3.1'); // steps will be hidden because they are level 3
$log->debug('Step 3.2');
$log->end('done.');
$log->end('done.');

StreamLogger

如果您创建并使用 TaskLogger 对象,它将默认输出到控制台。在内部,它使用 StreamLogger 对象来处理将任务格式化到输出流(在这种情况下是 stdout)的操作。如果您想更细粒度地控制日志记录,可以替换或修改 StreamLogger。以下是一些选项。

示例

以下示例创建了一个 StreamLogger 对象,并在将其传递给 TaskLogger 构造函数之前调整了一些设置。

$fmt = new StreamLogger(STDOUT);

$fmt->setLineFormat('{level}: {time} {message}');

$fmt->setLevelFormat('strtoupper');

$fmt->setTimeFormat(function ($ts) {
    return number_format(time() - $ts).' seconds ago';
});

$log = new TaskLogger($fmt);

实现自己的日志记录器

您可以将任何符合 PSR-3 的日志记录器提供给 TaskLogger,并将输出发送到它。为了使用一些特殊任务功能,您需要检查 log 方法的 $contenxt 参数。这里是一些您可能会收到的字段。