PHP的命令行界面库

v1.7.2 2024-09-05 00:08 UTC

README

PHP的框架无关命令行界面工具和助手。轻松构建控制台应用程序,享受乐趣和爱。

Latest Version Build Scrutinizer CI Codecov branch StyleCI Software License Tweet Support

  • 命令行应用程序制作简单
  • 受nodejs commander (感谢tj) 启发
  • 无依赖。
  • 适用于PHP7,PHP8,并且非常好

Screen Preview

包含内容

核心: Argv解析器 · 命令行应用程序 · Shell

IO: 着色器 · 光标操纵器 · 进度条 · 流写入器 · 流读取器

其他: 自动完成

安装

# PHP8.0 and above v1.0.0
composer require adhocore/cli:^v1.0.0

# PHP 7.x
composer require adhocore/cli:^v0.9.0

使用方法

Argv解析器

$command = new Ahc\Cli\Input\Command('rmdir', 'Remove dirs');

$command
    ->version('0.0.1-dev')
    // Arguments are separated by space
    // Format: `<name>` for required, `[name]` for optional
    //  `[name:default]` for default value, `[name...]` for variadic (last argument)
    ->arguments('<dir> [dirs...]')
    // `-h --help`, `-V --version`, `-v --verbosity` options are already added by default.
    // Format: `<name>` for required, `[name]` for optional
    ->option('-s --with-subdir', 'Also delete subdirs (`with` means false by default)')
    ->option('-e,--no-empty', 'Delete empty (`no` means true by default)')
    // Specify santitizer/callback as 3rd param, default value as 4th param
    ->option('-d|--depth [nestlevel]', 'How deep to process subdirs', 'intval', 5)
    ->parse(['thisfile.php', '-sev', 'dir', 'dir1', 'dir2', '-vv']) // `$_SERVER['argv']`
;

// Print all values:
print_r($command->values());

/*Array
(
    [help] =>
    [version] => 0.0.1
    [verbosity] => 3
    [dir] => dir
    [dirs] => Array
        (
            [0] => dir1
            [1] => dir2
        )

    [subdir] => true
    [empty] => false
    [depth] => 5
)*/

// To get values for options except the default ones (help, version, verbosity)
print_r($command->values(false));

// Pick a value by name
$command->dir;   // dir
$command->dirs;  // [dir1, dir2]
$command->depth; // 5

命令帮助

可以通过手动调用$command->showHelp()来触发,或者当将-h--help选项传递给$command->parse()时自动触发。

对于上面的示例,输出将是: Command Help

命令版本

可以通过手动调用$command->showVersion()来触发,或者当将-V--version选项传递给$command->parse()时自动触发。

对于上面的示例,输出将是

0.0.1-dev

控制台应用程序

一定要查看adhocore/phint - 使用adhocore/cli制作的实际控制台应用程序。

这里我们模拟了一个具有有限功能(如addcheckout)的git应用程序。您将看到构建控制台应用程序是多么直观、流畅和有趣!

Git应用程序

$app = new Ahc\Cli\Application('git', '0.0.1');

$app
    // Register `add` command
    ->command('add', 'Stage changed files', 'a') // alias a
        // Set options and arguments for this command
        ->arguments('<path> [paths...]')
        ->option('-f --force', 'Force add ignored file', 'boolval', false)
        ->option('-N --intent-to-add', 'Add content later but index now', 'boolval', false)
        // Handler for this command: param names should match but order can be anything :)
        ->action(function ($path, $paths, $force, $intentToAdd) {
            array_unshift($paths, $path);

            echo ($intentToAdd ? 'Intent to add ' : 'Add ')
                . implode(', ', $paths)
                . ($force ? ' with force' : '');

            // If you return integer from here, that will be taken as exit error code
        })
        // Done setting up this command for now, tap() to retreat back so we can add another command
        ->tap()
    ->command('checkout', 'Switch branches', 'co') // alias co
        ->arguments('<branch>')
        ->option('-b --new-branch', 'Create a new branch and switch to it', false)
        ->option('-f --force', 'Checkout even if index differs', 'boolval', false)
        ->action(function ($branch, $newBranch, $force) {
            echo 'Checkout to '
                . ($newBranch ? 'new ' . $branch : $branch)
                . ($force ? ' with force' : '');
        })
;

// Parse only parses input but doesnt invoke action
$app->parse(['git', 'add', 'path1', 'path2', 'path3', '-f']);

// Handle will do both parse and invoke action.
$app->handle(['git', 'add', 'path1', 'path2', 'path3', '-f']);
// Will produce: Add path1, path2, path3 with force

$app->handle(['git', 'co', '-b', 'master-2', '-f']);
// Will produce: Checkout to new master-2 with force

有组织的应用程序

而不是内联命令/操作,我们定义并添加自己的命令(具有interact()execute())到应用程序

class InitCommand extends Ahc\Cli\Input\Command
{
    public function __construct()
    {
        parent::__construct('init', 'Init something');

        $this
            ->argument('<arrg>', 'The Arrg')
            ->argument('[arg2]', 'The Arg2')
            ->option('-a --apple', 'The Apple')
            ->option('-b --ball', 'The ball')
            // Usage examples:
            ->usage(
                // append details or explanation of given example with ` ## ` so they will be uniformly aligned when shown
                '<bold>  init</end> <comment>--apple applet --ball ballon <arggg></end> ## details 1<eol/>' .
                // $0 will be interpolated to actual command name
                '<bold>  $0</end> <comment>-a applet -b ballon <arggg> [arg2]</end> ## details 2<eol/>'
            );
    }

    // This method is auto called before `self::execute()` and receives `Interactor $io` instance
    public function interact(Ahc\Cli\IO\Interactor $io) : void
    {
        // Collect missing opts/args
        if (!$this->apple) {
            $this->set('apple', $io->prompt('Enter apple'));
        }

        if (!$this->ball) {
            $this->set('ball', $io->prompt('Enter ball'));
        }

        // ...
    }

    // When app->handle() locates `init` command it automatically calls `execute()`
    // with correct $ball and $apple values
    public function execute($ball, $apple)
    {
        $io = $this->app()->io();

        $io->write('Apple ' . $apple, true);
        $io->write('Ball ' . $ball, true);

        // more codes ...

        // If you return integer from here, that will be taken as exit error code
    }
}

class OtherCommand extends Ahc\Cli\Input\Command
{
    public function __construct()
    {
        parent::__construct('other', 'Other something');
    }

    public function execute()
    {
        $io = $this->app()->io();

        $io->write('Other command');

        // more codes ...

        // If you return integer from here, that will be taken as exit error code
    }
}

// Init App with name and version
$app = new Ahc\Cli\Application('App', 'v0.0.1');

// Add commands with optional aliases`
$app->add(new InitCommand, 'i');
$app->add(new OtherCommand, 'o');

// Set logo
$app->logo('Ascii art logo of your app');

$app->handle($_SERVER['argv']); // if argv[1] is `i` or `init` it executes InitCommand

分组命令

分组命令在命令列表中一起列出。显式分组命令是可选的。默认情况下,如果命令名称有冒号:,则其前面的部分被视为组,否则使用*作为组。

示例:命令名称app:env具有默认组app,命令名称appenv具有组*

// Add grouped commands:
$app->group('Configuration', function ($app) {
    $app->add(new ConfigSetCommand);
    $app->add(new ConfigListCommand);
});

// Alternatively, set group one by one in each commands:
$app->add((new ConfigSetCommand)->inGroup('Config'));
$app->add((new ConfigListCommand)->inGroup('Config'));
...

异常处理器

设置自定义异常处理程序作为回调。回调接收异常和退出代码。回调可以重新抛出异常或退出程序,或者仅记录异常并执行其他操作。

$app = new Ahc\Cli\Application('App', 'v0.0.1');
$app->add(...);
$app->onException(function (Throwable $e, int $exitCode) {
    // send to sentry
    // write to logs

    // optionally, exit with exit code:
    exit($exitCode);

    // or optionally rethrow, a rethrown exception is propagated to top layer caller.
    throw $e;
})->handle($argv);

应用程序帮助

可以通过手动调用$app->showHelp()来触发,或者当将-h--help选项传递给$app->parse()时自动触发。**注意**:如果您将类似['app', 'cmd', '-h']的内容传递给$app->parse(),它将自动立即显示该cmd的帮助,而不是$app

对于上述示例,输出将是: App Help

应用程序版本

相同的版本号传递给所有附加命令。因此,您可以在任何命令上触发版本。

Shell

一个非常薄的shell包装器,提供围绕proc_open()的便利方法。

基本用法

$shell = new Ahc\Cli\Helper\Shell($command = 'php -v', $rawInput = null);

// Waits until proc finishes
$shell->execute($async = false); // default false

echo $shell->getOutput(); // PHP version string (often with zend/opcache info)

高级用法

$shell = new Ahc\Cli\Helper\Shell('php /some/long/running/scipt.php');

// With async flag, doesnt wait for proc to finish!
$shell->setOptions($workDir = '/home', $envVars = [])
    ->execute($async = true)
    ->isRunning(); // true

// Force stop anytime (please check php.net/proc_close)
$shell->stop(); // also closes pipes

// Force kill anytime (please check php.net/proc_terminate)
$shell->kill();

超时

$shell = new Ahc\Cli\Helper\Shell('php /some/long/running/scipt.php');

// Wait for at most 10.5 seconds for proc to finish!
// If it doesnt complete by then, throws exception
$shell->setOptions($workDir, $envVars, $timeout = 10.5)->execute();

// And if it completes within timeout, you can access the stdout/stderr
echo $shell->getOutput();
echo $shell->getErrorOutput();

Cli交互

您可以使用提供的Ahc\Cli\IO\Interactor执行用户交互,例如打印彩色输出、以编程方式读取用户输入并移动光标。

$interactor = new Ahc\Cli\IO\Interactor;

// For mocking io:
$interactor = new Ahc\Cli\IO\Interactor($inputPath, $outputPath);

确认

$confirm = $interactor->confirm('Are you happy?', 'n'); // Default: n (no)
$confirm // is a boolean
    ? $interactor->greenBold('You are happy :)', true)  // Output green bold text
    : $interactor->redBold('You are sad :(', true);     // Output red bold text

单选

$fruits = ['a' => 'apple', 'b' => 'banana'];
$choice = $interactor->choice('Select a fruit', $fruits, 'b');
$interactor->greenBold("You selected: {$fruits[$choice]}", true);

多选

$fruits  = ['a' => 'apple', 'b' => 'banana', 'c' => 'cherry'];
$choices = $interactor->choices('Select fruit(s)', $fruits, ['b', 'c']);
$choices = \array_map(function ($c) use ($fruits) { return $fruits[$c]; }, $choices);
$interactor->greenBold('You selected: ' . implode(', ', $choices), true);

无提示输入

$any = $interactor->prompt('Anything', rand(1, 100)); // Random default
$interactor->greenBold("Anything is: $any", true);

带验证的提示

$nameValidator = function ($value) {
    if (\strlen($value) < 5) {
        throw new \InvalidArgumentException('Name should be atleast 5 chars');
    }

    return $value;
};

// No default, Retry 5 more times
$name = $interactor->prompt('Name', null, $nameValidator, 5);
$interactor->greenBold("The name is: $name", true);

隐藏提示

在Windows平台上,它可能会更改字体样式,这可以通过此方法修复

$passValidator = function ($pass) {
    if (\strlen($pass) < 6) {
        throw new \InvalidArgumentException('Password too short');
    }

    return $pass;
};

$pass = $interactor->promptHidden('Password', $passValidator, 2);

Interactive Preview

IO组件

交互器由Ahc\Cli\Input\ReaderAhc\Cli\Output\Writer组成,而Writer本身由Ahc\Cli\Output\Color组成。所有这些组件都可以独立使用。

颜色

颜色看起来很酷!

$color = new Ahc\Cli\Output\Color;

简单用法

echo $color->warn('This is warning');
echo $color->info('This is info');
echo $color->error('This is error');
echo $color->comment('This is comment');
echo $color->ok('This is ok msg');

自定义样式

Ahc\Cli\Output\Color::style('mystyle', [
    'bg' => Ahc\Cli\Output\Color::CYAN,
    'fg' => Ahc\Cli\Output\Color::WHITE,
    'bold' => 1, // You can experiment with 0, 1, 2, 3 ... as well
]);

echo $color->mystyle('My text');

光标

移动光标,删除上一行或下一行,清除屏幕。

$cursor = new Ahc\Cli\Output\Cursor;

echo  $cursor->up(1)
    . $cursor->down(2)
    . $cursor->right(3)
    . $cursor->left(4)
    . $cursor->next(0)
    . $cursor->prev(2);
    . $cursor->eraseLine()
    . $cursor->clear()
    . $cursor->clearUp()
    . $cursor->clearDown()
    . $cursor->moveTo(5, 8); // x, y

进度条

轻松将进度条添加到您的输出中

$progress = new Ahc\Cli\Output\ProgressBar(100);
for ($i = 0; $i <= 100; $i++) {
    $progress->current($i);

    // Simulate something happening
    usleep(80000);
}

您也可以手动前进进度条

$progress = new Ahc\Cli\Output\ProgressBar(100);

// Do something

$progress->advance(); // Adds 1 to the current progress

// Do something

$progress->advance(10); // Adds 10 to the current progress

// Do something

$progress->advance(5, 'Still going.'); // Adds 5, displays a label

您可以覆盖进度条选项以自定义它

$progress = new Ahc\Cli\Output\ProgressBar(100);
$progress->option('pointer', '>>');
$progress->option('loader', '');

// You can set the progress fluently
$progress->option('pointer', '>>')->option('loader', '');

// You can also use an associative array to set many options in one time
$progress->option([
    'pointer' => '>>',
    'loader'  => ''
]);

// Available options
+---------------+------------------------------------------------------+---------------+
| Option        | Description                                          | Default value |
+===============+======================================================+===============+
| pointer       | The progress bar head symbol                         | >             |
| loader        | The loader symbol                                    | =             |
| color         | The color of progress bar                            | white         |
| labelColor    | The text color of the label                          | white         |
| labelPosition | The position of the label (top, bottom, left, right) | bottom        |
+---------------+------------------------------------------------------+---------------+

Writer

以风格写任何东西。

$writer = new Ahc\Cli\Output\Writer;

// All writes are forwarded to STDOUT
// But if you specify error, then to STDERR
$writer->errorBold('This is error');

输出格式化

您可以调用任何组合的方法:'<colorName>', 'bold', 'bg', 'fg', 'warn', 'info', 'error', 'ok', 'comment' ... 以任何顺序(例如:bgRedFgBlaockboldRedgreenBoldcommentBgPurple等 ...)

$writer->bold->green->write('It is bold green');
$writer->boldGreen('It is bold green'); // Same as above
$writer->comment('This is grayish comment', true); // True indicates append EOL character.
$writer->bgPurpleBold('This is white on purple background');

自由风格

一个调用中包含许多颜色:用标签<method></end>包裹文本。对于NL/EOL,请使用<eol></eol><eol/>

非常适合编写长彩色文本,例如命令用法信息。

$writer->colors('<red>This is red</end><eol><bgGreen>This has bg Green</end>');

原始输出

$writer->raw('Enter name: ');

表格

只需传递关联数组的数组。第一个数组的键将被用作标题。标题将自动转换为人类可读的大写单词(ucwords)。

$writer->table([
    ['a' => 'apple', 'b-c' => 'ball', 'c_d' => 'cat'],
    ['a' => 'applet', 'b-c' => 'bee', 'c_d' => 'cute'],
]);

给出类似的东西

+--------+------+------+
| A      | B C  | C D  |
+--------+------+------+
| apple  | ball | cat  |
| applet | bee  | cute |
+--------+------+------+

设计表格外观和感觉

只需传递第二个参数$styles

$writer->table([
    ['a' => 'apple', 'b-c' => 'ball', 'c_d' => 'cat'],
    ['a' => 'applet', 'b-c' => 'bee', 'c_d' => 'cute'],
], [
    // for => styleName (anything that you would call in $writer instance)
    'head' => 'boldGreen', // For the table heading
    'odd'  => 'bold',      // For the odd rows (1st row is odd, then 3, 5 etc)
    'even' => 'comment',   // For the even rows (2nd row is even, then 4, 6 etc)
]);

// 'head', 'odd', 'even' are all the styles for now
// In future we may support styling a column by its name!

内容对齐(显示设置)

如果您想以类似Laravel的方式(通过php artisan about命令)显示某些配置(例如从您的.env文件),则可以使用justify方法。

$writer->justify('Environment');
$writer->justify('PHP Version', PHP_VERSION);
$writer->justify('App Version', '1.0.0');
$writer->justify('Locale', 'en');

给出类似的东西

Environment ........................................
PHP Version .................................. 8.1.4
App Version .................................. 1.0.0
Locale .......................................... en

您可以使用sep参数来定义要使用的分隔符。

$writer->justify('Environment', '', ['sep' => '-']);
$writer->justify('PHP Version', PHP_VERSION);

给出类似的东西

Environment ----------------------------------------
PHP Version .................................. 8.1.4

此外,您还可以通过此方法的第三个参数定义文本的颜色、背景颜色以及两种文本的厚度。

$writer->justify('Cache Enable', 'true', [
    'first' => ['fg' => Ahc\Cli\Output\Color::CYAN], // style of the key
    'second' => ['fg' => Ahc\Cli\Output\Color::GREEN], // style of the value
]);
$writer->justify('Debug Mode', 'false', [
    'first' => ['fg' => Ahc\Cli\Output\Color::CYAN], // style of the key
    'second' => ['fg' => Ahc\Cli\Output\Color::RED], // style of the value
]);

有关不同颜色选项的更多详细信息,请参阅自定义样式

Reader

读取和预处理用户输入。

$reader = new Ahc\Cli\Input\Reader;

// No default, callback fn `ucwords()`
$reader->read(null, 'ucwords');

// Default 'abc', callback `trim()`
$reader->read('abc', 'trim');

// Read at most first 5 chars
// (if ENTER is pressed before 5 chars then further read is aborted)
$reader->read('', 'trim', 5);

// Read but dont echo back the input
$reader->readHidden($default, $callback);

// Read from piped stream (or STDIN) if available without waiting
$reader->readPiped();

// Pass in a callback for if STDIN is empty
// The callback recieves $reader instance and MUST return string
$reader->readPiped(function ($reader) {
    // Wait to read a line!
    return $reader->read();

    // Wait to read multi lines (until Ctrl+D pressed)
    return $reader->readAll();
});

异常

每当捕获到异常时,Application::handle()将显示漂亮的堆栈跟踪并退出状态码非0。

Exception Preview

自动完成

任何基于adhocore/cli构建的命令行应用程序都可以在zsh shell中使用oh-my-zsh进行命令和选项的自动完成。

您只需在~/.oh-my-zsh/custom/plugins/ahccli/ahccli.plugin.zsh的末尾添加一行即可。

compdef _ahccli <appname>

示例:对于phint,使用compdef _ahccli phint

手动执行此操作很麻烦,以下是一个您可以复制/粘贴/运行的完整命令

一次性设置

mkdir -p ~/.oh-my-zsh/custom/plugins/ahccli && cd ~/.oh-my-zsh/custom/plugins/ahccli

[ -f ./ahccli.plugin.zsh ] || curl -sSLo ./ahccli.plugin.zsh https://raw.githubusercontent.com/adhocore/php-cli/master/ahccli.plugin.zsh

chmod 760 ./ahccli.plugin.zsh && cd -
加载ahccli插件

这也是一次性设置。

# Open .zshrc
nano ~/.zshrc

# locate plugins=(... ...) and add ahccli
plugins=(git ... ... ahccli)

# ... then save it (Ctrl + O)

注册应用程序

# replace appname with real name eg: phint
echo compdef _ahccli appname >> ~/.oh-my-zsh/custom/plugins/ahccli/ahccli.plugin.zsh

当然,您可以添加多个应用程序,只需更改上述命令中的appname即可。

然后要么重新启动shell,要么像这样source插件

source ~/.oh-my-zsh/custom/plugins/ahccli/ahccli.plugin.zsh

触发自动完成

appname <tab>            # autocompletes commands               (phint <tab>)
appname subcommand <tab> # autocompletes options for subcommand (phint init <tab>)

相关

贡献者

许可证

© 2017-2020, Jitendra Adhikari | MIT

感谢

该项目由 please 管理发布。