diff --git a/README.md b/README.md index 36cd89d6..6750de09 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,18 @@ # php 命令行应用库 -[![License](https://img.shields.io/packagist/l/inhere/console.svg?style=flat-square)](LICENSE-2.0.txt) -[![Php Version](https://img.shields.io/badge/php-%3E=7.0-brightgreen.svg?maxAge=2592000)](https://packagist.org/packages/inhere/console) +[![License](https://img.shields.io/packagist/l/inhere/console.svg?style=flat-square)](LICENSE) +[![Php Version](https://img.shields.io/badge/php-%3E=5.6.0-brightgreen.svg?maxAge=2592000)](https://packagist.org/packages/inhere/console) [![Latest Stable Version](http://img.shields.io/packagist/v/inhere/console.svg)](https://packagist.org/packages/inhere/console) -简洁、功能全面的php命令行应用库。提供控制台参数解析, 颜色风格输出, 用户信息交互, 特殊格式信息显示。 - -> 无其他库依赖,可以方便的整合到任何已有项目中。 +简洁、功能全面的php命令行应用库。提供控制台参数解析, 命令运行,颜色风格输出, 用户信息交互, 特殊格式信息显示。 +- 可以方便的整合到任何已有项目中 - 功能全面的命令行的选项参数解析(命名参数,短选项,长选项 ...) -- 命令行应用, 命令行的 `controller`, `command` 解析运行 +- 命令行应用, 命令行的 `controller`, `command` 解析运行。(支持命令别名) - 命令行中功能强大的 `input`, `output` 管理、使用 - 消息文本的多种颜色风格输出支持(`info`, `comment`, `success`, `danger`, `error` ... ...) -- 丰富的特殊格式信息显示(`section`, `panel`, `padding`, `help-panel`, `table`, `title`, `list`, `progressBar`) -- 常用的用户信息交互支持(`select`, `confirm`, `ask/question`) +- 丰富的特殊格式信息显示(`section`, `panel`, `padding`, `help-panel`, `table`, `title`, `list`, `multiList`, `progressBar`) +- 常用的用户信息交互支持(`select`, `multiSelect`, `confirm`, `ask/question`, `askPassword/askHiddenInput`) - 命令方法注释自动解析(提取为参数 `arguments` 和 选项 `options` 等信息) - 类似 `symfony/console` 的预定义参数定义支持(按位置赋予参数值) - 输出是 windows,linux 兼容的,不支持颜色的环境会自动去除相关CODE @@ -29,18 +28,25 @@ **注意:** -- master 分支是要求 `php>= 7` 的(推荐使用)。 -- php5 分支是支持 php5 `php>= 5.5` 的代码分支。 +- `1.x` 分支是支持 php5 `php>= 5.6` 的代码分支。 ## 安装 -- 使用 composer +- 使用 composer 命令 + +```bash +composer require inhere/console +``` + +- 使用 composer.json 编辑 `composer.json`,在 `require` 添加 ``` "inhere/console": "dev-master", -// "inhere/console": "dev-php5", // for php5 + +// "inhere/console": "^2.0", // 指定稳定版本 +// "inhere/console": "~1.0", // for php5 ``` 然后执行: `composer update` @@ -84,10 +90,12 @@ $app->run(); 然后在命令行里执行 `php examples/app`, 立即就可以看到如下输出了: -!['output-commands-info'](images/example-app.png) +!['app-command-list'](docs/screenshots/app-command-list.png) > `Independent Commands` 中的 demo 就是我们上面添加的命令 +- `[alias: ...]` 命令最后的alias 表明了此命令拥有的别名。 + ## 添加命令 添加命令的方式有三种 @@ -104,7 +112,7 @@ $app->command('demo', function (Input $in, Output $out) { }, 'this is message for the command'); ``` -### 继承 `Inhere\Console\Command` +### 独立命令 通过继承 `Inhere\Console\Command` 添加独立命令 @@ -155,7 +163,7 @@ $app->command(TestCommand::class); // $app->command('test1', TestCommand::class); ``` -### 继承 `Inhere\Console\Controller` +### 命令组 通过继承 `Inhere\Console\Controller` 添加一组命令. 即是命令行的控制器 @@ -171,13 +179,29 @@ class HomeController extends Controller protected static $description = 'default command controller. there are some command usage examples'; /** - * this is a command's description message a color text + * this is a command's description message, color text * the second line text - * @usage usage message + * @usage {command} [arg ...] [--opt ...] + * @arguments + * arg1 argument description 1 + * the second line + * a2,arg2 argument description 2 + * the second line + * @options + * -s, --long option description 1 + * --opt option description 2 * @example example text one * the second line example */ - public function indexCommand() + public function testCommand() + { + $this->write('hello, welcome!! this is ' . __METHOD__); + } + + /** + * a example for use color text output on command + */ + public function otherCommand() { $this->write('hello, welcome!! this is ' . __METHOD__); } @@ -192,13 +216,14 @@ class HomeController extends Controller - 支持的tag有 `@usage` `@arguments` `@options` `@example` - 当你使用 `php examples/app home -h` 时,可以查看到 `HomeController` 的所有命令描述注释信息 -- 当使用 `php examples/app home:index -h` 时,可以查看到关于 `HomeController::indexCommand` 更详细的信息。包括描述注释文本、`@usage` 、`@example` + + ![group-command-list](docs/screenshots/group-command-list.png) +- 当使用 `php examples/app home:test -h` 时,可以查看到关于 `HomeController::testCommand` 更详细的信息。包括描述注释文本、`@usage` 、`@example` -> 小提示:注释里面同样支持带颜色的文本输出 `eg: this is a command's description message` + ![group-command-list](docs/screenshots/group-command-help.png) -- 运行效果(by `php examples/app home`): +> 小提示:注释里面同样支持带颜色的文本输出 `eg: this is a command's description message` -![command-group-example](./images/example-for-group.png) 更多请查看 [examples](./examples) 中的示例代码和在目录下运行示例 `php examples/app` 来查看效果 @@ -360,12 +385,24 @@ $output->write('hello world'); 已经内置了常用的风格: -![alt text](images/output-color-text.png "Title") +![alt text](docs/screenshots/output-color-text.png "Title") 来自于类 `Inhere\Console\Utils\Show`。 > output 实例拥有 `Inhere\Console\Utils\Show` 的所有格式化输出方法。不过都是通过对象式访问的。 +- **单独使用颜色风格** + +```php +$style = Inhere\Console\Style\Style::create(); + +echo $style->render('no color color text'); + +// 直接使用内置的风格 +echo $style->info('message'); +echo $style->error('message'); +``` + ### 标题文本输出 使用 `Show::title()/$output->title()` @@ -403,13 +440,13 @@ echo "Progress:\n"; $i = 0; while ($i <= $total) { - $bar->send($i); + $bar->send(1);// 发送步进长度,通常是 1 usleep(50000); $i++; } ``` -![show-progress](images/show-progress.png) +![show-progress](docs/screenshots/progress-demo.png) ### 列表数据展示输出 @@ -438,7 +475,9 @@ $data = [ Show::aList($data, $title); ``` -> 渲染效果请看下面的预览 +> 渲染效果 + +![fmt-list](docs/screenshots/fmt-list.png) ### 多列表数据展示输出 @@ -467,7 +506,9 @@ $data = [ Show::mList($data); ``` -> 渲染效果请看下面的预览 +> 渲染效果 + +![fmt-multi-list](docs/screenshots/fmt-multi-list.png) ### 面板展示信息输出 @@ -489,7 +530,9 @@ $data = [ Show::panel($data, 'panel show', '#'); ``` -> 渲染效果请看下面的预览 +> 渲染效果 + +![fmt-panel](docs/screenshots/fmt-panel.png) ### 数据表格信息输出 @@ -531,7 +574,7 @@ Show::table($data, 'a table', $opts); > 渲染效果请看下面的预览 -![table-show](images/table-show.png) +![table-show](docs/screenshots/table-show.png) ### 快速的渲染一个帮助信息面板 @@ -558,9 +601,9 @@ Show::helpPanel([ ], false); ``` -### 渲染效果预览 +> 渲染效果预览 -![alt text](images/output-format-msg.png "Title") +![alt text](docs/screenshots/fmt-help-panel.png "Title") ## 用户交互方法 diff --git a/README_en.md b/README_en.md index 15352c3b..5a74b748 100644 --- a/README_en.md +++ b/README_en.md @@ -12,7 +12,7 @@ a php console application library. - console color support, format message output - console interactive -[中文README](./README.md) +> [中文README](./README.md) ## project @@ -22,7 +22,7 @@ a php console application library. **NOTICE** - master branch -- is require `php>= 7` (recommended use)。 -- php5 branch -- It's a branch of PHP 5, but it hasn't been updated for some time (the basic functionality is complete). +- `1.x` branch -- It's a branch of PHP 5, but it hasn't been updated for some time (the basic functionality is complete). ## install @@ -32,7 +32,7 @@ edit `composer.json`,at `require` add ``` "inhere/console": "dev-master", -// "inhere/console": "dev-php5", // for php5 +// "inhere/console": "dev-1.x", // for php5 ``` run: `composer update` @@ -72,7 +72,7 @@ $app->run(); now, you can see: -!['output-commands-info'](images/example-app.png) +!['app-command-list'](docs/screenshots/app-command-list.png) ## input @@ -174,7 +174,7 @@ $output->write('hello'); #### use color style -![alt text](images/output-color-text.png "Title") +![alt text](docs/screenshots/output-color-text.png "Title") #### special format output @@ -184,7 +184,7 @@ $output->write('hello'); - `$output->table()` - `$output->helpPanel()` -![alt text](images/output-format-msg.png "Title") +![alt text](docs/screenshots/output-format-msg.png "Title") ## more interactive diff --git a/composer.json b/composer.json index ae4dfa6f..42fb9625 100644 --- a/composer.json +++ b/composer.json @@ -1,31 +1,39 @@ { - "name": "inhere/console", - "type": "library", - "description": "a php console library, provide console argument parse,color style,user interactive, information show.", - "keywords": ["library","console","console-color", "command", "command-line", "cli", "console-application"], - "homepage": "https://github.com/inhere/php-console", - "license": "MIT", - "authors": [ - { - "name": "inhere", - "email": "in.798@qq.com", - "homepage": "http://www.yzone.net/" + "name": "inhere/console", + "type": "library", + "description": "a php console library, provide console argument parse,color style,user interactive, information show.", + "keywords": [ + "library", + "console", + "console-color", + "command", + "command-line", + "cli", + "console-application" + ], + "homepage": "https://github.com/inhere/php-console", + "license": "MIT", + "authors": [ + { + "name": "inhere", + "email": "in.798@qq.com", + "homepage": "http://www.yzone.net/" + } + ], + "require": { + "php": ">=5.6.0" + }, + "autoload": { + "psr-4": { + "Inhere\\Console\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Inhere\\Console\\Tests\\": "tests/" + } + }, + "suggest": { + "inhere/simple-print-tool": "Very lightweight data printing tools" } - ], - "require": { - "php": ">=7.0.0" - }, - "autoload": { - "psr-4": { - "Inhere\\Console\\" : "src/" - } - }, - "autoload-dev": { - "psr-4": { - "Inhere\\Console\\Tests\\" : "tests/" - } - }, - "suggest": { - "inhere/simple-print-tool": "Very lightweight data printing tools" - } } diff --git a/docs/catelog.md b/docs/catelog.md new file mode 100644 index 00000000..f9e9bef0 --- /dev/null +++ b/docs/catelog.md @@ -0,0 +1,3 @@ +# php 命令行应用库 + +[简介和安装](intro.md) diff --git a/examples/baks/cli-color.md b/docs/cli-color.md similarity index 100% rename from examples/baks/cli-color.md rename to docs/cli-color.md diff --git a/docs/controller.md b/docs/controller.md new file mode 100644 index 00000000..c9cb977c --- /dev/null +++ b/docs/controller.md @@ -0,0 +1,34 @@ +# 注册命令 + +### 注册独立命令 +### 注册组命令 +### 设置命令名称 +### 设置命令描述 + +## 独立命令 + +## 组命令(controller) + +## 输入定义(InputDefinition) + + +## 设置参数 + +### 使用名称设置参数 + +### 根据位置设置参数 + +``` +$ php examples/app demo john male 43 --opt1 value1 -y +hello, this in Inhere\Console\examples\DemoCommand::execute +this is argument and option example: + the opt1's value + option: opt1 | + | | +php examples/app demo john male 43 --opt1 value1 -y + | | | | | | + script command | | |______ option: yes, it use shortcat: y, and it is a Input::OPT_BOOLEAN, so no value. + | |___ | + argument: name | argument: age + argument: sex +``` diff --git a/docs/input-output.md b/docs/input-output.md new file mode 100644 index 00000000..6a66f068 --- /dev/null +++ b/docs/input-output.md @@ -0,0 +1,39 @@ +# input and output + +## input + +## output + +### output buffer + +how tu use + +- use `Output` + +```php + // open buffer + $this->output->startBuffer(); + + $this->output->write('message 0'); + $this->output->write('message 1'); + // .... + $this->output->write('message n'); + + // stop and output buffer + $this->output->stopBuffer(); +``` + +- use `Show` + +```php + // open buffer + Show::startBuffer(); + + Show::write('message 0'); + Show::write('message 1'); + // .... + Show::write('message n'); + + // stop and output buffer + Show::stopBuffer(); +``` diff --git a/docs/intro.md b/docs/intro.md new file mode 100644 index 00000000..5b788180 --- /dev/null +++ b/docs/intro.md @@ -0,0 +1,56 @@ +# 简介 + +简洁、功能全面的php命令行应用库。提供控制台参数解析, 颜色风格输出, 用户信息交互, 特殊格式信息显示。 + +> 无其他库依赖,可以方便的整合到任何已有项目中。 + +- 功能全面的命令行的选项参数解析(命名参数,短选项,长选项 ...) +- 命令行应用, 命令行的 `controller`, `command` 解析运行 +- 命令行中功能强大的 `input`, `output` 管理、使用 +- 消息文本的多种颜色风格输出支持(`info`, `comment`, `success`, `danger`, `error` ... ...) +- 丰富的特殊格式信息显示(`section`, `panel`, `padding`, `help-panel`, `table`, `title`, `list`, `progressBar`) +- 常用的用户信息交互支持(`select`, `confirm`, `ask/question`) +- 命令方法注释自动解析(提取为参数 `arguments` 和 选项 `options` 等信息) +- 类似 `symfony/console` 的预定义参数定义支持(按位置赋予参数值) +- 输出是 windows,linux 兼容的,不支持颜色的环境会自动去除相关CODE + +> 下面所有的特性,效果都是运行 `examples/` 中的示例代码 `php examples/app` 展示出来的。下载后可以直接测试体验 + + +## 项目地址 + +- **github** https://github.com/inhere/php-console.git +- **git@osc** https://git.oschina.net/inhere/php-console.git + +**注意:** + +- master 分支是要求 `php>= 7` 的(推荐使用)。 +- php5 分支是支持 php5 `php>= 5.5` 的代码分支。 + +## 安装 + +- 使用 composer 命令 + +```bash +composer require inhere/console +``` + +- 使用 composer.json + +编辑 `composer.json`,在 `require` 添加 + +``` +"inhere/console": "dev-master", + +// "inhere/console": "^2.0", // 指定稳定版本 +// "inhere/console": "dev-php5", // for php5 +``` + +然后执行: `composer update` + +- 直接拉取 + +``` +git clone https://git.oschina.net/inhere/php-console.git // git@osc +git clone https://github.com/inhere/php-console.git // github +``` diff --git a/docs/screenshots/app-command-list.png b/docs/screenshots/app-command-list.png new file mode 100644 index 00000000..05e9d687 Binary files /dev/null and b/docs/screenshots/app-command-list.png differ diff --git a/docs/screenshots/fmt-help-panel.png b/docs/screenshots/fmt-help-panel.png new file mode 100644 index 00000000..660c0681 Binary files /dev/null and b/docs/screenshots/fmt-help-panel.png differ diff --git a/docs/screenshots/fmt-list.png b/docs/screenshots/fmt-list.png new file mode 100644 index 00000000..a3658afb Binary files /dev/null and b/docs/screenshots/fmt-list.png differ diff --git a/docs/screenshots/fmt-multi-list.png b/docs/screenshots/fmt-multi-list.png new file mode 100644 index 00000000..5facf094 Binary files /dev/null and b/docs/screenshots/fmt-multi-list.png differ diff --git a/docs/screenshots/fmt-panel.png b/docs/screenshots/fmt-panel.png new file mode 100644 index 00000000..f89f37d4 Binary files /dev/null and b/docs/screenshots/fmt-panel.png differ diff --git a/docs/screenshots/group-command-help.png b/docs/screenshots/group-command-help.png new file mode 100644 index 00000000..7bcd6b19 Binary files /dev/null and b/docs/screenshots/group-command-help.png differ diff --git a/docs/screenshots/group-command-list.png b/docs/screenshots/group-command-list.png new file mode 100644 index 00000000..c5c5216b Binary files /dev/null and b/docs/screenshots/group-command-list.png differ diff --git a/docs/screenshots/interactive-ask.png b/docs/screenshots/interactive-ask.png new file mode 100644 index 00000000..a988f8d9 Binary files /dev/null and b/docs/screenshots/interactive-ask.png differ diff --git a/docs/screenshots/interactive-limited-ask.png b/docs/screenshots/interactive-limited-ask.png new file mode 100644 index 00000000..88599d64 Binary files /dev/null and b/docs/screenshots/interactive-limited-ask.png differ diff --git a/images/output-color-text.png b/docs/screenshots/output-color-text.png similarity index 100% rename from images/output-color-text.png rename to docs/screenshots/output-color-text.png diff --git a/images/output-format-msg.png b/docs/screenshots/output-format-msg.png similarity index 100% rename from images/output-format-msg.png rename to docs/screenshots/output-format-msg.png diff --git a/docs/screenshots/progress-demo.png b/docs/screenshots/progress-demo.png new file mode 100644 index 00000000..0a1cb72d Binary files /dev/null and b/docs/screenshots/progress-demo.png differ diff --git a/images/table-show.png b/docs/screenshots/table-show.png similarity index 100% rename from images/table-show.png rename to docs/screenshots/table-show.png diff --git a/images/use-arg-position.png b/docs/screenshots/use-definition-args.png similarity index 100% rename from images/use-arg-position.png rename to docs/screenshots/use-definition-args.png diff --git a/docs/show-ascii-font.md b/docs/show-ascii-font.md new file mode 100644 index 00000000..61b7f31d --- /dev/null +++ b/docs/show-ascii-font.md @@ -0,0 +1,11 @@ +# show cli font + +```php + + $name = '404'; + ArtFont::create()->show($name, ArtFont::INTERNAL_GROUP,[ + 'type' => $this->input->getBoolOpt('italic') ? 'italic' : '', + 'style' => $this->input->getOpt('style'), + ]); + +``` diff --git a/docs/something.md b/docs/something.md new file mode 100644 index 00000000..4bd4748d --- /dev/null +++ b/docs/something.md @@ -0,0 +1,41 @@ +# some idea + +## controller + +```php + protected function commandConfigure($definition) + { + // old: own create. + $this->createDefinition()->addArgument(); + + // maybe: get by argument. + $definition->addArgument(); + + // .... + } +``` + +```php + /** + * the group controller metadata. to define name, description + * @return array + */ + public static function metadata() + { + return [ + 'name' => 'model', + 'description' => 'some console command handle for model user data.', + + // for command + 'aliases' => [ + 'i', 'in', + ], + + // for controller + 'aliases' => [ + 'i' => 'install', + 'up' => 'update', + ] + ]; + } +``` \ No newline at end of file diff --git a/examples/sprintf.md b/docs/sprintf.md similarity index 100% rename from examples/sprintf.md rename to docs/sprintf.md diff --git a/examples/DemoCommand.php b/examples/Commands/DemoCommand.php similarity index 90% rename from examples/DemoCommand.php rename to examples/Commands/DemoCommand.php index 9ae6ab5f..83a95171 100644 --- a/examples/DemoCommand.php +++ b/examples/Commands/DemoCommand.php @@ -6,7 +6,7 @@ * Time: 18:58 */ -namespace Inhere\Console\Examples; +namespace Inhere\Console\Examples\Commands; use Inhere\Console\Command; use Inhere\Console\IO\Input; @@ -14,7 +14,7 @@ /** * Class DemoCommand - * @package app\console\commands + * @package Inhere\Console\Examples\Commands */ class DemoCommand extends Command { @@ -23,12 +23,13 @@ class DemoCommand extends Command /** * {@inheritDoc} + * @throws \LogicException */ protected function configure() { $this->createDefinition() ->setDescription(self::getDescription()) - ->setExample($this->handleAnnotationVars('{script} {command} john male 43 --opt1 value1')) + ->setExample($this->parseAnnotationVars('{script} {command} john male 43 --opt1 value1')) ->addArgument('name', Input::ARG_REQUIRED, 'description for the argument [name], is required') ->addArgument('sex', Input::ARG_OPTIONAL, 'description for the argument [sex], is optional') ->addArgument('age', Input::ARG_OPTIONAL, 'description for the argument [age], is optional') diff --git a/examples/TestCommand.php b/examples/Commands/TestCommand.php similarity index 82% rename from examples/TestCommand.php rename to examples/Commands/TestCommand.php index 03e93a8d..76a4bf93 100644 --- a/examples/TestCommand.php +++ b/examples/Commands/TestCommand.php @@ -6,12 +6,13 @@ * Time: 18:58 */ -namespace Inhere\Console\Examples; +namespace Inhere\Console\Examples\Commands; use Inhere\Console\Command; /** * Class Test + * @package Inhere\Console\Examples\Commands */ class TestCommand extends Command { @@ -26,6 +27,8 @@ class TestCommand extends Command * @options * --long,-s option description 1 * --opt option description 2 + * @param $input + * @param $output */ public function execute($input, $output) { diff --git a/examples/HomeController.php b/examples/Controllers/HomeController.php similarity index 53% rename from examples/HomeController.php rename to examples/Controllers/HomeController.php index f9f56e1f..113e9b54 100644 --- a/examples/HomeController.php +++ b/examples/Controllers/HomeController.php @@ -1,51 +1,96 @@ 'index', + 'prg' => 'progress', + 'pgb' => 'progressBar', + 'pwd' => 'password', + 'l' => 'list', + 'h' => 'helpPanel', + 'hl' => 'highlight', + 'hp' => 'helpPanel', + 'af' => 'artFont', + 'ml' => 'multiList', + 'ms' => 'multiSelect', + ]; + } + + protected function init() + { + parent::init(); + + $this->addAnnotationVar('internalFonts', implode(',', ArtFont::getInternalFonts())); + } + + protected function afterExecute() + { + $this->write('after command execute'); + } /** * this is a command's description message * the second line text - * @usage usage message + * @usage {command} [arg ...] [--opt ...] * @arguments - * arg1 argument description 1 - * arg2 argument description 2 + * arg1 argument description 1 + * the second line + * a2,arg2 argument description 2 + * the second line * @options - * --long,-s option description 1 - * --opt option description 2 + * -s, --long option description 1 + * --opt option description 2 * @example example text one * the second line example */ - public function indexCommand() + public function testCommand() { $this->write('hello, welcome!! this is ' . __METHOD__); } /** - * a example for input password on command line - * @usage {fullCommand} + * a example for highlight code + * @options + * --ln With line number + * @param Input $in */ - public function passwdCommand() + public function highlightCommand($in) { - $pwd = $this->askPassword(); + // $file = $this->app->getRootPath() . '/examples/routes.php'; + $file = $this->app->getRootPath() . '/src/Utils/Show.php'; + $src = file_get_contents($file); - $this->write('Your input is:' . $pwd); + $hl = new Highlighter(); + $code = $hl->highlight($src, $in->getBoolOpt('ln')); + + $this->output->writeRaw($code); } /** @@ -60,7 +105,7 @@ public function colorCommand() return 0; } - $this->write('color text output:'); + $this->write('color style text output:'); $styles = $this->output->getStyle()->getStyleNames(); foreach ($styles as $style) { @@ -86,7 +131,31 @@ public function blockMsgCommand() } /** - * a counter example show. It is like progress txt, but no max value. + * output art font text + * @options + * --font Set the art font name(allow: {internalFonts}). + * --italic Set the art font type is italic. + * --style Set the art font style. + * @return int + */ + public function artFontCommand() + { + $name = $this->input->getLongOpt('font', '404'); + + if (!ArtFont::isInternalFont($name)) { + return $this->output->liteError("Your input font name: $name, is not exists. Please use '-h' see allowed."); + } + + ArtFont::create()->show($name, ArtFont::INTERNAL_GROUP,[ + 'type' => $this->input->getBoolOpt('italic') ? 'italic' : '', + 'style' => $this->input->getOpt('style'), + ]); + + return 0; + } + + /** + * dynamic notice message show: counterTxt. It is like progress txt, but no max value. * @example * {script} {command} * @return int @@ -110,7 +179,37 @@ public function counterCommand() } /** - * a progress bar example show + * dynamic notice message show: spinner + */ + public function spinnerCommand() + { + $total = 5000; + + while ($total--) { + Show::spinner(); + usleep(100); + } + + Show::spinner('Done', true); + } + + /** + * dynamic notice message show: pending + */ + public function pendingCommand() + { + $total = 8000; + + while ($total--) { + Show::pending(); + usleep(200); + } + + Show::pending('Done', true); + } + + /** + * a progress bar example show, by Show::progressBar() * @options * --type the progress type, allow: bar,txt. txt * --done-char the done show char. = @@ -135,7 +234,7 @@ public function progressCommand($input) 'signChar' => $input->getOpt('sign-char', '>'), ]); } else { - $bar = $this->output->progressTxt($total, 'Doing gggg ...', 'Done'); + $bar = $this->output->progressTxt($total, 'Doing go g...', 'Done'); } $this->write('Progress:'); @@ -150,13 +249,40 @@ public function progressCommand($input) } /** - * output more format message text + * a progress bar example show, by class ProgressBar + * @throws \LogicException */ - public function fmtMsgCommand() + public function progressBarCommand() + { + $i = 0; + $total = 120; + $bar = new ProgressBar(); + $bar->start(120); + + while ($i <= $total) { + $bar->advance(); + usleep(50000); + $i++; + } + + $bar->finish(); + } + + /** + * output format message: title + */ + public function titleCommand() { $this->output->title('title show'); - echo "\n"; + return 0; + } + + /** + * output format message: section + */ + public function sectionCommand() + { $body = 'If screen size could not be detected, or the indentation is greater than the screen size, the text will not be wrapped.' . 'Word wrap text with indentation to fit the screen size,' . 'Word wrap text with indentation to fit the screen size,' . @@ -167,17 +293,31 @@ public function fmtMsgCommand() 'pos' => 'l' ]); + return 0; + } + + /** + * output format message: panel + */ + public function panelCommand() + { $data = [ 'application version' => '1.2.0', 'system version' => '5.2.3', 'see help' => 'please use php bin/app -h', 'a only value message text', ]; + Show::panel($data, 'panel show', [ - 'borderChar' => '#' + 'borderChar' => '*' ]); + } - echo "\n"; + /** + * output format message: helpPanel + */ + public function helpPanelCommand() + { Show::helpPanel([ Show::HELP_DES => 'a help panel description text. (help panel show)', Show::HELP_USAGE => 'a usage text', @@ -192,6 +332,21 @@ public function fmtMsgCommand() '-h, --help' => 'Display this help message' ], ], false); + } + + /** + * output format message: list + */ + public function listCommand() + { + $list = [ + 'The is a list line 0', + 'The is a list line 1', + 'The is a list line 2', + 'The is a list line 3', + ]; + + Show::aList($list, 'a List show(No key)'); $commands = [ 'version' => 'Show application version information', @@ -199,32 +354,39 @@ public function fmtMsgCommand() 'list' => 'List all group and independent commands', 'a only value message text' ]; - Show::aList($commands, 'a List show'); - Show::table([ - [ - 'id' => 1, - 'name' => 'john', - 'status' => 2, - 'email' => 'john@email.com', + Show::aList($commands, 'a List show(Has key)'); + } + + /** + * output format message: multiList + */ + public function multiListCommand() + { + Show::multiList([ + 'list0' => [ + 'value in the list 0', + 'key' => 'value in the list 0', + 'key1' => 'value1 in the list 0', + 'key2' => 'value2 in the list 0', ], - [ - 'id' => 2, - 'name' => 'tom', - 'status' => 0, - 'email' => 'tom@email.com', + 'list1' => [ + 'key' => 'value in the list 1', + 'key1' => 'value1 in the list 1', + 'key2' => 'value2 in the list 1', + 'value in the list 1', ], - [ - 'id' => 3, - 'name' => 'jack', - 'status' => 1, - 'email' => 'jack-test@email.com', + 'list2' => [ + 'key' => 'value in the list 2', + 'value in the list 2', + 'key1' => 'value1 in the list 2', + 'key2' => 'value2 in the list 2', ], - ], 'table show'); + ]); } /** - * a example for display a table + * output format message: table */ public function tableCommand() { @@ -280,7 +442,7 @@ public function tableCommand() } /** - * a example use padding() for show data + * output format message: padding */ public function paddingCommand() { @@ -294,7 +456,7 @@ public function paddingCommand() } /** - * a example for dump, print, json data + * output format message: dump */ public function jsonCommand() { @@ -323,7 +485,7 @@ public function jsonCommand() $this->output->dump($data); $this->output->write('use print:'); - $this->output->print($data); + $this->output->prints($data); $this->output->write('use json:'); $this->output->json($data); @@ -350,6 +512,7 @@ public function useArgCommand() /** * command `defArgCommand` config + * @throws \LogicException */ protected function defArgConfigure() { @@ -369,24 +532,95 @@ public function defArgCommand() } /** - * use Interact::confirm method + * This is a demo for use Interact::confirm method */ public function confirmCommand() { + // can also: $this->confirm(); $a = Interact::confirm('continue'); - $this->write('you answer is: ' . ($a ? 'yes' : 'no')); + $this->write('Your answer is: ' . ($a ? 'yes' : 'no')); } /** - * example for use Interact::select method + * This is a demo for use Interact::select() method */ public function selectCommand() { $opts = ['john', 'simon', 'rose']; + // can also: $this->select(); $a = Interact::select('you name is', $opts); - $this->write('you answer is: ' . $opts[$a]); + $this->write('Your answer is: ' . $opts[$a]); + } + + /** + * This is a demo for use Interact::multiSelect() method + */ + public function multiSelectCommand() + { + $opts = ['john', 'simon', 'rose', 'tom']; + + // can also: $a = Interact::multiSelect('Your friends are', $opts); + $a = $this->multiSelect('Your friends are', $opts); + + $this->write('Your answer is: ' . json_encode($a)); + } + + /** + * This is a demo for use Interact::ask() method + */ + public function askCommand() + { + $a = Interact::ask('you name is: ', null, function ($val, &$err) { + if (!preg_match('/^\w{2,}$/', $val)) { + $err = 'Your input must match /^\w{2,}$/'; + + return false; + } + + return true; + }); + + $this->write('Your answer is: ' . $a); + } + + /** + * This is a demo for use Interact::limitedAsk() method + * @options + * --nv Not use validator. + * --limit limit times.(default: 3) + */ + public function limitedAskCommand() + { + $times = (int)$this->input->getOpt('limit', 3); + + if ($this->input->getBoolOpt('nv')) { + $a = Interact::limitedAsk('you name is: ', null, null, $times); + } else { + $a = Interact::limitedAsk('you name is: ', null, function ($val) { + if (!preg_match('/^\w{2,}$/', $val)) { + Show::error('Your input must match /^\w{2,}$/'); + + return false; + } + + return true; + }, $times); + } + + $this->write('Your answer is: ' . $a); + } + + /** + * This is a demo for input password. use: Interact::askPassword() + * @usage {fullCommand} + */ + public function passwordCommand() + { + $pwd = $this->askPassword(); + + $this->write('Your input is: ' . $pwd); } /** @@ -406,16 +640,16 @@ public function envCommand() } /** - * download a file to local + * This is a demo for download a file to local * @usage {command} url=url saveTo=[saveAs] type=[bar|text] - * @example {command} url=https://github.com/inhere/php-librarys/archive/v2.0.1.zip type=bar + * @example {command} url=https://github.com/inhere/php-console/archive/master.zip type=bar */ public function downCommand() { $url = $this->input->getArg('url'); if (!$url) { - Show::error('Please input you want to downloaded file url, use: url=[url]', 1); + $this->output->liteError('Please input you want to downloaded file url, use: url=[url]', 1); } $saveAs = $this->input->getArg('saveAs'); @@ -425,7 +659,7 @@ public function downCommand() $saveAs = __DIR__ . '/' . basename($url); } - $goon = Interact::confirm("Now, will download $url to $saveAs, go on"); + $goon = Interact::confirm("Now, will download $url \nto dir $saveAs, go on"); if (!$goon) { Show::notice('Quit download, Bye!'); @@ -433,47 +667,44 @@ public function downCommand() return 0; } - $d = Download::down($url, $saveAs, $type); - - echo Helper::dumpVars($d); + Download::down($url, $saveAs, $type); + // $d = Download::down($url, $saveAs, $type); + // echo Helper::dumpVars($d); return 0; } /** - * show cursor move on the screen + * This is a demo for show cursor move on the Terminal screen */ public function cursorCommand() { $this->write('hello, this in ' . __METHOD__); - - // $this->output->panel($_SERVER, 'Server information', ''); - $this->write('this is a message text.', false); sleep(1); - AnsiCode::make()->cursor(AnsiCode::CURSOR_BACKWARD, 6); + Terminal::make()->cursor(Terminal::CURSOR_BACKWARD, 6); sleep(1); - AnsiCode::make()->cursor(AnsiCode::CURSOR_FORWARD, 3); + Terminal::make()->cursor(Terminal::CURSOR_FORWARD, 3); sleep(1); - AnsiCode::make()->cursor(AnsiCode::CURSOR_BACKWARD, 2); + Terminal::make()->cursor(Terminal::CURSOR_BACKWARD, 2); sleep(2); - AnsiCode::make()->screen(AnsiCode::CLEAR_LINE, 3); + Terminal::make()->screen(Terminal::CLEAR_LINE, 3); $this->write('after 2s scroll down 3 row.'); sleep(2); - AnsiCode::make()->screen(AnsiCode::SCROLL_DOWN, 3); + Terminal::make()->screen(Terminal::SCROLL_DOWN, 3); $this->write('after 3s clear screen.'); sleep(3); - AnsiCode::make()->screen(AnsiCode::CLEAR); + Terminal::make()->screen(Terminal::CLEAR); } } diff --git a/examples/Controllers/ProcessController.php b/examples/Controllers/ProcessController.php new file mode 100644 index 00000000..162c12fe --- /dev/null +++ b/examples/Controllers/ProcessController.php @@ -0,0 +1,130 @@ + 'childProcess', + 'mpr' => 'multiProcess', + 'dr' => 'daemonRun', + 'rs' => 'runScript', + 'rb' => 'runInBackground', + ]; + } + + /** + * simple process example for child-process + */ + public function runScriptCommand() + { + /*$script = '';*/ + $script = ''; + + // $tmpDir = CliUtil::getTempDir(); + // $tmpFile = $tmpDir . '/' . md5($script) . '.php'; + // file_put_contents($tmpFile, $script); + + $descriptorSpec = [ + 0 => ['pipe', 'r'], // 标准输入,子进程从此管道中读取数据 + 1 => ['pipe', 'w'], // 标准输出,子进程向此管道中写入数据 + 2 => ['file', $this->app->getRootPath() . '/examples/tmp/error-output.log', 'a'] // 标准错误,写入到一个文件 + ]; + + $process = proc_open('php', $descriptorSpec, $pipes); + + if (\is_resource($process)) { + // $pipes 现在看起来是这样的: + // 0 => 可以向子进程标准输入写入的句柄 + // 1 => 可以从子进程标准输出读取的句柄 + // 错误输出将被追加到文件 error-output.txt + + fwrite($pipes[0], $script); + fclose($pipes[0]); + + $result = stream_get_contents($pipes[1]); + + fclose($pipes[1]); + + $this->write("RESULT:\n" . $result); + + // 切记:在调用 proc_close 之前关闭所有的管道以避免死锁。 + $retVal = proc_close($process); + + echo "command returned $retVal\n"; + } + } + + /** + * simple process example for child-process + */ + public function childProcessCommand() + { + $ret = ProcessUtil::create(function ($pid) { + echo "print in process $pid"; + + sleep(5); + }); + + if ($ret === false) { + $this->output->liteError('current env is not support process create.'); + } + } + + /** + * simple process example for daemon run + */ + public function daemonRunCommand() + { + $ret = ProcessUtil::daemonRun(function ($pid){ + $this->output->info("will running background by new process: $pid"); + }); + + if ($ret === false) { + $this->output->liteError('current env is not support process create.'); + } + } + + /** + * simple process example for run In Background + */ + public function runInBackgroundCommand() + { + $script = ''; + $ret = ProcessUtil::runInBackground("php $script"); + + if ($ret === false) { + $this->output->liteError('current env is not support process create.'); + } + } + + /** + * simple process example for multi-process + * @options + * + */ + public function multiProcessCommand() + { + + } +} diff --git a/examples/app b/examples/app index b174373d..13ed035d 100644 --- a/examples/app +++ b/examples/app @@ -3,14 +3,23 @@ define('BASE_PATH', dirname(__DIR__)); -require __DIR__ . '/s-autoload.php'; +require dirname(__DIR__) . '/tests/boot.php'; // create app instance $app = new \Inhere\Console\Application([ 'debug' => true, - 'rootPath' => BASE_PATH, + 'rootPath' => dirname(__DIR__), ]); +$app->setLogo(" + ________ ____ ___ ___ __ _ + / ____/ / / _/ / | ____ ____ / (_)________ _/ /_(_)___ ____ + / / / / / / / /| | / __ \/ __ \/ / / ___/ __ `/ __/ / __ \/ __ \ + / /___/ /____/ / / ___ |/ /_/ / /_/ / / / /__/ /_/ / /_/ / /_/ / / / / + \____/_____/___/ /_/ |_/ .___/ .___/_/_/\___/\__,_/\__/_/\____/_/ /_/ + /_/ /_/ +", 'success'); + // require dirname(__DIR__) . '/boot/cli-services.php'; require __DIR__ . '/routes.php'; diff --git a/examples/baks/OldInput.php b/examples/demo/OldInput.php similarity index 96% rename from examples/baks/OldInput.php rename to examples/demo/OldInput.php index fa34bd35..24611304 100644 --- a/examples/baks/OldInput.php +++ b/examples/demo/OldInput.php @@ -1,6 +1,6 @@ strlen($chars) - 1) { + $spinner = 0; + } + } + } + + /** + * Uses `stty` to hide input/output completely. + * @param boolean $hidden Will hide/show the next data. Defaults to true. + */ + public static function hide($hidden = true) + { + system( 'stty ' . ( $hidden? '-echo' : 'echo' ) ); + } + + /** + * Prompts the user for input. Optionally masking it. + * + * @param string $prompt The prompt to show the user + * @param bool $masked If true, the users input will not be shown. e.g. password input + * @param int $limit The maximum amount of input to accept + * @return string + */ + public static function prompt($prompt, $masked=false, $limit=100) + { + echo "$prompt: "; + if ($masked) { + `stty -echo`; // disable shell echo + } + $buffer = ""; + $char = ""; + $f = fopen('php://stdin', 'r'); + while (strlen($buffer) < $limit) { + $char = fread($f, 1); + if ($char === "\n" || $char === "\r") { + break; + } + $buffer.= $char; + } + if ($masked) { + `stty echo`; // enable shell echo + echo "\n"; + } + return $buffer; + } +} + +Status::hide(); +echo 'Password: '; +$input = fgets(STDIN); +Status::hide(false); +echo $input; +die; +$total = random_int(5000, 10000); +for ($x=1; $x<=$total; $x++) { + Status::spinner(); + usleep(50); +} + +Status::clearLine(); + +// +// $answer = Status::prompt("What is the secret word?", 0); +// if ($answer == "secret") { +// echo "Yay! You got it!"; +// } else { +// echo "Boo! That is wrong!"; +// } \ No newline at end of file diff --git a/examples/baks/color.php b/examples/demo/color.php similarity index 100% rename from examples/baks/color.php rename to examples/demo/color.php diff --git a/examples/tests/phar.php b/examples/demo/phar.php similarity index 100% rename from examples/tests/phar.php rename to examples/demo/phar.php diff --git a/examples/baks/progress_bar.php b/examples/demo/progress_bar.php similarity index 100% rename from examples/baks/progress_bar.php rename to examples/demo/progress_bar.php diff --git a/examples/baks/progress_bar1.php b/examples/demo/progress_bar1.php similarity index 100% rename from examples/baks/progress_bar1.php rename to examples/demo/progress_bar1.php diff --git a/examples/baks/progress_bar3.php b/examples/demo/progress_bar3.php similarity index 100% rename from examples/baks/progress_bar3.php rename to examples/demo/progress_bar3.php diff --git a/examples/baks/readline.php b/examples/demo/readline.php similarity index 100% rename from examples/baks/readline.php rename to examples/demo/readline.php diff --git a/examples/baks/rl_callback.php b/examples/demo/rl_callback.php similarity index 100% rename from examples/baks/rl_callback.php rename to examples/demo/rl_callback.php diff --git a/examples/baks/rl_history.php b/examples/demo/rl_history.php similarity index 100% rename from examples/baks/rl_history.php rename to examples/demo/rl_history.php diff --git a/examples/baks/sf2_color.php b/examples/demo/sf2_color.php similarity index 100% rename from examples/baks/sf2_color.php rename to examples/demo/sf2_color.php diff --git a/examples/home b/examples/home index 4c3db460..5e621150 100644 --- a/examples/home +++ b/examples/home @@ -1,19 +1,27 @@ #!/usr/env/php true, - 'rootPath' => BASE_PATH, -]); +$in = new \Inhere\Console\IO\Input(); +$ctrl = new HomeController($in, new \Inhere\Console\IO\Output()); +$ctrl->setStandAlone(); -$app->controller('home', HomeController::class); +exit($ctrl->run($in->getCommand())); -exit( - (int)$app->runAction('home', $app->getInput()->getCommand(), false, true) -); +// can also: + +// $app = new \Inhere\Console\Application([ +// 'debug' => true, +// 'rootPath' => BASE_PATH, +// ]); +// +// $app->controller('home', HomeController::class); +// +// exit( +// (int)$app->runAction('home', $app->getInput()->getCommand(), false, true) +// ); diff --git a/examples/liteApp b/examples/liteApp index e988fc25..95f6087e 100644 --- a/examples/liteApp +++ b/examples/liteApp @@ -3,10 +3,10 @@ define('BASE_PATH', dirname(__DIR__)); -require __DIR__ . '/s-autoload.php'; +require dirname(__DIR__) . '/tests/boot.php'; // create app instance -$app = new \Inhere\Console\LiteApplication; +$app = new \Inhere\Console\LiteApp; // register commands $app->addCommand('test', function () { diff --git a/examples/routes.php b/examples/routes.php index 6b5f5cc3..5d1fd80a 100644 --- a/examples/routes.php +++ b/examples/routes.php @@ -4,40 +4,34 @@ * User: inhere * Date: 2016年12月7日 * Time: 12:46 - * * @var Inhere\Console\Application $app */ use Inhere\Console\BuiltIn\PharController; -use Inhere\Console\Examples\HomeController; -use Inhere\Console\Examples\TestCommand; +use Inhere\Console\Examples\Commands\DemoCommand; +use Inhere\Console\Examples\Commands\TestCommand; +use Inhere\Console\Examples\Controllers\HomeController; +use Inhere\Console\Examples\Controllers\ProcessController; use Inhere\Console\IO\Input; use Inhere\Console\IO\Output; -use Inhere\Console\Utils\ProgressBar; -$app->command(\Inhere\Console\Examples\DemoCommand::class); +$app->command(DemoCommand::class); $app->command('exam', function (Input $in, Output $out) { $cmd = $in->getCommand(); $out->info('hello, this is a test command: ' . $cmd); -}); +}, 'a description message'); -$app->command('test', TestCommand::class); -$app->command('prg', function () { - $i = 0; - $total = 120; - $bar = new ProgressBar(); - $bar->start(120); +$app->command('test', TestCommand::class, [ + 'aliases' => ['t'] +]); - while ($i <= $total) { - $bar->advance(); - usleep(50000); - $i++; - } +$app->controller(PharController::class); - $bar->finish(); +$app->controller('home', HomeController::class, [ + 'aliases' => ['h'] +]); -}, 'a description message'); - -$app->controller('home', HomeController::class); -$app->controller(PharController::class); +$app->controller(ProcessController::class, null, [ + 'aliases' => 'prc' +]); diff --git a/examples/s-autoload.php b/examples/s-autoload.php deleted file mode 100644 index 8f428150..00000000 --- a/examples/s-autoload.php +++ /dev/null @@ -1,31 +0,0 @@ -validateName($name, true); - if (!class_exists($class)) { - throw new \InvalidArgumentException("The console controller class [$class] not exists!"); + throw new \InvalidArgumentException("The console controller class [{$class}] not exists!"); } - if (!is_subclass_of($class, Controller::class)) { throw new \InvalidArgumentException('The console controller class must is subclass of the: ' . Controller::class); } - $this->controllers[$name] = $class; + if (!$option) { + return $this; + } + // have option information + if (\is_string($option)) { + $this->addCommandMessage($name, $option); + } elseif (\is_array($option)) { + $this->addCommandAliases($name, isset($option['aliases']) ? $option['aliases'] : null); + $this->addCommandMessage($name, isset($option['description']) ? $option['description'] : null); + } return $this; } @@ -62,13 +66,14 @@ public function controller(string $name, string $class = null) * @see Application::controller() * @throws \InvalidArgumentException */ - public function addController(string $name, string $class = null) + public function addController($name, $class = null) { return $this->controller($name, $class); } /** * @param array $controllers + * @throws \InvalidArgumentException */ public function controllers(array $controllers) { @@ -79,48 +84,45 @@ public function controllers(array $controllers) * Register a app independent console command * @param string|Command $name * @param string|\Closure|Command $handler - * @param null|string $description + * @param null|array|string $option * @return $this * @throws \InvalidArgumentException */ - public function command(string $name, $handler = null, $description = null) + public function command($name, $handler = null, $option = null) { if (!$handler && class_exists($name)) { /** @var Command $name */ $handler = $name; $name = $name::getName(); } - if (!$name || !$handler) { - throw new \InvalidArgumentException("Command 'name' and 'handler' not allowed to is empty! name: $name"); + throw new \InvalidArgumentException("Command 'name' and 'handler' not allowed to is empty! name: {$name}"); } - $this->validateName($name); - if (isset($this->commands[$name])) { - throw new \InvalidArgumentException("Command '$name' have been registered!"); + throw new \InvalidArgumentException("Command '{$name}' have been registered!"); } - if (\is_string($handler)) { if (!class_exists($handler)) { - throw new \InvalidArgumentException("The console command class [$handler] not exists!"); + throw new \InvalidArgumentException("The console command class [{$handler}] not exists!"); } - if (!is_subclass_of($handler, Command::class)) { throw new \InvalidArgumentException('The console command class must is subclass of the: ' . Command::class); } } elseif (!\is_object($handler) || !method_exists($handler, '__invoke')) { - throw new \InvalidArgumentException(sprintf( - 'The console command handler must is an subclass of %s OR a Closure OR a object have method __invoke()', - Command::class - )); + throw new \InvalidArgumentException(sprintf('The console command handler must is an subclass of %s OR a Closure OR a object have method __invoke()', Command::class)); } - // is an class name string $this->commands[$name] = $handler; - - if ($description) { - $this->addCommandMessage($name, $description); + if (!$option) { + return $this; + } + // have option information + if (\is_string($option)) { + $this->addCommandMessage($name, $option); + } elseif (\is_array($option)) { + $this->addCommandAliases($name, isset($option['aliases']) ? $option['aliases'] : null); + $this->addCommandMessage($name, isset($option['description']) ? $option['description'] : null); } return $this; @@ -128,6 +130,7 @@ public function command(string $name, $handler = null, $description = null) /** * @param array $commands + * @throws \InvalidArgumentException */ public function commands(array $commands) { @@ -141,7 +144,7 @@ public function commands(array $commands) * @return $this * @throws \InvalidArgumentException */ - public function addCommand(string $name, $handler = null) + public function addCommand($name, $handler = null) { return $this->command($name, $handler); } @@ -153,7 +156,7 @@ public function addCommand(string $name, $handler = null) * @return static * @throws \InvalidArgumentException */ - public function addGroup(string $name, string $controller = null) + public function addGroup($name, $controller = null) { return $this->controller($name, $controller); } @@ -165,11 +168,10 @@ public function addGroup(string $name, string $controller = null) * @return $this * @throws \InvalidArgumentException */ - public function registerCommands(string $namespace, string $basePath) + public function registerCommands($namespace, $basePath) { $length = \strlen($basePath) + 1; $iterator = Helper::recursiveDirectoryIterator($basePath, $this->getFileFilter()); - foreach ($iterator as $file) { $class = $namespace . '\\' . \substr($file, $length, -4); $this->addCommand($class); @@ -185,11 +187,10 @@ public function registerCommands(string $namespace, string $basePath) * @return $this * @throws \InvalidArgumentException */ - public function registerGroups(string $namespace, string $basePath) + public function registerGroups($namespace, $basePath) { $length = \strlen($basePath) + 1; $iterator = Helper::recursiveDirectoryIterator($basePath, $this->getFileFilter()); - foreach ($iterator as $file) { $class = $namespace . '\\' . \substr($file, $length, -4); $this->addController($class); @@ -205,12 +206,10 @@ protected function getFileFilter() { return function (\SplFileInfo $f) { $name = $f->getFilename(); - // Skip hidden files and directories. if ($name[0] === '.') { return false; } - // go on read sub-dir if ($f->isDir()) { return true; @@ -220,56 +219,38 @@ protected function getFileFilter() return $f->isFile() && \substr($name, -4) === '.php'; }; } - /**************************************************************************** * dispatch and run console controller/command ****************************************************************************/ - /** * @inheritdoc * @throws \InvalidArgumentException */ protected function dispatch($name) { - $sep = $this->delimiter ?: '/'; - - //// is a command name - - if ($this->isCommand($name)) { - return $this->runCommand($name, true); + $sep = $this->delimiter ?: ':'; + // maybe is a command name + $realName = $this->getRealCommandName($name); + if ($this->isCommand($realName)) { + return $this->runCommand($realName, true); } - - //// is a controller name - + // maybe is a controller name $action = ''; - - // like 'home/index' + // like 'home:index' if (strpos($name, $sep)> 0) { - $input = array_filter(explode($sep, $name)); + $input = array_values(array_filter(explode($sep, $name))); list($name, $action) = \count($input)> 2 ? array_splice($input, 2) : $input; } - - if ($this->isController($name)) { - return $this->runAction($name, $action, true); + $realName = $this->getRealCommandName($name); + if ($this->isController($realName)) { + return $this->runAction($realName, $action, true); } - // command not found if (true !== self::fire(self::ON_NOT_FOUND, [$this])) { - $this->output->liteError("The console command '{$name}' not exists!"); - - // find similar command names by similar_text() - $similar = []; + $this->output->liteError("The command '{$name}' is not exists in the console application!"); $commands = array_merge($this->getControllerNames(), $this->getCommandNames()); - - foreach ($commands as $command) { - similar_text($name, $command, $percent); - - if (45 <= (int)$percent) { - $similar[] = $command; - } - } - - if ($similar) { + // find similar command names by similar_text() + if ($similar = Helper::findSimilar($name, $commands)) { $this->write(sprintf("\nMaybe what you mean is:\n %s", implode(', ', $similar))); } else { $this->showCommandList(false); @@ -290,26 +271,26 @@ public function runCommand($name, $believable = false) { // if $believable = true, will skip check. if (!$believable && $this->isCommand($name)) { - throw new \InvalidArgumentException("The console independent-command [$name] not exists!"); + throw new \InvalidArgumentException("The console independent-command [{$name}] not exists!"); } - /** @var \Closure|string $handler Command class */ $handler = $this->commands[$name]; - if (\is_object($handler) && method_exists($handler, '__invoke')) { + if ($this->input->getSameOpt(['h', 'help'])) { + $des = $this->getCommandMessage($name, 'No command description message.'); + + return $this->output->write($des); + } $status = $handler($this->input, $this->output); } else { if (!class_exists($handler)) { - throw new \InvalidArgumentException("The console command class [$handler] not exists!"); + throw new \InvalidArgumentException("The console command class [{$handler}] not exists!"); } - /** @var Command $object */ $object = new $handler($this->input, $this->output); - - if (!($object instanceof Command)) { - throw new \InvalidArgumentException("The console command class [$handler] must instanceof the " . Command::class); + if (!$object instanceof Command) { + throw new \InvalidArgumentException("The console command class [{$handler}] must instanceof the " . Command::class); } - $object::setName($name); $object->setApp($this); $status = $object->run(); @@ -330,23 +311,18 @@ public function runAction($name, $action, $believable = false, $standAlone = fal { // if $believable = true, will skip check. if (!$believable && !$this->isController($name)) { - throw new \InvalidArgumentException("The console controller-command [$name] not exists!"); + throw new \InvalidArgumentException("The console controller-command [{$name}] not exists!"); } - // Controller class $controller = $this->controllers[$name]; - if (!class_exists($controller)) { - throw new \InvalidArgumentException("The console controller class [$controller] not exists!"); + throw new \InvalidArgumentException("The console controller class [{$controller}] not exists!"); } - /** @var Controller $object */ $object = new $controller($this->input, $this->output); - - if (!($object instanceof Controller)) { - throw new \InvalidArgumentException("The console controller class [$object] must instanceof the " . Controller::class); + if (!$object instanceof Controller) { + throw new \InvalidArgumentException("The console controller class [{$object}] must instanceof the " . Controller::class); } - $object::setName($name); $object->setApp($this); $object->setDelimiter($this->delimiter); @@ -354,4 +330,4 @@ public function runAction($name, $action, $believable = false, $standAlone = fal return $object->run($action); } -} +} \ No newline at end of file diff --git a/src/Base/AbstractApplication.php b/src/Base/AbstractApplication.php index 535370a5..c4ced377 100644 --- a/src/Base/AbstractApplication.php +++ b/src/Base/AbstractApplication.php @@ -1,4 +1,5 @@ 'Show application version information', - 'help' => 'Show application help information', - 'list' => 'List all group and independent commands', - ]; - + use InputOutputAwareTrait, SimpleEventTrait; + /** @var array */ + protected static $internalCommands = ['version' => 'Show application version information', 'help' => 'Show application help information', 'list' => 'List all group and independent commands']; + /** @var array */ + protected static $internalOptions = ['--debug' => 'Setting the application runtime debug level', '--profile' => 'Display timing and memory usage information', '--no-color' => 'Disable color/ANSI for message output', '-h, --help' => 'Display this help message', '-V, --version' => 'Show application version information']; /** - * @var array - */ - protected static $internalOptions = [ - '--debug' => 'Setting the application runtime debug level', - '--profile' => 'Display timing and memory usage information', - '--no-color' => 'Disable color/ANSI for message output', - '-h, --help' => 'Display this help message', - '-V, --version' => 'Show application version information', - ]; - - /** - * app meta config + * application meta info * @var array */ private $meta = [ - 'name' => 'My Console', + 'name' => 'My Console Application', 'debug' => false, 'profile' => false, 'version' => '0.5.1', @@ -59,25 +44,24 @@ abstract class AbstractApplication implements ApplicationInterface // 'timeZone' => 'Asia/Shanghai', // 'env' => 'pdt', // dev test pdt // 'charset' => 'UTF-8', - + 'logoText' => '', + 'logoStyle' => 'info', // runtime stats '_stats' => [], ]; - /** @var string Command delimiter. e.g dev:serve */ - public $delimiter = ':'; // '/' ':' - - /** @var array The group commands */ - protected $controllers = []; - + public $delimiter = ':'; + // '/' ':' + /** @var string Current command name */ + private $commandName; + /** @var array Some message for command */ + private $commandMessages = []; + /** @var array Save command aliases */ + private $commandAliases = []; /** @var array The independent commands */ protected $commands = []; - - /** @var array */ - private $commandMessages = []; - - /** @var string */ - private $commandName; + /** @var array The group commands */ + protected $controllers = []; /** * App constructor. @@ -89,20 +73,14 @@ public function __construct(array $meta = [], Input $input = null, Output $outpu { $this->runtimeCheck(); $this->setMeta($meta); - $this->input = $input ?: new Input(); $this->output = $output ?: new Output(); - $this->init(); } protected function init() { - $this->meta['_stats'] = [ - 'startTime' => microtime(1), - 'startMemory' => memory_get_usage(true), - ]; - + $this->meta['_stats'] = ['startTime' => microtime(1), 'startMemory' => memory_get_usage(true)]; $this->commandName = $this->input->getCommand(); set_exception_handler([$this, 'handleException']); } @@ -110,7 +88,6 @@ protected function init() /********************************************************** * app run **********************************************************/ - protected function prepareRun() { // date_default_timezone_set($this->config('timeZone', 'UTC')); @@ -118,7 +95,8 @@ protected function prepareRun() } protected function beforeRun() - {} + { + } /** * run app @@ -127,14 +105,11 @@ protected function beforeRun() public function run($exit = true) { $command = trim($this->input->getCommand(), $this->delimiter); - $this->prepareRun(); $this->filterSpecialCommand($command); - // call 'onBeforeRun' service, if it is registered. self::fire(self::ON_BEFORE_RUN, [$this]); $this->beforeRun(); - // do run ... try { $returnCode = $this->dispatch($command); @@ -143,13 +118,10 @@ public function run($exit = true) $returnCode = $e->getCode() === 0 ? $e->getLine() : $e->getCode(); $this->handleException($e); } - $this->meta['_stats']['endTime'] = microtime(1); - // call 'onAfterRun' service, if it is registered. self::fire(self::ON_AFTER_RUN, [$this]); $this->afterRun(); - if ($exit) { $this->stop((int)$returnCode); } @@ -160,19 +132,19 @@ public function run($exit = true) * @param string $command A command name * @return int|mixed */ - abstract protected function dispatch($command); + protected abstract function dispatch($command); /** * run a independent command * {@inheritdoc} */ - abstract public function runCommand($name, $believable = false); + public abstract function runCommand($name, $believable = false); /** * run a controller's action * {@inheritdoc} */ - abstract public function runAction($name, $action, $believable = false, $standAlone = false); + public abstract function runAction($name, $action, $believable = false, $standAlone = false); protected function afterRun() { @@ -180,7 +152,7 @@ protected function afterRun() if ($this->isProfile()) { $title = '---------- Runtime Stats(profile=true) ----------'; $stats = $this->meta['_stats']; - $this->meta['_stats'] = Helper::runtime($stats['startTime'], $stats['startMemory'], $stats); + $this->meta['_stats'] = FormatUtil::runtime($stats['startTime'], $stats['startMemory'], $stats); $this->output->write(''); $this->output->aList($this->meta['_stats'], $title); } @@ -193,14 +165,11 @@ public function stop($code = 0) { // call 'onAppStop' service, if it is registered. self::fire(self::ON_STOP_RUN, [$this]); - exit((int)$code); } - /********************************************************** * helper method for the application **********************************************************/ - /** * runtime env check */ @@ -209,9 +178,7 @@ protected function runtimeCheck() // check env if (!\in_array(PHP_SAPI, ['cli', 'cli-server'], true)) { header('HTTP/1.1 403 Forbidden'); - exit(" 403 Forbidden \n\n" - . " current environment is CLI. \n" - . " :( Sorry! Run this script is only allowed in the terminal environment!\n,You are not allowed to access this file.\n"); + exit(" 403 Forbidden \n\n" . " current environment is CLI. \n" . " :( Sorry! Run this script is only allowed in the terminal environment!\n,You are not allowed to access this file.\n"); } } @@ -222,18 +189,20 @@ protected function runtimeCheck() public function handleException($e) { $type = $e instanceof \Error ? 'Error' : 'Exception'; - $title = ":( OO ... An $type Occurred!"; + $title = ":( OO ... An {$type} Occurred!"; $this->logError($e); - // open debug, throw exception if ($this->isDebug()) { $tpl = <<$title + {$title} Message %s At File %s line %d -Catch by %s()\n -Code Trace:\n%s\n +Catch by %s() + +Code Trace: +%s + ERR; $message = sprintf( $tpl, @@ -244,11 +213,9 @@ public function handleException($e) __METHOD__, $e->getTraceAsString() ); - if ($this->meta['hideRootPath'] && ($rootPath = $this->meta['rootPath'])) { $message = str_replace($rootPath, '{ROOT}', $message); } - $this->output->write($message, false); } else { // simple output @@ -273,18 +240,14 @@ protected function filterSpecialCommand($command) if ($this->input->getSameOpt(['V', 'version'])) { $this->showVersionInfo(); } - if ($this->input->getSameOpt(['h', 'help'])) { $this->showHelpInfo(); } } - if ($this->input->getSameOpt(['no-color'])) { Style::setNoColor(); } - $command = $command ?: 'list'; - switch ($command) { case 'help': $this->showHelpInfo(true, $this->input->getFirstArg()); @@ -303,29 +266,25 @@ protected function filterSpecialCommand($command) * @param bool $isGroup * @throws \InvalidArgumentException */ - protected function validateName(string $name, $isGroup = false) + protected function validateName($name, $isGroup = false) { - $pattern = $isGroup ? '/^[a-z][\w-]+$/' : '/^[a-z][\w-]*:?([a-z][\w-]+)?$/'; - + $pattern = $isGroup ? '/^[a-z][\\w-]+$/' : '/^[a-z][\\w-]*:?([a-z][\\w-]+)?$/'; if (1 !== preg_match($pattern, $name)) { throw new \InvalidArgumentException('The command name is must match: ' . $pattern); } - if ($this->isInternalCommand($name)) { - throw new \InvalidArgumentException("The command name [$name] is not allowed. It is a built in command."); + throw new \InvalidArgumentException("The command name [{$name}] is not allowed. It is a built in command."); } } - /*************************************************************************** * some information for the application ***************************************************************************/ - /** * show the application help information * @param bool $quit * @param string $command */ - public function showHelpInfo($quit = true, string $command = null) + public function showHelpInfo($quit = true, $command = null) { // display help for a special command if ($command) { @@ -335,19 +294,9 @@ public function showHelpInfo($quit = true, string $command = null) $this->dispatch($command); $this->stop(); } - $script = $this->input->getScript(); $sep = $this->delimiter; - - $this->output->helpPanel([ - 'usage' => "$script {command} [arg0 arg1=value1 arg2=value2 ...] [--opt -v -h ...]", - 'example' => [ - "$script test (run a independent command)", - "$script home{$sep}index (run a command of the group)", - "$script help {command} (see a command help information)", - "$script home{$sep}index -h (see a command help of the group)", - ] - ], $quit); + $this->output->helpPanel(['usage' => "{$script} {command} [arg0 arg1=value1 arg2=value2 ...] [--opt -v -h ...]", 'example' => ["{$script} test (run a independent command)", "{$script} home{$sep}index (run a command of the group)", "{$script} help {command} (see a command help information)", "{$script} home{$sep}index -h (see a command help of the group)"]], $quit); } /** @@ -356,23 +305,18 @@ public function showHelpInfo($quit = true, string $command = null) */ public function showVersionInfo($quit = true) { + $os = PHP_OS; $date = date('Y.m.d'); + $logo = ''; $name = $this->getMeta('name', 'Console Application'); $version = $this->getMeta('version', 'Unknown'); $publishAt = $this->getMeta('publishAt', 'Unknown'); $updateAt = $this->getMeta('updateAt', 'Unknown'); $phpVersion = PHP_VERSION; - $os = PHP_OS; - - $this->output->aList([ - "\n {$name}, Version $version\n", - 'System Info' => "PHP version $phpVersion, on $os system", - 'Application Info' => "Update at $updateAt, publish at $publishAt(current $date)", - ], null, [ - 'leftChar' => '', - 'sepChar' => ' : ' - ]); - + if ($logoTxt = $this->getLogoText()) { + $logo = Helper::wrapTag($logoTxt, $this->getLogoStyle()); + } + $this->output->aList(["{$logo}\n {$name}, Version {$version}\n", 'System Info' => "PHP version {$phpVersion}, on {$os} system", 'Application Info' => "Update at {$updateAt}, publish at {$publishAt}(current {$date})"], null, ['leftChar' => '', 'sepChar' => ' : ']); $quit && $this->stop(); } @@ -386,74 +330,53 @@ public function showCommandList($quit = true) $hasGroup = $hasCommand = false; $controllerArr = $commandArr = []; $desPlaceholder = 'No description of the command'; - // all console controllers - $controllerArr[] = PHP_EOL . '- Group Commands'; + $controllerArr[] = PHP_EOL . '- Group Commands'; $controllers = $this->controllers; ksort($controllers); - foreach ($controllers as $name => $controller) { $hasGroup = true; /** @var AbstractCommand $controller */ - $controllerArr[$name] = $controller::getDescription() ?: $desPlaceholder; + $desc = $controller::getDescription() ?: $desPlaceholder; + $aliases = $this->getCommandAliases($name); + $extra = $aliases ? Helper::wrapTag(' [alias: ' . implode(',', $aliases) . ']', 'info') : ''; + $controllerArr[$name] = $desc . $extra; } - if (!$hasGroup) { $controllerArr[] = '... No register any group command(controller)'; } - - // all independent commands + // all independent commands, Independent, Single, Alone $commands = $this->commands; - $commandArr[] = PHP_EOL . '- Independent Commands'; + $commandArr[] = PHP_EOL . '- Alone Commands'; ksort($commands); foreach ($commands as $name => $command) { $desc = $desPlaceholder; $hasCommand = true; - /** @var AbstractCommand $command */ if (is_subclass_of($command, CommandInterface::class)) { $desc = $command::getDescription() ?: $desPlaceholder; - } else if ($msg = $this->getCommandMessage($name)) { + } elseif ($msg = $this->getCommandMessage($name)) { $desc = $msg; - } else if (\is_string($command)) { + } elseif (\is_string($command)) { $desc = 'A handler : ' . $command; - } else if (\is_object($command)) { + } elseif (\is_object($command)) { $desc = 'A handler by ' . \get_class($command); } - - $commandArr[$name] = $desc; + $aliases = $this->getCommandAliases($name); + $extra = $aliases ? Helper::wrapTag(' [alias: ' . implode(',', $aliases) . ']', 'info') : ''; + $commandArr[$name] = $desc . $extra; } - if (!$hasCommand) { $commandArr[] = '... No register any group command(controller)'; } - // built in commands $internalCommands = static::$internalCommands; ksort($internalCommands); - array_unshift($internalCommands, "\n- Internal Commands"); - - $this->output->mList([ - //'There are all console controllers and independent commands.', - 'Usage:' => "$script {command} [arg0 arg1=value1 arg2=value2 ...] [--opt -v -h ...]", - 'Options:' => self::$internalOptions, - 'Available Commands:' => array_merge($controllerArr, $commandArr, $internalCommands), - //'Independent Commands:' => $commandArr ?: '... No register any independent command', - // 'Internal Commands:' => $internalCommands, - ]); - - // $this->output->mList([ - // //'There are all console controllers and independent commands.', - // 'Usage:' => "$script {command} [arg0 arg1=value1 arg2=value2 ...] [--opt -v -h ...]", - // 'Options:' => self::$internalOptions, - // 'Group Commands:' => $controllerArr ?: '... No register any group command(controller)', - // 'Independent Commands:' => $commandArr ?: '... No register any independent command', - // 'Internal Commands:' => $internalCommands, - // ]); - + // built in options + $internalOptions = FormatUtil::alignmentOptions(self::$internalOptions); + $this->output->mList(['Usage:' => "{$script} {command} [arg0 arg1=value1 arg2=value2 ...] [--opt -v -h ...]", 'Options:' => $internalOptions, 'Internal Commands:' => $internalCommands, 'Available Commands:' => array_merge($controllerArr, $commandArr)], ['sepChar' => ' ']); unset($controllerArr, $commandArr, $internalCommands); - $this->output->write("More command information, please use: $script {command} -h"); - + $this->output->write("More command information, please use: {$script} {command} -h"); $quit && $this->stop(); } @@ -464,23 +387,53 @@ public function showCommandList($quit = true) */ public function getCommandMessage($name, $default = null) { - return $this->commandMessages[$name] ?? $default; + return isset($this->commandMessages[$name]) ? $this->commandMessages[$name] : $default; } /** * @param string $name The command name * @param string $message - * @return string + * @return $this */ public function addCommandMessage($name, $message) { - return $this->commandMessages[$name] = $message; + if ($name && $message) { + $this->commandMessages[$name] = $message; + } + + return $this; + } + + /** + * @param string $name + * @param string|array $aliases + * @return $this + */ + public function addCommandAliases($name, $aliases) + { + if (!$name || !$aliases) { + return $this; + } + foreach ((array)$aliases as $alias) { + if ($alias = trim($alias)) { + $this->commandAliases[$alias] = $name; + } + } + + return $this; } + /** + * @param string $name + * @return string + */ + protected function getRealCommandName($name) + { + return isset($this->commandAliases[$name]) ? $this->commandAliases[$name] : $name; + } /********************************************************** * getter/setter methods **********************************************************/ - /** * @return array */ @@ -499,6 +452,7 @@ public function getCommandNames() /** * @param array $controllers + * @throws \InvalidArgumentException */ public function setControllers(array $controllers) { @@ -514,7 +468,7 @@ public function setControllers(array $controllers) /** * @return array */ - public function getControllers(): array + public function getControllers() { return $this->controllers; } @@ -530,6 +484,7 @@ public function isController($name) /** * @param array $commands + * @throws \InvalidArgumentException */ public function setCommands(array $commands) { @@ -545,7 +500,7 @@ public function setCommands(array $commands) /** * @return array */ - public function getCommands(): array + public function getCommands() { return $this->commands; } @@ -559,10 +514,54 @@ public function isCommand($name) return isset($this->commands[$name]); } + /** + * @return string|null + */ + public function getLogoText() + { + return isset($this->meta['logoText']) ? $this->meta['logoText'] : null; + } + + /** + * @param string $logoTxt + * @param string|null $style + */ + public function setLogo($logoTxt, $style = null) + { + $this->meta['logoText'] = $logoTxt; + if ($style) { + $this->meta['logoStyle'] = $style; + } + } + + /** + * @return string|null + */ + public function getLogoStyle() + { + return isset($this->meta['logoStyle']) ? $this->meta['logoStyle'] : 'info'; + } + + /** + * @param string $style + */ + public function setLogoStyle($style) + { + $this->meta['logoStyle'] = $style; + } + + /** + * @return string + */ + public function getRootPath() + { + return $this->getMeta('rootPath'); + } + /** * @return array */ - public static function getInternalCommands(): array + public static function getInternalCommands() { return static::$internalCommands; } @@ -571,7 +570,7 @@ public static function getInternalCommands(): array * @param $name * @return bool */ - public function isInternalCommand(string $name): bool + public function isInternalCommand($name) { return isset(static::$internalCommands[$name]); } @@ -607,7 +606,7 @@ public function getMeta($name = null, $default = null) return $this->meta; } - return $this->meta[$name] ?? $default; + return isset($this->meta[$name]) ? $this->meta[$name] : $default; } /** @@ -631,7 +630,7 @@ public function isProfile() /** * @return array */ - public function getCommandMessages(): array + public function getCommandMessages() { return $this->commandMessages; } @@ -643,4 +642,25 @@ public function setCommandMessages(array $commandMessages) { $this->commandMessages = $commandMessages; } -} + + /** + * @param null|string $name + * @return array + */ + public function getCommandAliases($name = null) + { + if (!$name) { + return $this->commandAliases; + } + + return array_keys($this->commandAliases, $name, true); + } + + /** + * @param array $commandAliases + */ + public function setCommandAliases(array $commandAliases) + { + $this->commandAliases = $commandAliases; + } +} \ No newline at end of file diff --git a/src/Base/AbstractCommand.php b/src/Base/AbstractCommand.php index 1cba8bbf..ff7347c9 100644 --- a/src/Base/AbstractCommand.php +++ b/src/Base/AbstractCommand.php @@ -1,4 +1,5 @@ {$name} - const ANNOTATION_VAR = '{%s}'; // '{$%s}'; - + use InputOutputAwareTrait, UserInteractAwareTrait; + const OK = 0; + // name -> {name} + const ANNOTATION_VAR = '{%s}'; + // '{$%s}'; /** * command name e.g 'test' 'test:one' * @var string */ protected static $name = ''; - /** * command/controller description message * please use the property setting current controller/command description * @var string */ protected static $description = ''; - /** * Allow display message tags in the command annotation * @var array @@ -52,15 +51,14 @@ abstract class AbstractCommand implements CommandInterface 'options' => true, 'example' => true, ]; - /** @var Application */ protected $app; - - /** @var InputDefinition|null */ + /** @var InputDefinition|null */ private $definition; - /** @var string */ private $processTitle; + /** @var array */ + private $annotationVars; /** * Command constructor. @@ -72,12 +70,11 @@ public function __construct(Input $input, Output $output, InputDefinition $defin { $this->input = $input; $this->output = $output; - if ($definition) { $this->definition = $definition; } - $this->init(); + $this->annotationVars = $this->annotationVars(); } protected function init() @@ -111,19 +108,20 @@ protected function createDefinition() */ public function annotationVars() { - // e.g: `more info see {name}/index` + // e.g: `more info see {name}:index` return [ + 'name' => self::getName(), + 'group' => self::getName(), 'script' => $this->input->getScript(), + // bin/app 'command' => $this->input->getCommand(), + // demo OR home:test 'fullCommand' => $this->input->getScript() . ' ' . $this->input->getCommand(), - 'name' => self::getName(), ]; } - /************************************************************************** * running a command **************************************************************************/ - /** * run command * @param string $command @@ -133,19 +131,16 @@ public function run($command = '') { // load input definition configure $this->configure(); - if ($this->input->sameOpt(['h', 'help'])) { return $this->showHelp(); } - + // some prepare check if (true !== $this->prepare()) { return -1; } - if (true !== $this->beforeExecute()) { return -1; } - $status = $this->execute($this->input, $this->output); $this->afterExecute(); @@ -167,7 +162,7 @@ protected function beforeExecute() * @param Output $output * @return int */ - abstract protected function execute($input, $output); + protected abstract function execute($input, $output); /** * after command execute @@ -182,10 +177,13 @@ protected function afterExecute() */ protected function showHelp() { - // 创建了 InputDefinition , 则使用它的信息。 - // 不会再解析和使用命令的注释。 + // 创建了 InputDefinition , 则使用它的信息。此时不会再解析和使用命令的注释。 if ($def = $this->getDefinition()) { - $this->output->mList($def->getSynopsis()); + $cmd = $this->input->getCommand(); + $spt = $this->input->getScript(); + $info = $def->getSynopsis(); + $info['usage'] = "{$spt} {$cmd} " . $info['usage']; + $this->output->mList($info); return true; } @@ -195,23 +193,22 @@ protected function showHelp() /** * prepare run + * @throws \RuntimeException */ protected function prepare() { if ($this->processTitle) { if (\function_exists('cli_set_process_title')) { if (false === @cli_set_process_title($this->processTitle)) { - if ('Darwin' === PHP_OS) { - $this->output->writeln('Running "cli_get_process_title" as an unprivileged user is not supported on MacOS.'); - } else { - $error = error_get_last(); - trigger_error($error['message'], E_USER_WARNING); + $error = error_get_last(); + if ($error && 'Darwin' !== PHP_OS) { + throw new \RuntimeException($error['message']); } } } elseif (\function_exists('setproctitle')) { setproctitle($this->processTitle); -// } elseif (isDebug) { -// $output->writeln('Install the proctitle PECL to be able to change the process title.'); + // } elseif (isDebug) { + // $output->writeln('Install the proctitle PECL to be able to change the process title.'); } } @@ -225,13 +222,11 @@ protected function prepare() */ public function validateInput() { - if (!$def = $this->definition) { + if (!($def = $this->definition)) { return true; } - $in = $this->input; $givenArgs = $errArgs = []; - foreach ($in->getArgs() as $key => $value) { if (\is_int($key)) { $givenArgs[$key] = $value; @@ -239,38 +234,31 @@ public function validateInput() $errArgs[] = $key; } } - if (\count($errArgs)> 0) { $this->output->liteError(sprintf('Unknown arguments (error: "%s").', implode(', ', $errArgs))); return false; } - $defArgs = $def->getArguments(); $missingArgs = array_filter(array_keys($defArgs), function ($name, $key) use ($def, $givenArgs) { return !array_key_exists($key, $givenArgs) && $def->argumentIsRequired($name); }, ARRAY_FILTER_USE_BOTH); - if (\count($missingArgs)> 0) { $this->output->liteError(sprintf('Not enough arguments (missing: "%s").', implode(', ', $missingArgs))); + return false; } - $index = 0; $args = []; - foreach ($defArgs as $name => $conf) { - $args[$name] = $givenArgs[$index] ?? $conf['default']; + $args[$name] = isset($givenArgs[$index]) ? $givenArgs[$index] : $conf['default']; $index++; } - $in->setArgs($args); - // check options $opts = $missingOpts = []; //$givenLOpts = $in->getLongOpts(); $defOpts = $def->getOptions(); - foreach ($defOpts as $name => $conf) { if (!$in->hasLOpt($name)) { if (($srt = $conf['shortcut']) && $in->hasSOpt($srt)) { @@ -280,102 +268,119 @@ public function validateInput() } } } - if (\count($missingOpts)> 0) { $this->output->liteError(sprintf('Not enough options parameters (missing: "%s").', implode(', ', $missingOpts))); return false; } - if ($opts) { $in->setLOpts($opts); } return true; } - /************************************************************************** * helper methods **************************************************************************/ + /** + * @param string $name + * @param string $value + */ + protected function addAnnotationVar($name, $value) + { + if (!isset($this->annotationVars[$name])) { + $this->annotationVars[$name] = (string)$value; + } + } /** - * 为命令注解提供可解析解析变量. 可以在命令的注释中使用 + * @param array $map + */ + protected function addAnnotationVars(array $map) + { + foreach ($map as $name => $value) { + $this->addAnnotationVar($name, $value); + } + } + + /** + * @param string $name + * @param string $value + */ + protected function setAnnotationVar($name, $value) + { + $this->annotationVars[$name] = (string)$value; + } + + /** + * 替换注解中的变量为对应的值 * @param string $str * @return string */ - protected function handleAnnotationVars($str) + protected function parseAnnotationVars($str) { - $map = []; - - foreach ($this->annotationVars() as $key => $value) { - $key = sprintf(self::ANNOTATION_VAR, $key); - $map[$key] = $value; + static $map; + if ($map === null) { + foreach ($this->annotationVars as $key => $value) { + $key = sprintf(self::ANNOTATION_VAR, $key); + $map[$key] = $value; + } + } + // not use vars + if (false === strpos($str, '{')) { + return $str; } return $map ? strtr($str, $map) : $str; } /** - * show help by parse method annotation + * show help by parse method annotations * @param string $method * @param null|string $action + * @param array $aliases * @return int - * @throws \ReflectionException */ - protected function showHelpByMethodAnnotation($method, $action = null) + protected function showHelpByMethodAnnotations($method, $action = null, array $aliases = []) { $ref = new \ReflectionClass($this); $name = $this->input->getCommand(); - if (!$ref->hasMethod($method)) { - $this->write("The command [$name] don't exist in the group: " . static::getName()); + $this->write("The command [{$name}] don't exist in the group: " . static::getName()); return 0; } - // is a console controller command if ($action && !$ref->getMethod($method)->isPublic()) { - $this->write("The command [$name] don't allow access in the class."); + $this->write("The command [{$name}] don't allow access in the class."); + return 0; } - $doc = $ref->getMethod($method)->getDocComment(); - $tags = Annotation::tagList($this->handleAnnotationVars($doc)); - - foreach ($tags as $tag => $msg) { - if (!$msg || !\is_string($msg)) { + $tags = Annotation::getTags($this->parseAnnotationVars($doc)); + $help = []; + if ($aliases) { + $help[] = sprintf("Alias Name: %s\n", implode(',', $aliases)); + } + foreach (array_keys(self::$annotationTags) as $tag) { + if (empty($tags[$tag]) || !\is_string($tags[$tag])) { continue; } - - if (isset(self::$annotationTags[$tag])) { - $msg = trim($msg); - - // need multi align - // if (self::$annotationTags[$tag]) { - // $lines = array_map(function ($line) { - // // return trim($line); - // return $line; - // }, explode("\n", $msg)); - - // $msg = implode("\n", array_filter($lines, 'trim')); - // } - - $tag = ucfirst($tag); - $this->write("$tag:\n $msg\n"); - } + $msg = trim($tags[$tag]); + $tag = ucfirst($tag); + $help[] = "{$tag}:\n {$msg}\n"; } + $this->output->write(implode("\n", $help), false); return 0; } - /************************************************************************** * getter/setter methods **************************************************************************/ - /** * @param string $name */ - public static function setName(string $name) + public static function setName($name) { static::$name = $name; } @@ -383,7 +388,7 @@ public static function setName(string $name) /** * @return string */ - final public static function getName(): string + public static final function getName() { return static::$name; } @@ -391,7 +396,7 @@ final public static function getName(): string /** * @return string */ - final public static function getDescription(): ?string + public static final function getDescription() { return static::$description; } @@ -399,7 +404,7 @@ final public static function getDescription(): ?string /** * @param string $description */ - public static function setDescription(string $description) + public static function setDescription($description) { static::$description = $description; } @@ -407,11 +412,21 @@ public static function setDescription(string $description) /** * @return array */ - public static function getAnnotationTags(): array + public static function getAnnotationTags() { return self::$annotationTags; } + /** + * @param string $name + */ + public static function addAnnotationTag($name) + { + if (!isset(self::$annotationTags[$name])) { + self::$annotationTags[$name] = true; + } + } + /** * @param array $annotationTags * @param bool $replace @@ -437,10 +452,18 @@ public function setDefinition(InputDefinition $definition) $this->definition = $definition; } + /** + * @return array + */ + public function getAnnotationVars() + { + return $this->annotationVars; + } + /** * @return ApplicationInterface */ - public function getApp(): ApplicationInterface + public function getApp() { return $this->app; } @@ -456,7 +479,7 @@ public function setApp(ApplicationInterface $app) /** * @return string */ - public function getProcessTitle(): string + public function getProcessTitle() { return $this->processTitle; } @@ -464,8 +487,8 @@ public function getProcessTitle(): string /** * @param string $processTitle */ - public function setProcessTitle(string $processTitle) + public function setProcessTitle($processTitle) { $this->processTitle = $processTitle; } -} +} \ No newline at end of file diff --git a/src/Base/ApplicationInterface.php b/src/Base/ApplicationInterface.php index 6590d088..ed6be6ab 100644 --- a/src/Base/ApplicationInterface.php +++ b/src/Base/ApplicationInterface.php @@ -1,4 +1,5 @@ * * src-dirs The source directory for pack, multi use ',' split.* - * * @options * -c, --compress Compress the phar file to 'gz','bz','zip'. * --format Format php source file content(will remove all annotations). * --file-include Append file include - * * @example * {command} phar=my.phar src-dir=./ -c --format */ public function packCommand() { $pcr = new PharCompiler(BASE_PATH); - $pcr->setOptions([ - 'cliIndex' => 'examples/app', - 'webIndex' => null, - - 'compress' => $this->getSameOpt(['c', 'compress'], false), - - 'dirExclude' => '#[\.git|tests]#', - - 'fileInclude' => ['LICENSE', 'app', 'liteApp'], - 'fileMatch' => '#\.php$#', - ]); - + $pcr->setOptions(['cliIndex' => 'examples/app', 'webIndex' => null, 'compress' => $this->getSameOpt(['c', 'compress'], false), 'dirExclude' => '#[\\.git|tests]#', 'fileInclude' => ['LICENSE', 'app', 'liteApp'], 'fileMatch' => '#\\.php$#']); $pharFile = BASE_PATH . '/test.phar'; $count = $pcr->pack($pharFile); - - $this->output->json([ - 'count' => $count, - 'size' => round(filesize($pharFile) / 1000, 2) . ' kb', - ]); + $this->output->json(['count' => $count, 'size' => round(filesize($pharFile) / 1000, 2) . ' kb']); } /** @@ -68,23 +51,16 @@ public function packCommand() public function buildCommand() { $packer = new PharBuilder(BASE_PATH); - $packer->addDirectory(BASE_PATH); $packer->setOptions([ 'cliIndex' => 'examples/app', 'webIndex' => null, - // 'compress' => $this->getSameOpt(['c', 'compress'], false), - - 'dirExclude' => '#[\.git|tests]#', - + 'dirExclude' => '#[\\.git|tests]#', 'fileInclude' => ['LICENSE', 'app', 'liteApp'], - 'fileMatch' => '#\.php$#', + 'fileMatch' => '#\\.php$#', ]); $packer->build($pharFile = BASE_PATH . '/example.phar'); - - $this->output->json([ - 'size' => round(filesize($pharFile) / 1000, 2) . ' kb', - ]); + $this->output->json(['size' => round(filesize($pharFile) / 1000, 2) . ' kb']); } -} +} \ No newline at end of file diff --git a/src/BuiltIn/Resources/art-fonts/404.txt b/src/BuiltIn/Resources/art-fonts/404.txt deleted file mode 100644 index ce0a592b..00000000 --- a/src/BuiltIn/Resources/art-fonts/404.txt +++ /dev/null @@ -1,6 +0,0 @@ - _ _ ___ _ _ - | || | / _ \| || | - | || |_| | | | || |_ - |__ _| | | |__ _| - | | | |_| | | | - |_| \___/ |_| \ No newline at end of file diff --git a/src/BuiltIn/Resources/exe/README.md b/src/BuiltIn/Resources/exe/README.md deleted file mode 100644 index 41fda62b..00000000 --- a/src/BuiltIn/Resources/exe/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# exe file - -## Hidden Input - -From https://github.com/Seldaek/hidden-input \ No newline at end of file diff --git a/src/BuiltIn/Resources/exe/hiddeninput.exe b/src/BuiltIn/Resources/exe/hiddeninput.exe deleted file mode 100644 index c8cf65e8..00000000 Binary files a/src/BuiltIn/Resources/exe/hiddeninput.exe and /dev/null differ diff --git a/src/Command.php b/src/Command.php index 53b0140c..dfba7153 100644 --- a/src/Command.php +++ b/src/Command.php @@ -1,4 +1,5 @@ addArgument('test') // ->addOption('test'); // } - /** * @return int */ @@ -47,6 +54,6 @@ protected function showHelp() return 0; } - return $this->showHelpByMethodAnnotation('execute'); + return $this->showHelpByMethodAnnotations('execute'); } -} +} \ No newline at end of file diff --git a/src/Components/ArtFont.php b/src/Components/ArtFont.php new file mode 100644 index 00000000..a546a84a --- /dev/null +++ b/src/Components/ArtFont.php @@ -0,0 +1,269 @@ + + */ + private $groups = []; + /** + * @var array + * [ + * group => [ name => path ] + * ] + */ + private $fonts = []; + /** + * @var array + * [ + * name => content + * ] + */ + private $fontContents = []; + + /** + * @return self + */ + public static function create() + { + if (!self::$instance) { + self::$instance = new self(); + } + + return self::$instance; + } + + /** + * ArtFont constructor. + */ + public function __construct() + { + $this->loadInternalFonts(); + } + + /** + * load Internal Fonts + */ + protected function loadInternalFonts() + { + $path = \dirname(__DIR__) . '/BuiltIn/Resources/art-fonts/'; + $group = self::INTERNAL_GROUP; + foreach (self::$internalFonts as $font) { + $this->fonts[$group][$font] = $path . $font . '%s.txt'; + } + $this->groups[$group] = $path; + } + + /** + * display the internal art font + * @param string $name + * @param array $opts + * @return bool + */ + public function showInternal($name, array $opts = []) + { + return $this->show($name, self::INTERNAL_GROUP, $opts); + } + + /** + * @param string $name + * @param string $group + * @return string + */ + public function showItalic($name, $group = null, array $opts = []) + { + $opts['type'] = 'italic'; + + return $this->show($name . '_italic', $group, $opts); + } + + /** + * display the art font + * @param string $name + * @param string $group + * @param array $opts + * contains: + * - type => '', // 'italic' + * - indent => 2, + * - style => '', // 'info' 'error' + * @return bool + */ + public function show($name, $group = null, array $opts = []) + { + $opts = array_merge(['type' => '', 'indent' => 2, 'style' => ''], $opts); + $type = $opts['type']; + $pfxType = $type ? '_' . $type : ''; + $txt = ''; + $group = trim($group); + $group = $group ?: self::DEFAULT_GROUP; + $longKey = $group . '.' . $name . $pfxType; + if (isset($this->fontContents[$longKey])) { + $txt = $this->fontContents[$longKey]; + } elseif (isset($this->fonts[$group][$name])) { + $font = sprintf($this->fonts[$group][$name], $pfxType); + if (is_file($font)) { + $txt = file_get_contents($font); + } + } elseif (isset($this->groups[$group])) { + $font = $this->groups[$group] . $name . $pfxType . '.txt'; + if (is_file($font)) { + $txt = file_get_contents($font); + } + } + // var_dump($txt, $this); + if ($txt) { + return Show::write(Helper::wrapTag($txt, $opts['style'])); + } + + return false; + } + + /** + * @param string $name + * @param string $group + * @return string + */ + public function font($name, $group = null) + { + return ''; + } + + /** + * @param string $group + * @param string $path + * @return $this + */ + public function addGroup($group, $path) + { + $group = trim($group, '_'); + if (!$group || !is_dir($path)) { + return $this; + } + if (!isset($this->groups[$group])) { + $this->groups[$group] = $path; + } + + return $this; + } + + /** + * @param string $group + * @param string $path + * @return $this + */ + public function setGroup($group, $path) + { + $group = trim($group, '_'); + if (!$group || !is_dir($path)) { + return $this; + } + $this->groups[$group] = $path; + + return $this; + } + + /** + * @param string $name + * @param string $file font file path + * @param string|null $group + * @return $this + */ + public function addFont($name, $file, $group = null) + { + $group = $group ?: self::DEFAULT_GROUP; + if (is_file($file)) { + $info = pathinfo($file); + $ext = !empty($info['extension']) ? $info['extension'] : 'txt'; + $this->fonts[$group][$name] = $info['dirname'] . '/' . $info['filename'] . '.' . $ext; + } + + return $this; + } + + /** + * @param string $name + * @param string $content + * @return $this + */ + public function addFontContent($name, $content) + { + if ($name && ($content = trim($content))) { + $this->fontContents[$name] = $content; + } + + return $this; + } + + /** + * @param string $name + * @return bool + */ + public static function isInternalFont($name) + { + return \in_array((string)$name, self::$internalFonts, true); + } + + /** + * @return array + */ + public static function getInternalFonts() + { + return self::$internalFonts; + } + + /** + * @return array + */ + public function getGroups() + { + return $this->groups; + } + + /** + * @param array $groups + */ + public function setGroups(array $groups) + { + $this->groups = array_merge($this->groups, $groups); + } + + /** + * @return array + */ + public function getFonts() + { + return $this->fonts; + } + + /** + * @param array $fonts + */ + public function setFonts(array $fonts) + { + foreach ($fonts as $name => $font) { + $this->addFont($name, $font); + } + } +} \ No newline at end of file diff --git a/src/Components/AryBuffer.php b/src/Components/AryBuffer.php new file mode 100644 index 00000000..a6b85aa2 --- /dev/null +++ b/src/Components/AryBuffer.php @@ -0,0 +1,116 @@ +body[] = $content; + } + } + + /** + * @param string $content + */ + public function write($content) + { + $this->body[] = $content; + } + + /** + * @param string $content + */ + public function append($content) + { + $this->write($content); + } + + /** + * @param string $content + */ + public function prepend($content) + { + array_unshift($this->body, $content); + } + + /** + * clear + */ + public function clear() + { + $this->body = []; + } + + /** + * @return string[] + */ + public function getBody() + { + return $this->body; + } + + /** + * @param string[] $body + */ + public function setBody(array $body) + { + $this->body = $body; + } + + /** + * @return string + */ + public function toString() + { + return implode($this->delimiter, $this->body); + } + + /** + * @return string + */ + public function __toString() + { + return $this->toString(); + } + + /** + * @return string + */ + public function getDelimiter() + { + return $this->delimiter; + } + + /** + * @param string $delimiter + */ + public function setDelimiter($delimiter) + { + $this->delimiter = $delimiter; + } +} \ No newline at end of file diff --git a/src/Components/AutoCompletion.php b/src/Components/AutoComplete/AutoCompletion.php similarity index 88% rename from src/Components/AutoCompletion.php rename to src/Components/AutoComplete/AutoCompletion.php index 4f64c0b2..363de333 100644 --- a/src/Components/AutoCompletion.php +++ b/src/Components/AutoComplete/AutoCompletion.php @@ -1,4 +1,5 @@ data = $data; - if ($enable) { $this->register(); } @@ -52,23 +50,19 @@ public function register() * The readline_completion_function callback handler. * @param string $input the user input * @param $index - * * @return array */ public function completionHandler($input, $index) { - $info = readline_info(); - $line = substr($info['line_buffer'], 0, $info['end']); - $tokens = token_get_all('data; } - $matches = []; $matcher = $this->matcher; - foreach ($this->data as $item) { if ($matcher && $matcher($input, $item)) { $matches[] = $item; @@ -76,12 +70,11 @@ public function completionHandler($input, $index) $matches[] = $item; } } - if (!$matches) { return []; } - $matches = array_unique($matches); + return $matches ?: ['']; } @@ -114,7 +107,7 @@ public function setMatcher(callable $matcher) /** * @return array */ - public function getData(): array + public function getData() { return $this->data; } @@ -136,4 +129,4 @@ public function reset() return $this; } -} +} \ No newline at end of file diff --git a/src/Components/AutoComplete/ScriptGenerator.php b/src/Components/AutoComplete/ScriptGenerator.php new file mode 100644 index 00000000..c8ed78ff --- /dev/null +++ b/src/Components/AutoComplete/ScriptGenerator.php @@ -0,0 +1,126 @@ +getRenderer(); + + return $tt->render(file_get_contents($tplFile), $vars, $dstFile); + } + + /** + * @return int + */ + public function getType() + { + return $this->type; + } + + /** + * @param int $type + */ + public function setType($type) + { + if (\in_array($type, self::typeList(), 1)) { + $this->type = $type; + } + } + + /** + * @return int + */ + public function getMode() + { + return $this->mode; + } + + /** + * @param int $mode + */ + public function setMode($mode) + { + if (\in_array($mode, self::modeList(), 1)) { + $this->mode = $mode; + } + } + + /** + * @return TextTemplate + */ + public function getRenderer() + { + if (!$this->renderer) { + $this->renderer = new TextTemplate(); + } + + return $this->renderer; + } + + /** + * @param TextTemplate $renderer + */ + public function setRenderer(TextTemplate $renderer) + { + $this->renderer = $renderer; + } +} \ No newline at end of file diff --git a/src/Utils/Download.php b/src/Components/Download.php similarity index 64% rename from src/Utils/Download.php rename to src/Components/Download.php index edd2e989..e23444f6 100644 --- a/src/Utils/Download.php +++ b/src/Components/Download.php @@ -1,4 +1,5 @@ * @param string $url @@ -45,7 +43,7 @@ class Download * @param string $type * @return Download */ - public static function down(string $url, string $saveAs, string $type = self::PROGRESS_TEXT) + public static function down($url, $saveAs, $type = self::PROGRESS_TEXT) { $d = new self($url, $saveAs, $type); @@ -58,53 +56,38 @@ public static function down(string $url, string $saveAs, string $type = self::PR * @param string $saveAs * @param string $type */ - public function __construct(string $url, string $saveAs, $type = self::PROGRESS_TEXT) + public function __construct($url, $saveAs, $type = self::PROGRESS_TEXT) { $this->url = $url; $this->saveAs = $saveAs; $this->showType = $type === self::PROGRESS_BAR ? self::PROGRESS_BAR : self::PROGRESS_TEXT; } + /** + * start download + * @return $this + */ public function start() { if (!$this->url || !$this->saveAs) { - Show::error("Please the property 'url' and 'saveAs'.", 1); + Show::liteError("Please the property 'url' and 'saveAs'.", 1); } - $ctx = stream_context_create(); - // register stream notification callback - stream_context_set_params($ctx, [ - 'notification' => [$this, 'progressShow'] - ]); - + stream_context_set_params($ctx, ['notification' => [$this, 'progressShow']]); Show::write("Download: {$this->url}\nSave As: {$this->saveAs}\n"); - $fp = fopen($this->url, 'rb', false, $ctx); - if (\is_resource($fp) && file_put_contents($this->saveAs, $fp)) { Show::write("\nDone!"); } else { $err = error_get_last(); - Show::error("\nErr.rrr..orr...\n {$err['message']}\n", 1); + Show::liteError("\nErr.rrr..orr...\n {$err['message']}\n", 1); } - $this->fileSize = null; return $this; } - /* - progressBar() OUT: - Connected... - Mime-type: text/html; charset=utf-8 - Being redirected to: http://no2.php.net/distributions/php-5.2.5.tar.bz2 - Connected... - FileSize: 7773024 - Mime-type: application/octet-stream - [========================================> ] 40% (3076/7590 kb) - */ - /** * @param int $notifyCode stream notify code * @param int $severity severity code @@ -113,46 +96,38 @@ public function start() * @param int $transferredBytes Have been transferred bytes * @param int $maxBytes Target max length bytes */ - protected function progressShow($notifyCode, $severity, $message, $messageCode, $transferredBytes, $maxBytes) + public function progressShow($notifyCode, $severity, $message, $messageCode, $transferredBytes, $maxBytes) { $msg = ''; - switch ($notifyCode) { case STREAM_NOTIFY_RESOLVE: case STREAM_NOTIFY_AUTH_REQUIRED: case STREAM_NOTIFY_COMPLETED: case STREAM_NOTIFY_FAILURE: case STREAM_NOTIFY_AUTH_RESULT: - $msg = "NOTIFY: $message(NO: $messageCode, Severity: $severity)"; + $msg = "NOTIFY: {$message}(NO: {$messageCode}, Severity: {$severity})"; /* Ignore */ break; - case STREAM_NOTIFY_REDIRECTED: - $msg = "Being redirected to: $message"; + $msg = "Being redirected to: {$message}"; break; - case STREAM_NOTIFY_CONNECT: $msg = 'Connected ...'; break; - case STREAM_NOTIFY_FILE_SIZE_IS: $this->fileSize = $maxBytes; $fileSize = sprintf('%2d', $maxBytes / 1024); - $msg = "Got the file size: $fileSize kb"; + $msg = "Got the file size: {$fileSize} kb"; break; - case STREAM_NOTIFY_MIME_TYPE_IS: - $msg = "Found the mime-type: $message"; + $msg = "Found the mime-type: {$message}"; break; - case STREAM_NOTIFY_PROGRESS: if ($transferredBytes> 0) { $this->showProgressByType($transferredBytes); } - break; } - $msg && Show::write($msg); } @@ -160,21 +135,19 @@ protected function progressShow($notifyCode, $severity, $message, $messageCode, * @param $transferredBytes * @return string */ - protected function showProgressByType($transferredBytes) + public function showProgressByType($transferredBytes) { if ($transferredBytes <= 0) { return ''; } - $tfKb = $transferredBytes / 1024; - if ($this->showType === self::PROGRESS_BAR) { $size = $this->fileSize; - if ($size === null) { printf("\rUnknown file size... %2d kb done..", $tfKb); } else { - $length = ceil(($transferredBytes / $size) * 100); // しかく = + $length = ceil($transferredBytes / $size * 100); + // しかく = printf("\r[%-100s] %d%% (%2d/%2d kb)", str_repeat('=', $length) . '>', $length, $tfKb, $size / 1024); } } else { @@ -188,7 +161,7 @@ protected function showProgressByType($transferredBytes) /** * @return int */ - public function getShowType(): int + public function getShowType() { return $this->showType; } @@ -196,7 +169,7 @@ public function getShowType(): int /** * @param int $showType */ - public function setShowType(int $showType) + public function setShowType($showType) { $this->showType = $showType; } @@ -204,7 +177,7 @@ public function setShowType(int $showType) /** * @return string */ - public function getUrl(): string + public function getUrl() { return $this->url; } @@ -212,7 +185,7 @@ public function getUrl(): string /** * @param string $url */ - public function setUrl(string $url) + public function setUrl($url) { $this->url = $url; } @@ -220,7 +193,7 @@ public function setUrl(string $url) /** * @return string */ - public function getSaveAs(): string + public function getSaveAs() { return $this->saveAs; } @@ -228,29 +201,8 @@ public function getSaveAs(): string /** * @param string $saveAs */ - public function setSaveAs(string $saveAs) + public function setSaveAs($saveAs) { $this->saveAs = $saveAs; } - - /* - progressText() OUT: - Connected... - Found the mime-type: text/html; charset=utf-8 - Being redirected to: http://no.php.net/contact - Connected... - Got the fileSize: 0 - Found the mime-type: text/html; charset=utf-8 - Being redirected to: http://no.php.net/contact.php - Connected... - Got the fileSize: 4589 - Found the mime-type: text/html;charset=utf-8 - Made some progress, downloaded 0 so far - Made some progress, downloaded 0 so far - Made some progress, downloaded 0 so far - Made some progress, downloaded 1440 so far - ... ... - */ - - -} +} \ No newline at end of file diff --git a/src/Components/ExecComparator.php b/src/Components/ExecComparator.php new file mode 100644 index 00000000..4ff1dcc7 --- /dev/null +++ b/src/Components/ExecComparator.php @@ -0,0 +1,42 @@ +vars; + } + + /** + * @param array $vars + */ + public function setVars(array $vars) + { + $this->vars = $vars; + } +} \ No newline at end of file diff --git a/src/Components/Formatter/Formatter.php b/src/Components/Formatter/Formatter.php new file mode 100644 index 00000000..5cd197a1 --- /dev/null +++ b/src/Components/Formatter/Formatter.php @@ -0,0 +1,18 @@ +speed; + } + + /** + * @param int $speed + */ + public function setSpeed($speed) + { + $this->speed = (int)$speed; + } +} \ No newline at end of file diff --git a/src/Components/PharBuilder.php b/src/Components/PharBuilder.php index eada9a01..0d611920 100644 --- a/src/Components/PharBuilder.php +++ b/src/Components/PharBuilder.php @@ -1,11 +1,11 @@ \Phar::SHA512, - 'SHA-256' => \Phar::SHA256, - 'SHA-1' => \Phar::SHA1 - ]; - - /** - * @var array - */ - private $options = [ - // for create phar Stub. It is relative the srcDir path. - 'cliIndex' => null, - 'webIndex' => null, - - // compress php code - // 'compress' => false, - - 'dirExclude' => '#[\.git|tests]#', - - 'fileInclude' => [], - 'fileExclude' => [], - 'fileMatch' => '#\.php#', - ]; - - public function __construct($basedir) - { - $this->basedir = $basedir; +/** @var int @see \Phar::GZ, \Phar::BZ2 */ +private $compressMode; +/** @var resource */ +private $key; +/** @var string */ +private $basedir; +/** @var string */ +private $aliasName; +/** @var callable */ +private $iteratorFilter; +private $signatureType; +private $directories = []; +private $supportedSignatureTypes = ['SHA-512' => \Phar::SHA512, 'SHA-256' => \Phar::SHA256, 'SHA-1' => \Phar::SHA1]; +/** + * @var array + */ +private $options = [ + // for create phar Stub. It is relative the srcDir path. + 'cliIndex' => null, + 'webIndex' => null, + // compress php code + // 'compress' => false, + 'dirExclude' => '#[\\.git|tests]#', + 'fileInclude' => [], + 'fileExclude' => [], + 'fileMatch' => '#\\.php#', +]; +public function __construct($basedir) +{ + $this->basedir = $basedir; +} +/** + * @param int $mode + */ +public function setCompressMode($mode) +{ + $this->compressMode = (int)$mode; +} +/** + * @param $type + * @throws \InvalidArgumentException + */ +public function setSignatureType($type) +{ + if (!array_key_exists($type, $this->supportedSignatureTypes)) { + throw new \InvalidArgumentException(sprintf('Signature type "%s" not known or not supported by this PHP installation.', $type)); } - - /** - * @param int $mode - */ - public function setCompressMode($mode) - { - $this->compressMode = (int)$mode; + $this->signatureType = $type; +} +/** + * @param $key + */ +public function setSignatureKey($key) +{ + $this->key = $key; +} +/** + * @param string $directory + */ +public function addDirectory($directory) +{ + $this->directories[] = $directory; +} +/** + * @param $name + */ +public function setAliasName($name) +{ + $this->aliasName = $name; +} +/** + * @param string $filename + * @param string $stub + * @throws \RuntimeException + * @throws \InvalidArgumentException + * @throws \UnexpectedValueException + * @throws \BadMethodCallException + */ +public function build($filename, $stub = null) +{ + if (file_exists($filename)) { + unlink($filename); } - - /** - * @param $type - * @throws \InvalidArgumentException - */ - public function setSignatureType($type) - { - if (!array_key_exists($type, $this->supportedSignatureTypes)) { - throw new \InvalidArgumentException( - sprintf('Signature type "%s" not known or not supported by this PHP installation.', $type) - ); - } - - $this->signatureType = $type; + if (ini_get('phar.readonly')) { + throw new \RuntimeException("The 'phar.readonly' is 'On', build phar must setting it 'Off'"); } - - /** - * @param $key - */ - public function setSignatureKey($key) - { - $this->key = $key; + if (!$this->directories) { + throw new \RuntimeException("Please setting the 'directories' want building directories"); } - - /** - * @param string $directory - */ - public function addDirectory($directory) - { - $this->directories[] = $directory; + $aliasName = $this->aliasName ?: basename($filename); + $phar = new \Phar($filename, 0, $aliasName); + $phar->startBuffering(); + $phar->setStub($stub ?: $this->createStub($aliasName)); + if ($this->key !== null) { + $privateKey = ''; + openssl_pkey_export($this->key, $privateKey); + $phar->setSignatureAlgorithm(\Phar::OPENSSL, $privateKey); + $keyDetails = openssl_pkey_get_details($this->key); + file_put_contents($filename . '.pubkey', $keyDetails['key']); + } else { + $phar->setSignatureAlgorithm($this->selectSignatureType()); } - - /** - * @param $name - */ - public function setAliasName($name) - { - $this->aliasName = $name; + $filter = $this->getIteratorFilter(); + $basedir = $this->basedir ?: $this->directories[0]; + foreach ($this->directories as $directory) { + $iterator = Helper::recursiveDirectoryIterator($directory, $filter); + $phar->buildFromIterator($iterator, $basedir); } - - /** - * @param string $filename - * @param string $stub - * @throws \RuntimeException - * @throws \InvalidArgumentException - * @throws \UnexpectedValueException - * @throws \BadMethodCallException - */ - public function build($filename, $stub = null) - { - if (file_exists($filename)) { - unlink($filename); - } - - if (ini_get('phar.readonly')) { - throw new \RuntimeException("The 'phar.readonly' is 'On', build phar must setting it 'Off'"); - } - - if (!$this->directories) { - throw new \RuntimeException("Please setting the 'directories' want building directories"); - } - - $aliasName = $this->aliasName ?: basename($filename); - $phar = new \Phar($filename, 0, $aliasName); - $phar->startBuffering(); - $phar->setStub($stub ?: $this->createStub($aliasName)); - - if ($this->key !== null) { - $privateKey = ''; - openssl_pkey_export($this->key, $privateKey); - $phar->setSignatureAlgorithm(\Phar::OPENSSL, $privateKey); - $keyDetails = openssl_pkey_get_details($this->key); - file_put_contents($filename . '.pubkey', $keyDetails['key']); - } else { - $phar->setSignatureAlgorithm($this->selectSignatureType()); - } - - $filter = $this->getIteratorFilter(); - $basedir = $this->basedir ?: $this->directories[0]; - - foreach ($this->directories as $directory) { - $iterator = Helper::recursiveDirectoryIterator($directory, $filter); - $phar->buildFromIterator($iterator, $basedir); - } - - if ($this->compressMode !== null) { - $phar->compressFiles($this->compressMode); - } - - $phar->stopBuffering(); + if ($this->compressMode !== null) { + $phar->compressFiles($this->compressMode); } - - private function createStub($pharName) - { - // Stubs + $phar->stopBuffering(); +} +private function createStub($pharName) +{ +// Stubs // $phar->setStub($this->getStub()); - return \Phar::createDefaultStub($this->options['cliIndex'], $this->options['webIndex']); - - // 设置入口 +return \Phar::createDefaultStub($this->options['cliIndex'], $this->options['webIndex']); +// 设置入口 // return "";*/ - } - - /** - * @return int|mixed - */ - private function selectSignatureType() - { - if ($this->signatureType) { - return $this->supportedSignatureTypes[$this->signatureType]; - } - - $supported = \Phar::getSupportedSignatures(); - - foreach ($this->supportedSignatureTypes as $candidate => $type) { - if (\in_array($candidate, $supported, true)) { - return $type; - } - } - - // Is there any PHP Version out there that does not support at least SHA-1? - // But hey, fallback to md5, better than nothing - return \Phar::MD5; - } - - /** - * @return callable - */ - public function getIteratorFilter(): callable - { - return $this->iteratorFilter ?: function (\SplFileInfo $file) { - $name = $file->getFilename(); - - // Skip hidden files and directories. - if ($name[0] === '.') { - return false; - } - - if ($file->isDir()) { - // Only recurse into intended subdirectories. - return preg_match($this->options['dirExclude'], $name); - } - - if (\in_array($name, $this->options['fileInclude'], true)) { - return true; - } - - // Only consume files of interest. - return preg_match($this->options['fileMatch'], $name); - }; - } - - /** - * @param callable $iteratorFilter - * @return $this - */ - public function setIteratorFilter(callable $iteratorFilter) - { - $this->iteratorFilter = $iteratorFilter; - - return $this; - } - - /** - * @param resource $key - * @return $this - */ - public function setKey($key) - { - $this->key = $key; - - return $this; - } - - /** - * @return array - */ - public function getOptions(): array - { - return $this->options; - } - - /** - * @param array $options - */ - public function setOptions(array $options) - { - $this->options = $options; - } } +/** +* @return int|mixed +*/ +private function selectSignatureType() +{ +if ($this->signatureType) { +return $this->supportedSignatureTypes[$this->signatureType]; +} +$supported = \Phar::getSupportedSignatures(); +foreach ($this->supportedSignatureTypes as $candidate => $type) { +if (\in_array($candidate, $supported, true)) { +return $type; +} +} +// Is there any PHP Version out there that does not support at least SHA-1? +// But hey, fallback to md5, better than nothing +return \Phar::MD5; +} +/** +* @return callable +*/ +public function getIteratorFilter() +{ +return $this->iteratorFilter ?: function (\SplFileInfo $file) { +$name = $file->getFilename(); +// Skip hidden files and directories. +if ($name[0] === '.') { +return false; +} +if ($file->isDir()) { +// Only recurse into intended subdirectories. +return preg_match($this->options['dirExclude'], $name); +} +if (\in_array($name, $this->options['fileInclude'], true)) { +return true; +} +// Only consume files of interest. +return preg_match($this->options['fileMatch'], $name); +}; +} +/** +* @param callable $iteratorFilter +* @return $this +*/ +public function setIteratorFilter(callable $iteratorFilter) +{ +$this->iteratorFilter = $iteratorFilter; +return $this; +} +/** +* @param resource $key +* @return $this +*/ +public function setKey($key) +{ +$this->key = $key; +return $this; +} +/** +* @return array +*/ +public function getOptions() +{ +return $this->options; +} +/** +* @param array $options +*/ +public function setOptions(array $options) +{ +$this->options = $options; +} +} \ No newline at end of file diff --git a/src/Components/PharCompiler.php b/src/Components/PharCompiler.php index 82fb2601..4e366eef 100644 --- a/src/Components/PharCompiler.php +++ b/src/Components/PharCompiler.php @@ -1,4 +1,5 @@ null, 'webIndex' => null, - // compress php code 'compress' => false, - - 'dirExclude' => '#[\.git|tests]#', - + 'dirExclude' => '#[\\.git|tests]#', 'fileInclude' => [], - 'fileMatch' => '#\.php#', + 'fileMatch' => '#\\.php#', ]; - /** @var array */ private $srcDirs; - /** @var array */ private $appendFiles; - /** @var string */ private $dstDir; @@ -55,7 +49,6 @@ public function __construct($srcDirs = null, $dstDir = null, array $options = [] { $this->srcDirs = $srcDirs ? (array)$srcDirs : []; $this->dstDir = $dstDir; - $this->setOptions($options); } @@ -73,38 +66,26 @@ public function pack($pharFile = 'your.phar', $version = '0.0.1') if (file_exists($pharFile)) { unlink($pharFile); } - $pharName = basename($pharFile); $this->version = $version; - if (!$this->srcDirs) { throw new \LogicException('Please setting the source directory for pack'); } - $phar = new Phar($pharFile, 0, $pharName); -// $phar = new Phar($pharFile, \FilesystemIterator::CURRENT_AS_FILEINFO | \FilesystemIterator::KEY_AS_FILENAME, $pharName); + // $phar = new Phar($pharFile, \FilesystemIterator::CURRENT_AS_FILEINFO | \FilesystemIterator::KEY_AS_FILENAME, $pharName); $phar->setMetadata(['author' => 'inhere']); $phar->setSignatureAlgorithm(Phar::SHA1); - // begin $phar->startBuffering(); - foreach ($this->srcDirs as $srcDir) { $this->collectFiles($phar, $srcDir); } - // Stubs -// $phar->setStub($this->getStub()); -// $stub = Phar::createDefaultStub($this->options['cliIndex'], $this->options['webIndex']); -// $phar->setStub($stub); - + // $phar->setStub($this->getStub()); + // $stub = Phar::createDefaultStub($this->options['cliIndex'], $this->options['webIndex']); + // $phar->setStub($stub); // 设置入口 - $phar->setStub(""); - + $phar->setStub(""); $phar->stopBuffering(); $count = $phar->count(); unset($phar); @@ -117,9 +98,9 @@ public function pack($pharFile = 'your.phar', $version = '0.0.1') * @param string|null $distDir * @throws \LogicException */ - public function unpack(string $file, string $distDir = null) + public function unpack($file, $distDir = null) { - if (!$distDir = $distDir ?: $this->dstDir) { + if (!($distDir = $distDir ?: $this->dstDir)) { throw new \LogicException('Please setting the dist directory for unpack'); } } @@ -129,17 +110,14 @@ protected function collectFiles(Phar $phar, $srcDir) $iterator = Helper::recursiveDirectoryIterator($srcDir, function ($file) { /** @var \SplFileInfo $file */ $name = $file->getFilename(); - // Skip hidden files and directories. if ($name[0] === '.') { return false; } - if ($file->isDir()) { // Only recurse into intended subdirectories. return preg_match($this->options['dirExclude'], $name); } - if (\in_array($name, $this->options['fileInclude'], true)) { return true; } @@ -147,13 +125,11 @@ protected function collectFiles(Phar $phar, $srcDir) // Only consume files of interest. return preg_match($this->options['fileMatch'], $name); }); - $phar->buildFromIterator($iterator, $srcDir); -// $phar->buildFromDirectory($srcDir, '/[\.php|app]$/'); - -// foreach ($iterator as $file) { -// $this->addFileToPhar($phar, $file, $srcDir); -// } + // $phar->buildFromDirectory($srcDir, '/[\.php|app]$/'); + // foreach ($iterator as $file) { + // $this->addFileToPhar($phar, $file, $srcDir); + // } } /** @@ -167,7 +143,6 @@ private function addFileToPhar($phar, \SplFileInfo $file, $basePath) $isPhp = $file->getExtension() === 'php'; // $path = str_replace($basePath . DIRECTORY_SEPARATOR, '', $file->getRealPath()); $path = substr($file->getRealPath(), \strlen($basePath) + 1); - if ($isPhp && $this->options['compress']) { $content = php_strip_whitespace($file); } elseif ('LICENSE' === basename($file)) { @@ -176,7 +151,6 @@ private function addFileToPhar($phar, \SplFileInfo $file, $basePath) } else { $content = file_get_contents($file); } - $phar->addFromString($path, $content); } @@ -193,9 +167,8 @@ private function addFileToPhar($phar, \SplFileInfo $file, $basePath) protected function getStub() { $content = file_get_contents(__DIR__ . '/../../bin/psysh'); - $content = preg_replace('{/\* <<<.*?>>> \*/}sm', self::STUB_AUTOLOAD, $content); + $content = preg_replace('{/\\* <<<.*?>>> \\*/}sm', self::STUB_AUTOLOAD, $content); $content = preg_replace('/\\(c\\) .*?with this source code./sm', self::getStubLicense(), $content); - $content .= '__HALT_COMPILER();'; return $content; @@ -214,7 +187,7 @@ private static function getStubLicense() * @param string $file * @return $this */ - public function setCliIndex(string $file) + public function setCliIndex($file) { $this->options['cliIndex'] = $file; @@ -225,7 +198,7 @@ public function setCliIndex(string $file) * @param string $file * @return $this */ - public function setWebIndex(string $file) + public function setWebIndex($file) { $this->options['webIndex'] = $file; @@ -247,7 +220,7 @@ public function addFileInclude($name) * @param string $regex * @return $this */ - public function setDirExclude(string $regex) + public function setDirExclude($regex) { $this->options['dirExclude'] = $regex; @@ -258,7 +231,7 @@ public function setDirExclude(string $regex) * @param string $regex * @return $this */ - public function setFileMatch(string $regex) + public function setFileMatch($regex) { $this->options['fileMatch'] = $regex; @@ -269,7 +242,7 @@ public function setFileMatch(string $regex) * @param string $srcDir * @return $this */ - public function addSrcDir(string $srcDir) + public function addSrcDir($srcDir) { $this->srcDirs[] = $srcDir; @@ -279,7 +252,7 @@ public function addSrcDir(string $srcDir) /** * @return array */ - public function getAppendFiles(): array + public function getAppendFiles() { return $this->appendFiles; } @@ -288,7 +261,7 @@ public function getAppendFiles(): array * @param array $appendFiles * @return PharCompiler */ - public function setAppendFiles(array $appendFiles): PharCompiler + public function setAppendFiles(array $appendFiles) { $this->appendFiles = $appendFiles; @@ -298,7 +271,7 @@ public function setAppendFiles(array $appendFiles): PharCompiler /** * @return array */ - public function getSrcDirs(): array + public function getSrcDirs() { return $this->srcDirs; } @@ -314,7 +287,7 @@ public function setSrcDirs($srcDirs) /** * @return string */ - public function getDstDir(): ?string + public function getDstDir() { return $this->dstDir; } @@ -322,7 +295,7 @@ public function getDstDir(): ?string /** * @param string $dstDir */ - public function setDstDir(string $dstDir) + public function setDstDir($dstDir) { $this->dstDir = $dstDir; } @@ -330,7 +303,7 @@ public function setDstDir(string $dstDir) /** * @return array */ - public function getOptions(): array + public function getOptions() { return $this->options; } @@ -342,4 +315,4 @@ public function setOptions(array $options) { $this->options = array_merge($this->options, $options); } -} +} \ No newline at end of file diff --git a/src/Components/Progress/Bar.php b/src/Components/Progress/Bar.php new file mode 100644 index 00000000..03798873 --- /dev/null +++ b/src/Components/Progress/Bar.php @@ -0,0 +1,18 @@ +body .= $content; } @@ -35,7 +36,7 @@ public function write(string $content) /** * @param string $content */ - public function append(string $content) + public function append($content) { $this->write($content); } @@ -43,7 +44,7 @@ public function append(string $content) /** * @param string $content */ - public function prepend(string $content) + public function prepend($content) { $this->body = $content . $this->body; } @@ -59,7 +60,7 @@ public function clear() /** * @return string */ - public function getBody(): string + public function getBody() { return $this->body; } @@ -67,7 +68,7 @@ public function getBody(): string /** * @param string $body */ - public function setBody(string $body) + public function setBody($body) { $this->body = $body; } @@ -75,8 +76,16 @@ public function setBody(string $body) /** * @return string */ - public function __toString() + public function toString() { return $this->body; } + + /** + * @return string + */ + public function __toString() + { + return $this->toString(); + } } \ No newline at end of file diff --git a/src/Utils/AnsiCode.php b/src/Components/Terminal.php similarity index 88% rename from src/Utils/AnsiCode.php rename to src/Components/Terminal.php index 86e066f8..ab22be3a 100644 --- a/src/Utils/AnsiCode.php +++ b/src/Components/Terminal.php @@ -1,4 +1,5 @@ '?25l', - // Will show a cursor again when it has been hidden by [hide] 'show' => '?25h', - // Saves the current cursor position, Position can then be restored with [restorePosition]. 'savePosition' => 's', - // Restores the cursor position saved with [savePosition] 'restorePosition' => 'u', - // Moves the terminal cursor up 'up' => '%dA', - // Moves the terminal cursor down 'down' => '%B', - // Moves the terminal cursor forward - 移动终端光标前进多远 'forward' => '%dC', - // Moves the terminal cursor backward - 移动终端光标后退多远 'backward' => '%dD', - // Moves the terminal cursor to the beginning of the previous line - 移动终端光标到前一行的开始 'prevLine' => '%dF', - // Moves the terminal cursor to the beginning of the next line - 移动终端光标到下一行的开始 'nextLine' => '%dE', - // Moves the cursor to an absolute position given as column and row // $column 1-based column number, 1 is the left edge of the screen. // $row 1-based row number, 1 is the top edge of the screen. if not set, will move cursor only in current line. - 'coordinate' => '%dG|%d;%dH' // only column: '%dG', column and row: '%d;%dH'. + 'coordinate' => '%dG|%d;%dH', ]; - /** * Control screen code list * @var array */ private static $ctrlScreenCodes = [ // Clears entire screen content - 'clear' => '2J', // "033円[2J" - + 'clear' => '2J', + // "033円[2J" // Clears text from cursor to the beginning of the screen 'clearBeforeCursor' => '1J', - // Clears the line - 清除此行 'clearLine' => '2K', - // Clears text from cursor position to the beginning of the line - 清除此行从光标位置开始到开始的字符 'clearLineBeforeCursor' => '1K', - // Clears text from cursor position to the end of the line - 清除此行从光标位置开始到结束的字符 'clearLineAfterCursor' => '0K', - // Scrolls whole page up. e.g "033円[2S" scroll up 2 line. - 上移多少行 'scrollUp' => '%dS', - // Scrolls whole page down.e.g "033円[2T" scroll down 2 line. - 下移多少行 'scrollDown' => '%dT', ]; @@ -121,7 +102,7 @@ final class AnsiCode public static function make() { if (!self::$instance) { - self::$instance = new self; + self::$instance = new self(); } return self::$instance; @@ -129,12 +110,10 @@ public static function make() /** * build ansi code string - * * ``` - * AnsiCode::build(null, 'u'); // "033円[s" Saves the current cursor position - * AnsiCode::build(0); // "033円[0m" Build end char, Resets any ANSI format + * Terminal::build(null, 'u'); // "033円[s" Saves the current cursor position + * Terminal::build(0); // "033円[0m" Build end char, Resets any ANSI format * ``` - * * @param mixed $format * @param string $type * @return string @@ -143,7 +122,7 @@ public static function build($format, $type = 'm') { $format = null === $format ? '' : implode(';', (array)$format); - return "033円[" . implode(';', (array)$format) . $type; + return "33円[" . implode(';', (array)$format) . $type; } /** @@ -156,28 +135,23 @@ public static function build($format, $type = 'm') public function cursor($typeName, $arg1 = 1, $arg2 = null) { if (!isset(self::$ctrlCursorCodes[$typeName])) { - Show::error("The [$typeName] is not supported cursor control.", __LINE__); + Show::error("The [{$typeName}] is not supported cursor control.", __LINE__); } - $code = self::$ctrlCursorCodes[$typeName]; - // allow argument if (false !== strpos($code, '%')) { // The special code: ` 'coordinate' => '%dG|%d;%dH' ` if ($typeName === self::CURSOR_COORDINATE) { $codes = explode('|', $code); - if (null === $arg2) { $code = sprintf($codes[0], $arg1); } else { $code = sprintf($codes[1], $arg1, $arg2); } - } else { $code = sprintf($code, $arg1); } } - echo self::build($code, ''); return $this; @@ -192,16 +166,13 @@ public function cursor($typeName, $arg1 = 1, $arg2 = null) public function screen($typeName, $arg = null) { if (!isset(self::$ctrlScreenCodes[$typeName])) { - Show::error("The [$typeName] is not supported cursor control.", __LINE__); + Show::error("The [{$typeName}] is not supported cursor control.", __LINE__); } - $code = self::$ctrlScreenCodes[$typeName]; - // allow argument if (false !== strpos($code, '%')) { $code = sprintf($code, $arg); } - echo self::build($code, ''); return $this; @@ -227,4 +198,4 @@ public static function supportedScreenCtrl() { return array_keys(self::$ctrlScreenCodes); } -} +} \ No newline at end of file diff --git a/src/Components/TextTemplate.php b/src/Components/TextTemplate.php new file mode 100644 index 00000000..8b5feed0 --- /dev/null +++ b/src/Components/TextTemplate.php @@ -0,0 +1,191 @@ +setVars($vars); + } + } + + /** + * @param string $tplFile + * @param array $vars + * @param null|string $saveAs + * @return string|bool + */ + public function renderFile($tplFile, array $vars = [], $saveAs = null) + { + if (!\is_file($tplFile)) { + throw new \InvalidArgumentException("Template file not exists. FILE: {$tplFile}"); + } + + return $this->render(file_get_contents($tplFile), $vars, $saveAs); + } + + /** + * @param string $template + * @param array $vars + * @param null|string $saveAs + * @return string + */ + public function render($template, array $vars = [], $saveAs = null) + { + if (!$template || false === strpos($template, $this->openChar)) { + return $template; + } + if ($this->vars) { + $vars = array_merge($this->vars, $vars); + } + $pairs = $map = []; + $this->expandVars($vars, $map); + foreach ($map as $name => $value) { + $key = $this->openChar . $name . $this->closeChar; + $pairs[$key] = $value; + } + // replace vars to values. + $rendered = strtr($template, $pairs); + if (!$saveAs) { + return $rendered; + } + $dstDir = \dirname($saveAs); + if (!is_dir($dstDir) && !mkdir($dstDir, 0775, true) && !is_dir($dstDir)) { + throw new \RuntimeException(sprintf('Directory "%s" was not created', $dstDir)); + } + + return (bool)file_put_contents($saveAs, $rendered); + } + + /** + * Multidimensional array expansion to one dimension array + * @param array $vars + * @param null|string $prefix + * @param array $map + */ + protected function expandVars(array $vars, array &$map = [], $prefix = null) + { + foreach ($vars as $name => $value) { + $key = $prefix !== null ? $prefix . '.' . $name : $name; + if (is_scalar($value)) { + $map[$key] = $value; + } elseif (\is_array($value)) { + $this->expandVars($value, $map, (string)$key); + } + } + } + + /** + * @param string $name + * @param null $default + * @return mixed + */ + public function getVar($name, $default = null) + { + return isset($this->vars[$name]) ? $this->vars[$name] : $default; + } + + /** + * @param string $name + * @param mixed $value + */ + public function addVar($name, $value) + { + if (!isset($this->vars[$name])) { + $this->vars[$name] = $value; + } + } + + /** + * @param string $name + * @param mixed $value + */ + public function setVar($name, $value) + { + $this->vars[$name] = $value; + } + + /** + * @param array $vars + */ + public function addVars(array $vars) + { + $this->vars = array_merge($this->vars, $vars); + } + + /** + * @return array + */ + public function getVars() + { + return $this->vars; + } + + /** + * @param array $vars + */ + public function setVars(array $vars) + { + $this->vars = $vars; + } + + /** + * @return string + */ + public function getOpenChar() + { + return $this->openChar; + } + + /** + * @param string $openChar + */ + public function setOpenChar($openChar) + { + if ($openChar) { + $this->openChar = $openChar; + } + } + + /** + * @return string + */ + public function getCloseChar() + { + return $this->closeChar; + } + + /** + * @param string $closeChar + */ + public function setCloseChar($closeChar) + { + if ($closeChar) { + $this->closeChar = $closeChar; + } + } +} \ No newline at end of file diff --git a/src/Controller.php b/src/Controller.php index 4604c262..07ab70fe 100644 --- a/src/Controller.php +++ b/src/Controller.php @@ -1,4 +1,5 @@ action = trim($command)) { + $this->action = $this->getRealCommandName(trim($command, $this->delimiter)); + if (!$this->action) { return $this->showHelp(); } @@ -55,13 +65,12 @@ public function run($command = '') /** * load command configure */ - protected function configure() + protected final function configure() { if ($action = $this->action) { $method = $action . 'Configure'; - if (method_exists($this, $method)) { - $this->$method(); + $this->{$method}(); } } } @@ -71,37 +80,24 @@ protected function configure() * @param Input $input * @param Output $output * @return mixed - * @throws \ReflectionException */ protected function execute($input, $output) { - $action = Helper::camelCase(trim($this->action ?: $this->defaultAction, $this->delimiter)); + $action = FormatUtil::camelCase(trim($this->action ?: $this->defaultAction, $this->delimiter)); $method = $this->actionSuffix ? $action . ucfirst($this->actionSuffix) : $action; - // the action method exists and only allow access public method. if (method_exists($this, $method) && (($rfm = new \ReflectionMethod($this, $method)) && $rfm->isPublic())) { // run action - $status = $this->$method($input, $output); - + $status = $this->{$method}($input, $output); // if you defined the method '$this->notFoundCallback' , will call it } elseif (($notFoundCallback = $this->notFoundCallback) && method_exists($this, $notFoundCallback)) { $status = $this->{$notFoundCallback}($action); } else { $group = static::getName(); $status = -1; - $output->liteError("Sorry, The command '$action' not exist of the group '{$group}'!"); - - // find similar command names by similar_text() - $similar = []; - - foreach ($this->getAllCommandMethods() as $cmd => $refM) { - similar_text($action, $cmd, $percent); - - if (45 <= (int)$percent) { - $similar[] = $cmd; - } - } - + $output->liteError("Sorry, The command '{$action}' not exist of the group '{$group}'!"); + // find similar command names + $similar = Helper::findSimilar($action, $this->getAllCommandMethods(null, true)); if ($similar) { $output->write(sprintf("\nMaybe what you mean is:\n %s", implode(', ', $similar))); } else { @@ -114,7 +110,6 @@ protected function execute($input, $output) /** * @return int - * @throws \ReflectionException */ protected function showHelp() { @@ -127,122 +122,146 @@ protected function showHelp() /** * Show help of the controller command group or specified command action - * @usage {name}/[command] -h OR {command} [command] OR {name} [command] -h + * @usage {name}:[command] -h OR {command} [command] OR {name} [command] -h + * @options + * -s, --search Search command by input keywords + * --format Set the help information dump format(raw, xml, json, markdown) * @example * {script} {name} -h - * {script} {name}/help - * {script} {name}/help index - * {script} {name}/index -h + * {script} {name}:help + * {script} {name}:help index + * {script} {name}:index -h * {script} {name} index - * * @return int - * @throws \ReflectionException */ - final public function helpCommand() + public final function helpCommand() { $action = $this->action; - // show all commands of the controller if (!$action && !($action = $this->input->getFirstArg())) { $this->showCommandList(); + return 0; } - - $action = Helper::camelCase($action); + $action = FormatUtil::camelCase($action); $method = $this->actionSuffix ? $action . ucfirst($this->actionSuffix) : $action; + $aliases = self::getCommandAliases($action); // show help info for a command. - return $this->showHelpByMethodAnnotation($method, $action); + return $this->showHelpByMethodAnnotations($method, $action, $aliases); } /** * show command list of the controller class - * @throws \ReflectionException */ - final public function showCommandList() + public final function showCommandList() { $ref = new \ReflectionClass($this); $sName = lcfirst(self::getName() ?: $ref->getShortName()); - if (!($classDes = self::getDescription())) { - $classDes = Annotation::description($ref->getDocComment()) ?: 'No Description for the console controller'; + $classDes = Annotation::description($ref->getDocComment()) ?: 'No description for the console controller'; } - $commands = []; + $defCommandDes = 'No description message'; foreach ($this->getAllCommandMethods($ref) as $cmd => $m) { - $desc = Annotation::firstLine($m->getDocComment()); - + $desc = Annotation::firstLine($m->getDocComment()) ?: $defCommandDes; + // is a annotation tag + if ($desc[0] === '@') { + $desc = $defCommandDes; + } if ($cmd) { - $commands[$cmd] = $desc; + $aliases = self::getCommandAliases($cmd); + $extra = $aliases ? Helper::wrapTag(' [alias: ' . implode(',', $aliases) . ']', 'info') : ''; + $commands[$cmd] = $desc . $extra; } } - // sort commands ksort($commands); - // move 'help' to last. - if ($helpCmd = $commands['help'] ?? null) { + if ($helpCmd = isset($commands['help']) ? $commands['help'] : null) { unset($commands['help']); $commands['help'] = $helpCmd; } - $script = $this->getScriptName(); - if ($this->standAlone) { $name = $sName . ' '; - $usage = "$script {command} [arguments] [options]"; + $usage = "{$script} {command} [arguments ...] [options ...]"; } else { $name = $sName . $this->delimiter; - $usage = "$script {$name}{command} [arguments] [options]"; + $usage = "{$script} {$name}{command} [arguments ...] [options ...]"; } - + $this->output->startBuffer(); + $this->output->write(ucfirst($classDes) . PHP_EOL); $this->output->mList([ - 'Description:' => $classDes, 'Usage:' => $usage, //'Group Name:' => "$sName", + 'Options:' => ['-h, --help' => 'Show help of the command group or specified command action'], 'Commands:' => $commands, - 'Options:' => [ - '-h,--help' => 'Show help of the command group or specified command action', - ], - ]); - - $this->write(sprintf( - "More information about a command, please use: $script $name{command} -h", - $this->standAlone ? ' ' . $name : '' - )); + ], ['sepChar' => ' ']); + $this->write(sprintf("More information about a command, please use: {$script} {$name}{command} -h", $this->standAlone ? ' ' . $name : '')); + $this->output->flush(); } /** * @param \ReflectionClass|null $ref + * @param bool $onlyName * @return \Generator */ - protected function getAllCommandMethods(\ReflectionClass $ref = null) + protected function getAllCommandMethods(\ReflectionClass $ref = null, $onlyName = false) { $ref = $ref ?: new \ReflectionObject($this); - $suffix = $this->actionSuffix; $suffixLen = Helper::strLen($suffix); - foreach ($ref->getMethods() as $m) { $mName = $m->getName(); - - if ($m->isPublic() && substr($mName, - $suffixLen) === $suffix) { + if ($m->isPublic() && substr($mName, -$suffixLen) === $suffix) { // suffix is empty ? $cmd = $suffix ? substr($mName, 0, -$suffixLen) : $mName; - - yield $cmd => $m; + if ($onlyName) { + (yield $cmd); + } else { + (yield $cmd => $m); + } } } } + /** + * @param string $name + * @return mixed|string + */ + protected function getRealCommandName($name) + { + if (!$name) { + return $name; + } + $map = self::getCommandAliases(); + + return isset($map[$name]) ? $map[$name] : $name; + } /************************************************************************** * getter/setter methods **************************************************************************/ + /** + * @param string|null $name + * @return array + */ + public static function getCommandAliases($name = null) + { + if (null === self::$aliases) { + self::$aliases = static::commandAliases(); + } + if ($name) { + return self::$aliases ? array_keys(self::$aliases, $name, true) : []; + } + + return self::$aliases; + } /** * @return string */ - public function getAction(): string + public function getAction() { return $this->action; } @@ -251,10 +270,10 @@ public function getAction(): string * @param string $action * @return $this */ - public function setAction(string $action) + public function setAction($action) { if ($action) { - $this->action = Helper::camelCase($action); + $this->action = FormatUtil::camelCase($action); } return $this; @@ -263,7 +282,7 @@ public function setAction(string $action) /** * @return string */ - public function getDefaultAction(): string + public function getDefaultAction() { return $this->defaultAction; } @@ -279,7 +298,7 @@ public function setDefaultAction($defaultAction) /** * @return string */ - public function getActionSuffix(): string + public function getActionSuffix() { return $this->actionSuffix; } @@ -311,7 +330,7 @@ public function setNotFoundCallback($notFoundCallback) /** * @return bool */ - public function isStandAlone(): bool + public function isStandAlone() { return $this->standAlone; } @@ -327,7 +346,7 @@ public function setStandAlone($standAlone = true) /** * @return string */ - public function getDelimiter(): string + public function getDelimiter() { return $this->delimiter; } @@ -335,8 +354,8 @@ public function getDelimiter(): string /** * @param string $delimiter */ - public function setDelimiter(string $delimiter) + public function setDelimiter($delimiter) { $this->delimiter = $delimiter; } -} +} \ No newline at end of file diff --git a/src/IO/FixedInput.php b/src/IO/FixedInput.php new file mode 100644 index 00000000..9b872018 --- /dev/null +++ b/src/IO/FixedInput.php @@ -0,0 +1,101 @@ + has value + 'h' => false, + 'V' => false, + 'help' => false, + 'debug' => true, + 'profile' => false, + 'version' => false, + ]; + /** @var array */ + private $cleanedTokens; + + /** + * FixedInput constructor. + * @param null|array $argv + */ + public function __construct($argv = null) + { + if (null === $argv) { + $argv = $_SERVER['argv']; + } + parent::__construct($argv, false); + $copy = $argv; + // command name + if (!empty($copy[1]) && $copy[1][0] !== '-' && false === strpos($copy[1], '=')) { + $this->setCommand($copy[1]); + // unset command + unset($copy[1]); + } + // pop script name + array_shift($copy); + $this->cleanedTokens = $copy; + $this->collectPreParsed($copy); + } + + private function collectPreParsed(array $tokens) + { + foreach ($this->preParsed as $name => $hasVal) { + } + } + + /** + * @param array $allowArray + * @param array $noValues + */ + public function parseTokens(array $allowArray = [], array $noValues = []) + { + $params = $this->getTokens(); + array_shift($params); + // pop script name + } + + /** + * @return array + */ + public function getPreParsed() + { + return $this->preParsed; + } + + /** + * @param array $preParsed + */ + public function setPreParsed(array $preParsed) + { + $this->preParsed = $preParsed; + } + + /** + * @return array|null + */ + public function getCleanedTokens() + { + return $this->cleanedTokens; + } +} \ No newline at end of file diff --git a/src/IO/Input.php b/src/IO/Input.php index 4d3261a6..d407de9f 100644 --- a/src/IO/Input.php +++ b/src/IO/Input.php @@ -1,4 +1,5 @@ pwd = $this->getPwd(); - + $this->tokens = $argv; $this->fullScript = implode(' ', $argv); $this->script = array_shift($argv); - $this->tokens = $argv; - - list($this->args, $this->sOpts, $this->lOpts) = ArgumentOptionParse::byArgv($argv); - - // collect command `server` - $this->command = isset($this->args[0]) ? array_shift($this->args) : null; + if ($parsing) { + list($this->args, $this->sOpts, $this->lOpts) = CommandLine::parseByArgv($argv); + // collect command. it is first argument. + $this->command = isset($this->args[0]) ? array_shift($this->args) : null; + } } /** @@ -98,11 +91,10 @@ public function __toString() { $tokens = array_map(function ($token) { if (preg_match('{^(-[^=]+=)(.+)}', $token, $match)) { - return $match[1] . ArgumentOptionParse::escapeToken($match[2]); + return $match[1] . CommandLine::escapeToken($match[2]); } - if ($token && $token[0] !== '-') { - return ArgumentOptionParse::escapeToken($token); + return CommandLine::escapeToken($token); } return $token; @@ -117,21 +109,21 @@ public function __toString() * @param bool $nl true 会添加换行符 false 原样输出,不添加换行符 * @return string */ - public function read($question = null, $nl = false): string + public function read($question = null, $nl = false) { - fwrite(STDOUT, $question . ($nl ? "\n" : '')); + if ($question) { + fwrite(\STDOUT, $question . ($nl ? "\n" : '')); + } return trim(fgets($this->inputStream)); } - -///////////////////////////////////////////////////////////////////////////////////////// -/// arguments (eg: name=john city=chengdu) -///////////////////////////////////////////////////////////////////////////////////////// - + /*************************************************************************** + * arguments (eg: arg0 name=john city=chengdu) + ***************************************************************************/ /** * @return array */ - public function getArguments(): array + public function getArguments() { return $this->args; } @@ -139,7 +131,7 @@ public function getArguments(): array /** * @return array */ - public function getArgs(): array + public function getArgs() { return $this->args; } @@ -157,7 +149,7 @@ public function setArgs(array $args, $replace = false) * @param string|int $name * @return bool */ - public function hasArg($name): bool + public function hasArg($name) { return isset($this->args[$name]); } @@ -192,7 +184,7 @@ public function getArg($name, $default = null) */ public function get($name, $default = null) { - return $this->args[$name] ?? $default; + return isset($this->args[$name]) ? $this->args[$name] : $default; } /** @@ -206,7 +198,6 @@ public function getRequiredArg($name) if ('' !== $this->get($name, '')) { return $this->args[$name]; } - throw new \InvalidArgumentException("The argument '{$name}' is required"); } @@ -215,7 +206,7 @@ public function getRequiredArg($name) * @param string $default * @return string */ - public function getFirstArg($default = ''): string + public function getFirstArg($default = '') { return $this->get(0, $default); } @@ -225,7 +216,7 @@ public function getFirstArg($default = ''): string * @param string $default * @return string */ - public function getSecondArg($default = ''): string + public function getSecondArg($default = '') { return $this->get(1, $default); } @@ -235,7 +226,7 @@ public function getSecondArg($default = ''): string * @param int $default * @return int */ - public function getInt($key, $default = 0): int + public function getInt($key, $default = 0) { $value = $this->get($key); @@ -245,11 +236,9 @@ public function getInt($key, $default = 0): int /** * get same args value * eg: des description - * * ```php * $input->sameArg(['des', 'description']); * ``` - * * @param array $names * @param mixed $default * @return bool|mixed|null @@ -282,11 +271,9 @@ public function clearArgs() { $this->args = []; } - /*************************************************************************** * long/short options (eg: -d --help) ***************************************************************************/ - /** * get (long/short)opt value * eg: -e dev --name sam @@ -294,10 +281,10 @@ public function clearArgs() * @param null $default * @return bool|mixed|null */ - public function getOpt(string $name, $default = null) + public function getOpt($name, $default = null) { // is long-opt - if (isset($name{1})) { + if (isset($name[1])) { return $this->lOpt($name, $default); } @@ -310,7 +297,7 @@ public function getOpt(string $name, $default = null) * @param mixed $default * @return mixed */ - public function getOption(string $name, $default = null) + public function getOption($name, $default = null) { return $this->getOpt($name, $default); } @@ -337,12 +324,12 @@ public function getRequiredOpt($name) * @param bool $default * @return bool */ - public function getBoolOpt(string $name, $default = false): bool + public function getBoolOpt($name, $default = false) { return (bool)$this->getOpt($name, $default); } - public function boolOpt(string $name, $default = false): bool + public function boolOpt($name, $default = false) { return (bool)$this->getOpt($name, $default); } @@ -352,7 +339,7 @@ public function boolOpt(string $name, $default = false): bool * @param $name * @return bool */ - public function hasOpt(string $name): bool + public function hasOpt($name) { return isset($this->sOpts[$name]) || isset($this->lOpts[$name]); } @@ -360,11 +347,9 @@ public function hasOpt(string $name): bool /** * get same opts value * eg: -h --help - * * ```php * $input->sameOpt(['h','help']); * ``` - * * @param array $names * @param mixed $default * @return bool|mixed|null @@ -392,9 +377,7 @@ public function clearOpts() { $this->sOpts = $this->lOpts = []; } - /************************** short-opts **********************/ - /** * get short-opt value * @param $name @@ -403,12 +386,12 @@ public function clearOpts() */ public function sOpt($name, $default = null) { - return $this->sOpts[$name] ?? $default; + return isset($this->sOpts[$name]) ? $this->sOpts[$name] : $default; } public function getShortOpt($name, $default = null) { - return $this->sOpts[$name] ?? $default; + return isset($this->sOpts[$name]) ? $this->sOpts[$name] : $default; } /** @@ -416,7 +399,7 @@ public function getShortOpt($name, $default = null) * @param $name * @return bool */ - public function hasSOpt(string $name): bool + public function hasSOpt($name) { return isset($this->sOpts[$name]); } @@ -427,7 +410,7 @@ public function hasSOpt(string $name): bool * @param bool $default * @return bool */ - public function sBoolOpt(string $name, $default = false): bool + public function sBoolOpt($name, $default = false) { $val = $this->sOpt($name); @@ -437,7 +420,7 @@ public function sBoolOpt(string $name, $default = false): bool /** * @return array */ - public function getShortOpts(): array + public function getShortOpts() { return $this->sOpts; } @@ -446,7 +429,7 @@ public function getShortOpts(): array * @param string $name * @param $value */ - public function setSOpt(string $name, $value) + public function setSOpt($name, $value) { $this->sOpts[$name] = $value; } @@ -454,7 +437,7 @@ public function setSOpt(string $name, $value) /** * @return array */ - public function getSOpts(): array + public function getSOpts() { return $this->sOpts; } @@ -475,9 +458,7 @@ public function clearSOpts() { $this->sOpts = []; } - /************************** long-opts **********************/ - /** * get long-opt value * @param $name @@ -486,12 +467,12 @@ public function clearSOpts() */ public function lOpt($name, $default = null) { - return $this->lOpts[$name] ?? $default; + return isset($this->lOpts[$name]) ? $this->lOpts[$name] : $default; } public function getLongOpt($name, $default = null) { - return $this->lOpts[$name] ?? $default; + return isset($this->lOpts[$name]) ? $this->lOpts[$name] : $default; } /** @@ -499,7 +480,7 @@ public function getLongOpt($name, $default = null) * @param $name * @return bool */ - public function hasLOpt(string $name): bool + public function hasLOpt($name) { return isset($this->lOpts[$name]); } @@ -510,7 +491,7 @@ public function hasLOpt(string $name): bool * @param bool $default * @return bool */ - public function lBoolOpt(string $name, $default = false): bool + public function lBoolOpt($name, $default = false) { $val = $this->lOpt($name); @@ -520,7 +501,7 @@ public function lBoolOpt(string $name, $default = false): bool /** * @return array */ - public function getLongOpts(): array + public function getLongOpts() { return $this->lOpts; } @@ -528,7 +509,7 @@ public function getLongOpts(): array /** * @return array */ - public function getLOpts(): array + public function getLOpts() { return $this->lOpts; } @@ -537,7 +518,7 @@ public function getLOpts(): array * @param string $name * @param $value */ - public function setLOpt(string $name, $value) + public function setLOpt($name, $value) { $this->lOpts[$name] = $value; } @@ -554,7 +535,7 @@ public function setLOpts(array $lOpts, $replace = false) /** * @return array */ - public function getOpts(): array + public function getOpts() { return array_merge($this->sOpts, $this->lOpts); } @@ -566,15 +547,13 @@ public function clearLOpts() { $this->lOpts = []; } - -///////////////////////////////////////////////////////////////////////////////////////// -/// getter/setter -///////////////////////////////////////////////////////////////////////////////////////// - + ///////////////////////////////////////////////////////////////////////////////////////// + /// getter/setter + ///////////////////////////////////////////////////////////////////////////////////////// /** * @return string */ - public function getFullScript(): string + public function getFullScript() { return $this->fullScript; } @@ -582,7 +561,7 @@ public function getFullScript(): string /** * @return string */ - public function getScriptName(): string + public function getScriptName() { return $this->script; } @@ -590,7 +569,7 @@ public function getScriptName(): string /** * @return string */ - public function getScript(): string + public function getScript() { return $this->script; } @@ -598,7 +577,7 @@ public function getScript(): string /** * @param string $script */ - public function setScript(string $script) + public function setScript($script) { $this->script = $script; } @@ -607,7 +586,7 @@ public function setScript(string $script) * @param null|string $default * @return string */ - public function getCommand($default = ''): string + public function getCommand($default = '') { return $this->command ?: $default; } @@ -615,7 +594,7 @@ public function getCommand($default = ''): string /** * @param string $command */ - public function setCommand(string $command) + public function setCommand($command) { $this->command = $command; } @@ -631,7 +610,7 @@ public function getInputStream() /** * @return string */ - public function getPwd(): string + public function getPwd() { if (!$this->pwd) { $this->pwd = getcwd(); @@ -639,4 +618,12 @@ public function getPwd(): string return $this->pwd; } -} + + /** + * @return array + */ + public function getTokens() + { + return $this->tokens; + } +} \ No newline at end of file diff --git a/src/IO/InputDefinition.php b/src/IO/InputDefinition.php index 3f6d8281..b1bdd7e1 100644 --- a/src/IO/InputDefinition.php +++ b/src/IO/InputDefinition.php @@ -1,4 +1,5 @@ null, 'description' => '', 'default' => null]; private $example; private $description; - /** * @var array[] */ private $arguments; private $requiredCount = 0; - private $hasAnArrayArgument = false; private $hasOptional = false; - + private $hasAnArrayArgument = false; /** * @var array[] */ private $options; + /** @var array */ + private $shortcuts; /** - * @var array + * @param array $arguments + * @param array $options + * @return InputDefinition */ - private $shortcuts; - public static function make(array $arguments = [], array $options = []) { return new self($arguments, $options); @@ -43,7 +46,6 @@ public static function make(array $arguments = [], array $options = []) /** * Constructor. - * * @param array $arguments * @param array $options */ @@ -71,14 +73,8 @@ public function setArguments(array $arguments) */ public function addArguments(array $arguments) { - $def = [ - 'mode' => null, - 'description' => '', - 'default' => null, - ]; - foreach ($arguments as $name => $arg) { - $arg = array_merge($def, $arg); + $arg = $this->mergeArgOptConfig($arg); $this->addArgument($name, $arg['mode'], $arg['description'], $arg['default']); } @@ -87,7 +83,6 @@ public function addArguments(array $arguments) /** * Adds an argument. - * * @param string $name The argument name * @param int $mode The argument mode: InputArgument::REQUIRED or InputArgument::OPTIONAL * @param string $description A description text @@ -102,50 +97,35 @@ public function addArgument($name, $mode = null, $description = '', $default = n } elseif (!\is_int($mode) || $mode> 7 || $mode < 1) { throw new \InvalidArgumentException(sprintf('Argument mode "%s" is not valid.', $mode)); } - if (isset($this->arguments[$name])) { throw new \LogicException(sprintf('An argument with name "%s" already exists.', $name)); } - if ($this->hasAnArrayArgument) { throw new \LogicException('Cannot add an argument after an array argument.'); } - if (($required = $mode === Input::ARG_REQUIRED) && $this->hasOptional) { throw new \LogicException('Cannot add a required argument after an optional one.'); } - if ($isArray = ($mode === Input::ARG_IS_ARRAY)) { if (!$this->argumentIsAcceptValue($mode)) { throw new \InvalidArgumentException('Impossible to have an option mode ARG_IS_ARRAY if the option does not accept a value.'); } - $this->hasAnArrayArgument = true; - if (null === $default) { $default = []; } elseif (!\is_array($default)) { throw new \LogicException('A default value for an array argument must be an array.'); } } - if ($required) { if (null !== $default) { throw new \LogicException('Cannot set a default value except for OPTIONAL-ARGUMENT mode.'); } - ++$this->requiredCount; } else { $this->hasOptional = true; } - - $this->arguments[$name] = [ - 'mode' => $mode, - 'required' => $required, - 'isArray' => $isArray, - 'description' => $description, - 'default' => $default, - ]; + $this->arguments[$name] = ['mode' => $mode, 'required' => $required, 'isArray' => $isArray, 'description' => $description, 'default' => $default]; return $this; } @@ -160,7 +140,6 @@ public function getArgument($name) if (!$this->hasArgument($name)) { throw new \InvalidArgumentException(sprintf('The "%s" argument does not exist.', $name)); } - $arguments = \is_int($name) ? array_values($this->arguments) : $this->arguments; return $arguments[$name]; @@ -204,7 +183,6 @@ public function getArgumentRequiredCount() /** * Sets the options - * * @param array[] $options An array of InputOption objects * @throws \LogicException * @throws \InvalidArgumentException @@ -217,34 +195,25 @@ public function setOptions(array $options = []) /** * Adds an array of option - * * @param array * @throws \LogicException * @throws \InvalidArgumentException */ public function addOptions(array $options = []) { - $def = [ - 'mode' => null, - 'description' => '', - 'default' => null, - ]; - foreach ($options as $name => $opt) { - $opt = array_merge($def, $opt); + $opt = $this->mergeArgOptConfig($opt); $this->addOption($name, $opt['mode'], $opt['description'], $opt['default']); } } /** * Adds an option. - * * @param string|bool $name The option name, must is a string * @param string|array $shortcut The shortcut (can be null) * @param int $mode The option mode: One of the Input::OPT_* constants * @param string $description A description text * @param mixed $default The default value (must be null for InputOption::OPT_BOOL) - * * @return $this * @throws \InvalidArgumentException * @throws \LogicException @@ -254,34 +223,27 @@ public function addOption($name, $shortcut = null, $mode = null, $description = if (0 === strpos($name, '--')) { $name = substr($name, 2); } - if (empty($name)) { throw new \InvalidArgumentException('An option name cannot be empty.'); } - if (empty($shortcut)) { $shortcut = null; } - if (null === $mode) { $mode = Input::OPT_BOOLEAN; } elseif (!\is_int($mode) || $mode> 15 || $mode < 1) { throw new \InvalidArgumentException(sprintf('Option mode "%s" is not valid.', $mode)); } - if (($isArray = $mode === Input::OPT_IS_ARRAY) && !$this->optionIsAcceptValue($mode)) { throw new \InvalidArgumentException('Impossible to have an option mode VALUE_IS_ARRAY if the option does not accept a value.'); } - if (isset($this->options[$name])) { throw new \LogicException(sprintf('An option named "%s" already exists.', $name)); } - // set default value if (Input::OPT_BOOLEAN === (Input::OPT_BOOLEAN & $mode) && null !== $default) { throw new \LogicException('Cannot set a default value when using Input::OPT_BOOLEAN mode.'); } - if ($isArray) { if (null === $default) { $default = array(); @@ -289,30 +251,25 @@ public function addOption($name, $shortcut = null, $mode = null, $description = throw new \LogicException('A default value for an array option must be an array.'); } } - $default = $this->optionIsAcceptValue($mode) ? $default : false; - if ($shortcut) { if (\is_array($shortcut)) { $shortcut = implode('|', $shortcut); } - - $shortcuts = preg_split('{(\|)-?}', ltrim($shortcut, '-')); + $shortcuts = preg_split('{(\\|)-?}', ltrim($shortcut, '-')); $shortcuts = array_filter($shortcuts); $shortcut = implode('|', $shortcuts); - foreach ($shortcuts as $srt) { if (isset($this->shortcuts[$srt])) { throw new \LogicException(sprintf('An option with shortcut "%s" already exists.', $srt)); } - $this->shortcuts[$srt] = $name; } } - $this->options[$name] = [ 'mode' => $mode, - 'shortcut' => $shortcut, // 允许数组 + 'shortcut' => $shortcut, + // 允许数组 'required' => $mode === Input::OPT_REQUIRED, 'optional' => $mode === Input::OPT_OPTIONAL, 'description' => $description, @@ -347,7 +304,6 @@ public function hasOption($name) /** * Gets the array of options - * * @return array[] */ public function getOptions() @@ -388,6 +344,15 @@ private function shortcutToName($shortcut) return $this->shortcuts[$shortcut]; } + /** + * @param array $map + * @return array + */ + private function mergeArgOptConfig(array $map) + { + return array_merge(self::$defaultArgOptConfig, $map); + } + /** * Gets the synopsis. * @param bool $short 简化版显示 @@ -396,61 +361,40 @@ private function shortcutToName($shortcut) public function getSynopsis($short = false) { $elements = $args = $opts = []; - if ($short && $this->options) { $elements[] = '[options]'; } elseif (!$short) { foreach ($this->options as $name => $option) { $value = ''; - if ($this->optionIsAcceptValue($option['mode'])) { - $value = sprintf( - ' %s%s%s', - $option['optional'] ? '[' : '', - strtoupper($name), - $option['optional'] ? ']' : '' - ); + $value = sprintf(' %s%s%s', $option['optional'] ? '[' : '', strtoupper($name), $option['optional'] ? ']' : ''); } - - $shortcut = $option['shortcut'] ? sprintf('-%s|', $option['shortcut']) : ''; + $shortcut = $option['shortcut'] ? sprintf('-%s, ', $option['shortcut']) : ''; $elements[] = sprintf('[%s--%s%s]', $shortcut, $name, $value); - $key = "{$shortcut}--{$name}"; $opts[$key] = ($option['required'] ? '*' : '') . $option['description']; } } - if ($this->arguments && \count($elements)) { $elements[] = '[--]'; } - foreach ($this->arguments as $name => $argument) { $des = $argument['required'] ? '*' . $argument['description'] : $argument['description']; - $element = '<' . $name . '>'; if (!$argument['required']) { $element = '[' . $element . ']'; } elseif ($argument['isArray']) { $element = $element . ' (' . $element . ')'; } - if ($argument['isArray']) { $element .= '...'; } - $elements[] = $element; $args[$name] = $des; } + $opts['-h, --help'] = 'Show help information for the command'; - $opts['-h|--help'] = 'Show help information for the command'; - - return [ - 'description' => $this->description, - 'usage' => implode(' ', $elements), - 'arguments' => $args, - 'options' => $opts, - 'example' => $this->example, - ]; + return ['description' => $this->description, 'usage' => implode(' ', $elements), 'arguments' => $args, 'options' => $opts, 'example' => $this->example]; } /** @@ -525,8 +469,8 @@ public function setDescription($description) /** * @return array */ - public function getShortcuts(): array + public function getShortcuts() { return $this->shortcuts; } -} +} \ No newline at end of file diff --git a/src/IO/InputInterface.php b/src/IO/InputInterface.php index 51592e82..e4557a92 100644 --- a/src/IO/InputInterface.php +++ b/src/IO/InputInterface.php @@ -1,4 +1,5 @@ outputStream = $outputStream; } - $this->getStyle(); } + /*************************************************************************** + * Output buffer + ***************************************************************************/ + /** + * start buffering + */ + public function startBuffer() + { + Show::startBuffer(); + } -///////////////////////////////////////////////////////////////// -/// Output Message -///////////////////////////////////////////////////////////////// + /** + * clear buffering + */ + public function clearBuffer() + { + Show::clearBuffer(); + } + /** + * stop buffering and flush buffer text + * {@inheritdoc} + * @see Show::stopBuffer() + */ + public function stopBuffer($flush = true, $nl = false, $quit = false, array $opts = []) + { + Show::stopBuffer($flush, $nl, $quit, $opts); + } + + /** + * stop buffering and flush buffer text + * {@inheritdoc} + */ + public function flush($nl = false, $quit = false, array $opts = []) + { + $this->stopBuffer(true, $nl, $quit, $opts); + } + /*************************************************************************** + * Output Message + ***************************************************************************/ /** * 读取输入信息 * @param string $question 若不为空,则先输出文本 * @param bool $nl true 会添加换行符 false 原样输出,不添加换行符 * @return string */ - public function read($question = null, $nl = false): string + public function read($question = null, $nl = false) { if ($question) { $this->write($question, $nl); } - return trim(fgets(STDIN)); + return trim(fgets(\STDIN)); } /** @@ -81,20 +113,17 @@ public function read($question = null, $nl = false): string public function stderr($text = '', $nl = true) { $text = $this->getStyle()->format($text); - fwrite($this->errorStream, $text . ($nl ? "\n" : null)); return $this; } - -///////////////////////////////////////////////////////////////// -/// Getter/Setter -///////////////////////////////////////////////////////////////// - + /*************************************************************************** + * Getter/Setter + ***************************************************************************/ /** * @return Style */ - public function getStyle(): Style + public function getStyle() { if (!$this->style) { $this->style = Show::getStyle(); @@ -106,7 +135,7 @@ public function getStyle(): Style /** * @return bool */ - public function supportColor(): bool + public function supportColor() { return Helper::isSupportColor(); } @@ -150,4 +179,4 @@ public function setErrorStream($errorStream) return $this; } -} +} \ No newline at end of file diff --git a/src/IO/OutputInterface.php b/src/IO/OutputInterface.php index 6c1c5458..1e934943 100644 --- a/src/IO/OutputInterface.php +++ b/src/IO/OutputInterface.php @@ -1,4 +1,5 @@ parseCliArgv(); - if (isset($this->args[0])) { $this->command = $this->args[0]; unset($this->args[0]); } - $this->dispatch($exit); } @@ -71,12 +63,10 @@ public function run($exit = true) */ public function dispatch($exit = true) { - if (!$command = $this->command) { + if (!($command = $this->command)) { $this->showCommands(); } - $status = 0; - try { if (isset($this->commands[$command])) { $status = $this->runHandler($command, $this->commands[$command]); @@ -86,7 +76,6 @@ public function dispatch($exit = true) } catch (\Throwable $e) { $status = $this->handleException($e); } - if ($exit) { $this->stop($status); } @@ -113,23 +102,19 @@ public function runHandler($command, $handler) if (\function_exists($handler)) { return $handler($this); } - if (class_exists($handler)) { - $handler = new $handler; - + $handler = new $handler(); // $handler->execute() if (method_exists($handler, 'execute')) { return $handler->execute($this); } } } - // a \Closure OR $handler->__invoke() if (method_exists($handler, '__invoke')) { return $handler($this); } - - throw new \InvalidArgumentException("Invalid handler of the command: $command"); + throw new \InvalidArgumentException("Invalid handler of the command: {$command}"); } /** @@ -139,15 +124,7 @@ public function runHandler($command, $handler) protected function handleException(\Throwable $e) { $code = $e->getCode() !== 0 ? $e->getCode() : 133; - - printf( - "Exception(%d): %s\nFile: %s(Line %d)\nTrace:\n%s\n", - $code, - $e->getMessage(), - $e->getFile(), - $e->getLine(), - $e->getTraceAsString() - ); + printf("Exception(%d): %s\nFile: %s(Line %d)\nTrace:\n%s\n", $code, $e->getMessage(), $e->getFile(), $e->getLine(), $e->getTraceAsString()); return $code; } @@ -160,16 +137,13 @@ public function parseCliArgv() /** @var array $argv */ $argv = $_SERVER['argv']; $this->script = array_shift($argv); - foreach ($argv as $key => $value) { // opts if (strpos($value, '-') === 0) { $value = trim($value, '-'); - if (!$value) { continue; } - if (strpos($value, '=')) { list($n, $v) = explode('=', $value); $this->opts[$n] = $v; @@ -198,7 +172,6 @@ public function addCommand($command, $handler, $description = '') if (!$command || !$handler) { throw new \InvalidArgumentException('Invalid arguments'); } - $this->commands[$command] = $handler; $this->messages[$command] = trim($description); } @@ -210,39 +183,32 @@ public function commands(array $commands) { foreach ($commands as $command => $handler) { $des = ''; - if (\is_array($handler)) { $conf = array_values($handler); $handler = $conf[0]; - $des = $conf[1] ?? ''; + $des = isset($conf[1]) ? $conf[1] : ''; } - $this->addCommand($command, $handler, $des); } } - -/////////////////////////////////////////////////////////////////////////////////// -/// helper methods -/////////////////////////////////////////////////////////////////////////////////// - + /**************************************************************************** + * helper methods + ****************************************************************************/ /** * @param string $err */ public function showCommands($err = '') { if ($err) { - echo LiteStyle::color("ERROR: $err\n\n"); + echo LiteStyle::color("ERROR: {$err}\n\n"); } - $commandWidth = 12; $help = "Welcome to the Lite Console Application.\n\nAvailable Commands:\n"; - foreach ($this->messages as $command => $desc) { $command = str_pad($command, $commandWidth, ' '); $desc = $desc ?: 'No description for the command'; - $help .= " $command $desc\n"; + $help .= " {$command} {$desc}\n"; } - echo LiteStyle::color($help) . PHP_EOL; exit(0); } @@ -254,7 +220,7 @@ public function showCommands($err = '') */ public function getArg($name, $default = null) { - return $this->args[$name] ?? $default; + return isset($this->args[$name]) ? $this->args[$name] : $default; } /** @@ -264,17 +230,15 @@ public function getArg($name, $default = null) */ public function getOpt($name, $default = null) { - return $this->opts[$name] ?? $default; + return isset($this->opts[$name]) ? $this->opts[$name] : $default; } - -/////////////////////////////////////////////////////////////////////////////////// -/// getter/setter methods -/////////////////////////////////////////////////////////////////////////////////// - + /**************************************************************************** + * getter/setter methods + ****************************************************************************/ /** * @return array */ - public function getArgs(): array + public function getArgs() { return $this->args; } @@ -290,7 +254,7 @@ public function setArgs(array $args) /** * @return array */ - public function getOpts(): array + public function getOpts() { return $this->opts; } @@ -306,7 +270,7 @@ public function setOpts(array $opts) /** * @return string */ - public function getScript(): string + public function getScript() { return $this->script; } @@ -314,7 +278,7 @@ public function getScript(): string /** * @param string $script */ - public function setScript(string $script) + public function setScript($script) { $this->script = $script; } @@ -322,7 +286,7 @@ public function setScript(string $script) /** * @return string */ - public function getCommand(): string + public function getCommand() { return $this->command; } @@ -330,7 +294,7 @@ public function getCommand(): string /** * @param string $command */ - public function setCommand(string $command) + public function setCommand($command) { $this->command = $command; } @@ -338,7 +302,7 @@ public function setCommand(string $command) /** * @return array */ - public function getCommands(): array + public function getCommands() { return $this->commands; } @@ -354,7 +318,7 @@ public function setCommands(array $commands) /** * @return array */ - public function getMessages(): array + public function getMessages() { return $this->messages; } @@ -366,5 +330,4 @@ public function setMessages(array $messages) { $this->messages = $messages; } - -} +} \ No newline at end of file diff --git a/src/Style/Color.php b/src/Style/Color.php index dc360ed5..782cf2d2 100644 --- a/src/Style/Color.php +++ b/src/Style/Color.php @@ -1,4 +1,5 @@ 0, 'red' => 1, 'green' => 2, 'yellow' => 3, 'blue' => 4, - 'magenta' => 5, // 洋红色 洋红 品红色 - 'cyan' => 6, // 青色 青绿色 蓝绿色 + 'magenta' => 5, + // 洋红色 洋红 品红色 + 'cyan' => 6, + // 青色 青绿色 蓝绿色 'white' => 7, 'normal' => 9, ); - - /** - * Known style option - * @var array - */ + /** @var array Known style option */ private static $knownOptions = [ - 'bold' => 1, // 22 加粗 - 'fuzzy' => 2, // 模糊(不是所有的终端仿真器都支持) - 'italic' => 3, // 斜体(不是所有的终端仿真器都支持) - 'underscore' => 4, // 24 下划线 - 'blink' => 5, // 25 闪烁 - 'reverse' => 7, // 27 颠倒的 交换背景色与前景色 - 'concealed' => 8, // 28 隐匿的 + 'bold' => 1, + // 22 加粗 + 'fuzzy' => 2, + // 模糊(不是所有的终端仿真器都支持) + 'italic' => 3, + // 斜体(不是所有的终端仿真器都支持) + 'underscore' => 4, + // 24 下划线 + 'blink' => 5, + // 25 闪烁 + 'reverse' => 7, + // 27 颠倒的 交换背景色与前景色 + 'concealed' => 8, ]; - - /** - * Foreground color - */ + /** @var int Foreground color */ private $fgColor = 0; - - /** - * Background color - */ + /** @var int Background color */ private $bgColor = 0; - - /** - * Array of style options - */ + /** @var array Array of style options */ private $options = []; /** * @param string $fg * @param string $bg * @param array $options + * @param bool $extra * @return Color */ - public static function make($fg = '', $bg = '', array $options = []) + public static function make($fg = '', $bg = '', array $options = [], $extra = false) { - return new self($fg, $bg, $options); + return new self($fg, $bg, $options, $extra); } /** * Create a color style from a parameter string. - * - * @param string $string e.g 'fg=white;bg=black;options=bold,underscore' + * @param string $string e.g 'fg=white;bg=black;options=bold,underscore;extra=1' * @return static * @throws \RuntimeException */ public static function makeByString($string) { $fg = $bg = ''; + $extra = false; $options = []; - $parts = explode(';', $string); - + $parts = explode(';', str_replace(' ', '', $string)); foreach ($parts as $part) { $subParts = explode('=', $part); - if (\count($subParts) < 2) { continue; } - switch ($subParts[0]) { case 'fg': $fg = $subParts[1]; @@ -125,17 +121,19 @@ public static function makeByString($string) case 'bg': $bg = $subParts[1]; break; + case 'extra': + $extra = $subParts[1]; + break; case 'options': $options = explode(',', $subParts[1]); break; - default: throw new \RuntimeException('Invalid option'); break; } } - return new self($fg, $bg, $options); + return new self($fg, $bg, $options, $extra); } /** @@ -143,46 +141,26 @@ public static function makeByString($string) * @param string $fg Foreground color. e.g 'white' * @param string $bg Background color. e.g 'black' * @param array $options Style options. e.g ['bold', 'underscore'] - * @throws \InvalidArgumentException + * @param bool $extra */ - public function __construct($fg = '', $bg = '', array $options = []) + public function __construct($fg = '', $bg = '', array $options = [], $extra = false) { if ($fg) { if (false === array_key_exists($fg, static::$knownColors)) { - throw new \InvalidArgumentException( - sprintf('Invalid foreground color "%1$s" [%2$s]', - $fg, - implode(', ', $this->getKnownColors()) - ) - ); + throw new \InvalidArgumentException(sprintf('Invalid foreground color "%1$s" [%2$s]', $fg, implode(', ', $this->getKnownColors()))); } - - $this->fgColor = self::FG_BASE + static::$knownColors[$fg]; + $this->fgColor = ($extra ? self::FG_EXTRA : self::FG_BASE) + static::$knownColors[$fg]; } - if ($bg) { if (false === array_key_exists($bg, static::$knownColors)) { - throw new \InvalidArgumentException( - sprintf('Invalid background color "%1$s" [%2$s]', - $bg, - implode(', ', $this->getKnownColors()) - ) - ); + throw new \InvalidArgumentException(sprintf('Invalid background color "%1$s" [%2$s]', $bg, implode(', ', $this->getKnownColors()))); } - - $this->bgColor = self::BG_BASE + static::$knownColors[$bg]; + $this->bgColor = ($extra ? self::BG_EXTRA : self::BG_BASE) + static::$knownColors[$bg]; } - foreach ($options as $option) { if (false === array_key_exists($option, static::$knownOptions)) { - throw new \InvalidArgumentException( - sprintf('Invalid option "%1$s" [%2$s]', - $option, - implode(', ', $this->getKnownOptions()) - ) - ); + throw new \InvalidArgumentException(sprintf('Invalid option "%1$s" [%2$s]', $option, implode(', ', $this->getKnownOptions()))); } - $this->options[] = $option; } } @@ -201,15 +179,12 @@ public function __toString() public function toStyle() { $values = []; - if ($this->fgColor) { $values[] = $this->fgColor; } - if ($this->bgColor) { $values[] = $this->bgColor; } - foreach ($this->options as $option) { $values[] = static::$knownOptions[$option]; } @@ -236,5 +211,4 @@ public function getKnownOptions($onlyName = true) { return (bool)$onlyName ? array_keys(static::$knownOptions) : static::$knownOptions; } - -} +} \ No newline at end of file diff --git a/src/Style/Highlighter.php b/src/Style/Highlighter.php index ec3df5de..c190eb71 100644 --- a/src/Style/Highlighter.php +++ b/src/Style/Highlighter.php @@ -1,4 +1,5 @@ 'red', self::TOKEN_COMMENT => 'yellow', self::TOKEN_KEYWORD => 'info', self::TOKEN_DEFAULT => 'normal', self::TOKEN_HTML => 'cyan', self::ACTUAL_LINE_MARK => 'red', self::LINE_NUMBER => 'darkGray']; + + /** + * @param Style $color + */ + public function __construct(Style $color = null) + { + $this->color = $color ?: Style::create(); + } + + /** + * @param string $source + * @param bool $withLn with line number + * @return string + */ + public function highlight($source, $withLn = false) + { + $tokenLines = $this->getHighlightedLines($source); + $lines = $this->colorLines($tokenLines); + if ($withLn) { + return $this->lineNumbers($lines); + } + + return implode(PHP_EOL, $lines); + } + + /** + * @param string $source + * @param int $lineNumber + * @param int $linesBefore + * @param int $linesAfter + * @return string + * @throws \InvalidArgumentException + */ + public function getCodeSnippet($source, $lineNumber, $linesBefore = 2, $linesAfter = 2) + { + $tokenLines = $this->getHighlightedLines($source); + $offset = $lineNumber - $linesBefore - 1; + $offset = max($offset, 0); + $length = $linesAfter + $linesBefore + 1; + $tokenLines = \array_slice($tokenLines, $offset, $length, $preserveKeys = true); + $lines = $this->colorLines($tokenLines); + + return $this->lineNumbers($lines, $lineNumber); + } + + /** + * @param string $source + * @return string + * @throws \InvalidArgumentException + */ + public function getWholeFile($source) + { + $tokenLines = $this->getHighlightedLines($source); + $lines = $this->colorLines($tokenLines); + + return implode(PHP_EOL, $lines); + } + + /** + * @param string $source + * @return string + * @throws \InvalidArgumentException + */ + public function getWholeFileWithLineNumbers($source) + { + $tokenLines = $this->getHighlightedLines($source); + $lines = $this->colorLines($tokenLines); + + return $this->lineNumbers($lines); + } + + /** + * @param string $source + * @return array + */ + private function getHighlightedLines($source) + { + $source = str_replace(array("\r\n", "\r"), "\n", $source); + $tokens = $this->tokenize($source); + + return $this->splitToLines($tokens); + } + + /** + * @param string $source + * @return array + */ + private function tokenize($source) + { + $buffer = ''; + $output = []; + $tokens = token_get_all($source); + $newType = $currentType = null; + foreach ($tokens as $token) { + if (\is_array($token)) { + switch ($token[0]) { + case T_INLINE_HTML: + $newType = self::TOKEN_HTML; + break; + case T_COMMENT: + case T_DOC_COMMENT: + $newType = self::TOKEN_COMMENT; + break; + case T_ENCAPSED_AND_WHITESPACE: + case T_CONSTANT_ENCAPSED_STRING: + $newType = self::TOKEN_STRING; + break; + case T_WHITESPACE: + break; + case T_OPEN_TAG: + case T_OPEN_TAG_WITH_ECHO: + case T_CLOSE_TAG: + case T_STRING: + case T_VARIABLE: + // Constants + // Constants + case T_DIR: + case T_FILE: + case T_METHOD_C: + case T_DNUMBER: + case T_LNUMBER: + case T_NS_C: + case T_LINE: + case T_CLASS_C: + case T_FUNC_C: + //case T_TRAIT_C: + $newType = self::TOKEN_DEFAULT; + break; + default: + // Compatibility with PHP 5.3 + if (\defined('T_TRAIT_C') && $token[0] === T_TRAIT_C) { + $newType = self::TOKEN_DEFAULT; + } else { + $newType = self::TOKEN_KEYWORD; + } + } + } else { + $newType = $token === '"' ? self::TOKEN_STRING : self::TOKEN_KEYWORD; + } + if ($currentType === null) { + $currentType = $newType; + } + if ($currentType != $newType) { + $output[] = [$currentType, $buffer]; + $buffer = ''; + $currentType = $newType; + } + $buffer .= \is_array($token) ? $token[1] : $token; + } + if (null !== $newType) { + $output[] = [$newType, $buffer]; + } + + return $output; + } + + /** + * @param array $tokens + * @return array + */ + private function splitToLines(array $tokens) + { + $lines = $line = []; + foreach ($tokens as $token) { + foreach (explode("\n", $token[1]) as $count => $tokenLine) { + if ($count> 0) { + $lines[] = $line; + $line = []; + } + if ($tokenLine === '') { + continue; + } + $line[] = [$token[0], $tokenLine]; + } + } + $lines[] = $line; + + return $lines; + } + + /** + * @param array[] $tokenLines + * @return array + * @throws \InvalidArgumentException + */ + private function colorLines(array $tokenLines) + { + $lines = []; + foreach ($tokenLines as $lineCount => $tokenLine) { + $line = ''; + // foreach ($tokenLine as $token) { + foreach ($tokenLine as list($tokenType, $tokenValue)) { + $style = $this->defaultTheme[$tokenType]; + if ($this->color->hasStyle($style)) { + $line .= $this->color->apply($style, $tokenValue); + } else { + $line .= $tokenValue; + } + } + $lines[$lineCount] = $line; + } + + return $lines; + } + + /** + * @param array $lines + * @param null|int $markLine + * @return string + */ + private function lineNumbers(array $lines, $markLine = null) + { + end($lines); + $lineStrlen = \strlen(key($lines) + 1); + $snippet = ''; + $lmStyle = $this->defaultTheme[self::ACTUAL_LINE_MARK]; + $lnStyle = $this->defaultTheme[self::LINE_NUMBER]; + foreach ($lines as $i => $line) { + if ($markLine !== null) { + $snippet .= $markLine === $i + 1 ? $this->color->apply($lmStyle, '> ') : ' '; + } + $snippet .= $this->color->apply($lnStyle, str_pad($i + 1, $lineStrlen, ' ', STR_PAD_LEFT) . '| '); + $snippet .= $line . PHP_EOL; + } + + return $snippet; + } + + /** + * @return array + */ + public function getDefaultTheme() + { + return $this->defaultTheme; + } -} + /** + * @param array $defaultTheme + */ + public function setDefaultTheme(array $defaultTheme) + { + $this->defaultTheme = array_merge($this->defaultTheme, $defaultTheme); + } +} \ No newline at end of file diff --git a/src/Style/LiteStyle.php b/src/Style/LiteStyle.php index 30e94239..10045436 100644 --- a/src/Style/LiteStyle.php +++ b/src/Style/LiteStyle.php @@ -1,4 +1,5 @@ (.*?)<\/\1円>/s'; - + const COLOR_TAG = '/<([a-z=;]+)>(.*?)<\\/\1円>/s'; /** * some styles * @var array */ const STYLES = [ - 'light_red' => '1;31', - 'light_green' => '1;32', 'yellow' => '1;33', - 'light_blue' => '1;34', 'magenta' => '1;35', - 'light_cyan' => '1;36', 'white' => '1;37', 'black' => '0;30', 'red' => '0;31', @@ -91,23 +88,38 @@ class LiteStyle 'brown' => '0;33', 'blue' => '0;34', 'cyan' => '0;36', - + 'light_red' => '1;31', + 'light_blue' => '1;34', + 'light_gray' => '37', + 'light_green' => '1;32', + 'light_cyan' => '1;36', + 'dark_gray' => '90', + 'light_red_ex' => '91', + 'light_green_ex' => '92', + 'light_yellow' => '93', + 'light_blue_ex' => '94', + 'light_magenta' => '95', + 'light_cyan_ex' => '96', + 'white_ex' => '97', 'bold' => '1', 'underscore' => '4', 'reverse' => '7', - // - 'suc' => '1;32',// same 'green' and 'bold' + 'suc' => '1;32', + // same 'green' and 'bold' 'success' => '1;32', - 'info' => '0;32',// same 'green' - 'comment' => '0;33',// same 'brown' + 'info' => '0;32', + // same 'green' + 'comment' => '0;33', + // same 'brown' 'warning' => '0;30;43', - 'danger' => '0;31',// same 'red' + 'danger' => '0;31', + // same 'red' 'error' => '30;41', ]; /** - * @param $text + * @param string $text * @param string|int|array $style * @return string */ @@ -116,16 +128,13 @@ public static function color($text, $style = null) if (!$text) { return $text; } - - if (!Helper::isSupportColor()) { + if (!Helper::supportColor()) { return self::clearColor($text); } - if (\is_string($style)) { - $color = self::STYLES[$style] ?? '0'; + $color = isset(self::STYLES[$style]) ? self::STYLES[$style] : '0'; } elseif (\is_int($style)) { $color = $style; - // array: [self::FG_GREEN, self::BG_WHITE, self::UNDERSCORE] } elseif (\is_array($style)) { $color = implode(';', $style); @@ -135,13 +144,13 @@ public static function color($text, $style = null) return $text; } -// $result = chr(27). "$color{$text}" . chr(27) . chr(27) . "[0m". chr(27); - return "033円[{$color}m{$text}033円[0m"; + // $result = chr(27). "$color{$text}" . chr(27) . chr(27) . "[0m". chr(27); + return "33円[{$color}m{$text}33円[0m"; } /** * render color tag to color style - * @param $text + * @param string $text * @return mixed|string */ public static function renderColor($text) @@ -149,23 +158,19 @@ public static function renderColor($text) if (!$text || false === strpos($text, '<')) { return $text; } - // if don't support output color text, clear color tag. - if (!Helper::isSupportColor()) { + if (!Helper::supportColor()) { return static::clearColor($text); } - if (!preg_match_all(self::COLOR_TAG, $text, $matches)) { return $text; } - foreach ((array)$matches[0] as $i => $m) { - if ($style = self::STYLES[$matches[1][$i]] ?? null) { + if ($style = isset(self::STYLES[$matches[1][$i]]) ? self::STYLES[$matches[1][$i]] : null) { $tag = $matches[1][$i]; $match = $matches[2][$i]; - - $replace = sprintf("033円[%sm%s033円[0m", $style, $match); - $text = str_replace("<$tag>$match", $replace, $text); + $replace = sprintf("33円[%sm%s33円[0m", $style, $match); + $text = str_replace("<{$tag}>{$match}", $replace, $text); } } @@ -179,6 +184,6 @@ public static function renderColor($text) public static function clearColor($text) { // return preg_replace('/033円\[(?:\d;?)+m/', '' , "033円[0;36mtext033円[0m"); - return preg_replace('/033円\[(?:\d;?)+m/', '', strip_tags($text)); + return preg_replace('/\033円\\[(?:\\d;?)+m/', '', strip_tags($text)); } -} +} \ No newline at end of file diff --git a/src/Style/Style.php b/src/Style/Style.php index 4ff01e02..4355f52f 100644 --- a/src/Style/Style.php +++ b/src/Style/Style.php @@ -1,4 +1,5 @@ (.*?)<\/\1円>/s'; - + const COLOR_TAG = '/<([a-za-z=;]+)>(.*?)<\\/\1円>/s'; /** * Regex used for removing color codes */ - const STRIP_TAG = '/<[\/]?[a-z=;]+>/'; - + const STRIP_TAG = '/<[\\/]?[a-za-z=;]+>/'; /** * @var self */ private static $instance; - /** * Flag to remove color codes from the output * @var bool */ protected static $noColor = false; - /** * Array of Style objects * @var array @@ -67,7 +69,7 @@ class Style /** * @return Style */ - public static function create(): Style + public static function create() { if (!self::$instance) { self::$instance = new self(); @@ -85,43 +87,43 @@ public static function create(): Style public function __construct($fg = '', $bg = '', array $options = []) { if ($fg || $bg || $options) { - $this->add('base', [ - 'fg' => $fg, - 'bg' => $bg, - 'options' => $options - ]); + $this->add('base', ['fg' => $fg, 'bg' => $bg, 'options' => $options]); } - $this->loadDefaultStyles(); } + /** + * @param string $method + * @param array $args + * @return mixed|string + * @throws \InvalidArgumentException + */ + public function __call($method, array $args) + { + if (isset($args[0]) && $this->hasStyle($method)) { + return $this->format(sprintf('<%s>%s', $method, $args[0], $method)); + } + throw new \InvalidArgumentException("You called method is not exists: {$method}"); + } + /** * Adds predefined color styles to the Color styles * default primary success info warning danger */ protected function loadDefaultStyles() { - $this - ->add(self::NORMAL, ['fg' => 'normal']) - // 不明显的 浅灰色的 - ->add(self::FAINTLY, ['fg' => 'normal', 'options' => ['italic']]) - ->add(self::BOLD, ['options' => ['bold']]) - ->add(self::INFO, ['fg' => 'green',])//'options' => ['bold'] - ->add(self::NOTE, ['fg' => 'green', 'options' => ['bold']])//'options' => ['bold'] - ->add(self::PRIMARY, ['fg' => 'blue',])//'options' => ['bold'] - ->add(self::SUCCESS, ['fg' => 'green', 'options' => ['bold']]) - ->add(self::NOTICE, ['options' => ['bold', 'underscore'],]) - ->add(self::WARNING, ['fg' => 'black', 'bg' => 'yellow',])//'options' => ['bold'] - ->add(self::COMMENT, ['fg' => 'yellow',])//'options' => ['bold'] - ->add(self::QUESTION, ['fg' => 'black', 'bg' => 'cyan']) - ->add(self::DANGER, ['fg' => 'red',])// 'bg' => 'magenta', 'options' => ['bold'] - ->add(self::ERROR, ['fg' => 'black', 'bg' => 'red']) - ->add('underline', ['fg' => 'normal', 'options' => ['underscore']]) - ->add('blue', ['fg' => 'blue']) - ->add('cyan', ['fg' => 'cyan']) - ->add('magenta', ['fg' => 'magenta']) - ->add('red', ['fg' => 'red']) - ->add('yellow', ['fg' => 'yellow']); + $this->add(self::NORMAL, ['fg' => 'normal'])->add(self::FAINTLY, ['fg' => 'normal', 'options' => ['italic']])->add(self::BOLD, ['options' => ['bold']])->add(self::INFO, ['fg' => 'green'])->add(self::NOTE, ['fg' => 'cyan', 'options' => ['bold']])->add(self::PRIMARY, ['fg' => 'yellow', 'options' => ['bold']])->add(self::SUCCESS, ['fg' => 'green', 'options' => ['bold']])->add(self::NOTICE, ['options' => ['bold', 'underscore']])->add(self::WARNING, ['fg' => 'black', 'bg' => 'yellow'])->add(self::COMMENT, ['fg' => 'yellow'])->add(self::QUESTION, ['fg' => 'black', 'bg' => 'cyan'])->add(self::DANGER, ['fg' => 'red'])->add(self::ERROR, ['fg' => 'black', 'bg' => 'red'])->add('underline', ['fg' => 'normal', 'options' => ['underscore']])->add('blue', ['fg' => 'blue'])->add('cyan', ['fg' => 'cyan'])->add('magenta', ['fg' => 'magenta'])->add('red', ['fg' => 'red'])->add('darkGray', ['fg' => 'black', 'extra' => true])->add('yellow', ['fg' => 'yellow']); + } + + /** + * Process a string use style + * @param string $style + * @param $text + * @return string + */ + public function apply($style, $text) + { + return $this->format(Helper::wrapTag($text, $style)); } /** @@ -143,20 +145,16 @@ public function format($text) if (!$text || false === strpos($text, '<')) { return $text; } - // if don't support output color text, clear color tag. if (!Helper::isSupportColor() || self::isNoColor()) { return static::stripColor($text); } - if (!preg_match_all(self::COLOR_TAG, $text, $matches)) { return $text; } - foreach ((array)$matches[0] as $i => $m) { if (array_key_exists($matches[1][$i], $this->styles)) { $text = $this->replaceColor($text, $matches[1][$i], $matches[2][$i], (string)$this->styles[$matches[1][$i]]); - // Custom style format @see Style::makeByString() } elseif (strpos($matches[1][$i], '=')) { $text = $this->replaceColor($text, $matches[1][$i], $matches[2][$i], (string)Color::makeByString($matches[1][$i])); @@ -174,11 +172,11 @@ public function format($text) * @param string $style The color style to apply. * @return string */ - protected function replaceColor($text, $tag, $match, $style): string + protected function replaceColor($text, $tag, $match, $style) { - $replace = self::$noColor ? $match : sprintf("033円[%sm%s033円[0m", $style, $match); + $replace = self::$noColor ? $match : sprintf("33円[%sm%s33円[0m", $style, $match); - return str_replace("<$tag>$match", $replace, $text); + return str_replace("<{$tag}>{$match}", $replace, $text); // return sprintf("033円[%sm%s033円[%sm", implode(';', $setCodes), $text, implode(';', $unsetCodes)); } @@ -192,28 +190,28 @@ public static function stripColor($string) // $text = strip_tags($text); return preg_replace(self::STRIP_TAG, '', $string); } - -///////////////////////////////////////// Attr Color Style ///////////////////////////////////////// - + /**************************************************************************** + * Attr Color Style + ****************************************************************************/ /** * Add a style. - * @param string $name - * @param string|Color|array $fg 前景色|也可以穿入Color对象|也可以是style配置数组(@see self::addByArray()) - * 当它为Color对象或配置数组时,后面两个参数无效 - * @param string $bg 背景色 - * @param array $options 其它选项 + * @param string $name + * @param string|Color|array $fg 前景色|Color对象|也可以是style配置数组(@see self::addByArray()) + * 当它为Color对象或配置数组时,后面两个参数无效 + * @param string $bg 背景色 + * @param array $options 其它选项 + * @param bool $extra * @return $this */ - public function add($name, $fg = '', $bg = '', array $options = []) + public function add($name, $fg = '', $bg = '', array $options = [], $extra = false) { if (\is_array($fg)) { return $this->addByArray($name, $fg); } - if (\is_object($fg) && $fg instanceof Color) { $this->styles[$name] = $fg; } else { - $this->styles[$name] = Color::make($fg, $bg, $options); + $this->styles[$name] = Color::make($fg, $bg, $options, $extra); } return $this; @@ -221,27 +219,23 @@ public function add($name, $fg = '', $bg = '', array $options = []) /** * Add a style by an array config - * @param $name + * @param string $name * @param array $styleConfig 样式设置信息 - * e.g [ - * 'fg' => 'white', - * 'bg' => 'black', - * 'options' => ['bold', 'underscore'] - * ] + * e.g + * [ + * 'fg' => 'white', + * 'bg' => 'black', + * 'extra' => true, + * 'options' => ['bold', 'underscore'] + * ] * @return $this */ public function addByArray($name, array $styleConfig) { - $style = [ - 'fg' => '', - 'bg' => '', - 'options' => [] - ]; - + $style = ['fg' => '', 'bg' => '', 'extra' => false, 'options' => []]; $config = array_merge($style, $styleConfig); - list($fg, $bg, $options) = array_values($config); - - $this->styles[$name] = Color::make($fg, $bg, $options); + list($fg, $bg, $extra, $options) = array_values($config); + $this->styles[$name] = Color::make($fg, $bg, $options, $extra); return $this; } @@ -249,7 +243,7 @@ public function addByArray($name, array $styleConfig) /** * @return array */ - public function getStyleNames(): array + public function getStyleNames() { return array_keys($this->styles); } @@ -257,7 +251,7 @@ public function getStyleNames(): array /** * @return array */ - public function getNames(): array + public function getNames() { return array_keys($this->styles); } @@ -265,7 +259,7 @@ public function getNames(): array /** * @return array */ - public function getStyles(): array + public function getStyles() { return $this->styles; } @@ -287,7 +281,7 @@ public function getStyle($name) * @param $name * @return bool */ - public function hasStyle($name): bool + public function hasStyle($name) { return isset($this->styles[$name]); } @@ -295,7 +289,7 @@ public function hasStyle($name): bool /** * Method to get property NoColor */ - public static function isNoColor(): bool + public static function isNoColor() { return (bool)self::$noColor; } @@ -308,4 +302,4 @@ public static function setNoColor($noColor = true) { self::$noColor = (bool)$noColor; } -} +} \ No newline at end of file diff --git a/src/Traits/AdvancedFormatOutputTrait.php b/src/Traits/AdvancedFormatOutputTrait.php new file mode 100644 index 00000000..f02b656f --- /dev/null +++ b/src/Traits/AdvancedFormatOutputTrait.php @@ -0,0 +1,18 @@ + true, - 'stream' => $this->outputStream, - ]); + return Show::write($messages, $nl, $quit, ['flush' => true, 'stream' => $this->outputStream]); } /** @@ -57,6 +58,15 @@ public function writeln($text, $quit = false, array $opts = []) return Show::writeln($text, $quit, $opts); } + /** + * @inheritdoc + * @see Show::writeRaw() + */ + public function writeRaw($text, $nl = true, $quit = false, array $opts = []) + { + return Show::writeRaw($text, $nl, $quit, $opts); + } + /** * @inheritdoc * @see Show::block() @@ -175,25 +185,22 @@ public function progressBar($total, array $opts = []) public function __call($method, array $args = []) { $map = Show::getBlockMethods(false); - if (isset($map[$method])) { $msg = $args[0]; - $quit = $args[1] ?? false; + $quit = isset($args[1]) ? $args[1] : false; $style = $map[$method]; - if (0 === strpos($method, 'lite')) { $type = substr($method, 4); + return Show::liteBlock($msg, $type === 'Primary' ? 'IMPORTANT' : $type, $style, $quit); } return Show::block($msg, $style === 'primary' ? 'IMPORTANT' : $style, $style, $quit); } - if (method_exists(Show::class, $method)) { return Show::$method(...$args); } - - throw new \LogicException("Call a not exists method: $method of the " . static::class); + throw new \LogicException("Call a not exists method: {$method} of the " . static::class); } /** @@ -204,7 +211,6 @@ public function __call($method, array $args = []) public function json($data, $echo = true) { $string = json_encode($data, JSON_PRETTY_PRINT); - if ($echo) { return Show::write($string); } @@ -223,8 +229,8 @@ public function dump(...$vars) /** * @param array ...$vars */ - public function print(...$vars) + public function prints(...$vars) { Show::write(Helper::printVars(...$vars)); } -} +} \ No newline at end of file diff --git a/src/Traits/InputOutputTrait.php b/src/Traits/InputOutputAwareTrait.php similarity index 88% rename from src/Traits/InputOutputTrait.php rename to src/Traits/InputOutputAwareTrait.php index 57e77f30..f688d907 100644 --- a/src/Traits/InputOutputTrait.php +++ b/src/Traits/InputOutputAwareTrait.php @@ -1,4 +1,5 @@ input->getScript(); } @@ -45,8 +45,9 @@ public function getArg($name, $default = null) } /** - * @see Input::getRequiredArg() * {@inheritdoc} + * @see Input::getRequiredArg() + * @throws \InvalidArgumentException */ public function getRequiredArg($name) { @@ -54,8 +55,8 @@ public function getRequiredArg($name) } /** - * @see Input::getSameArg() * {@inheritdoc} + * @see Input::getSameArg() */ public function getSameArg(array $names, $default = null) { @@ -63,8 +64,8 @@ public function getSameArg(array $names, $default = null) } /** - * @see Input::getOpt() * {@inheritdoc} + * @see Input::getOpt() */ public function getOpt($name, $default = null) { @@ -72,8 +73,8 @@ public function getOpt($name, $default = null) } /** - * @see Input::getSameOpt() * {@inheritdoc} + * @see Input::getSameOpt() */ public function getSameOpt(array $names, $default = null) { @@ -81,8 +82,9 @@ public function getSameOpt(array $names, $default = null) } /** - * @see Input::getRequiredOpt() * {@inheritdoc} + * @see Input::getRequiredOpt() + * @throws \InvalidArgumentException */ public function getRequiredOpt($name) { @@ -93,7 +95,7 @@ public function getRequiredOpt($name) * @param string $msg * @return string */ - protected function read($msg = ''): string + protected function read($msg = '') { return $this->input->read($msg); } @@ -104,7 +106,7 @@ protected function read($msg = ''): string * @param bool|int $quit * @return int */ - protected function write($message, $nl = true, $quit = false): int + protected function write($message, $nl = true, $quit = false) { return $this->output->write($message, $nl, $quit); } @@ -114,7 +116,7 @@ protected function write($message, $nl = true, $quit = false): int * @param bool|int $quit * @return int */ - protected function writeln($message, $quit = false): int + protected function writeln($message, $quit = false) { return $this->output->write($message, true, $quit); } @@ -122,7 +124,7 @@ protected function writeln($message, $quit = false): int /** * @return Input */ - public function getInput(): Input + public function getInput() { return $this->input; } @@ -138,7 +140,7 @@ public function setInput(Input $input) /** * @return Output */ - public function getOutput(): Output + public function getOutput() { return $this->output; } @@ -150,4 +152,4 @@ public function setOutput(Output $output) { $this->output = $output; } -} +} \ No newline at end of file diff --git a/src/Traits/SimpleEventTrait.php b/src/Traits/SimpleEventTrait.php index 82fb4633..9ed45ac2 100644 --- a/src/Traits/SimpleEventTrait.php +++ b/src/Traits/SimpleEventTrait.php @@ -1,4 +1,5 @@ fonts[$name] = $content; - } - } - - /** - * @param string $path - */ - public function addPath(string $path) - { - if (file_exists($path)) { - $this->artPaths[] = $path; - } - } - - /** - * @return array - */ - public function getArtPaths(): array - { - return $this->artPaths; - } - - /** - * @param array $artPaths - */ - public function setArtPaths(array $artPaths) - { - foreach ($artPaths as $path) { - $this->addPath($path); - } - } - - /** - * @return array - */ - public function getFonts(): array - { - return $this->fonts; - } - - /** - * @param array $fonts - */ - public function setFonts(array $fonts) - { - foreach ($fonts as $name => $font) { - $this->addFont($name, $font); - } - } -} \ No newline at end of file diff --git a/src/Utils/CliUtil.php b/src/Utils/CliUtil.php new file mode 100644 index 00000000..4d806fb9 --- /dev/null +++ b/src/Utils/CliUtil.php @@ -0,0 +1,208 @@ +> \"{$logfile}\" 2>&1", $dummy, $retVal); + if ($retVal !== 0) { + throw new \RuntimeException("command exited with status '{$retVal}'."); + } + + return $dummy; + } + + /** + * Method to execute a command in the sys + * Uses : + * 1. system + * 2. passthru + * 3. exec + * 4. shell_exec + * @param $command + * @param bool $returnStatus + * @return array|string + */ + public static function runCommand($command, $returnStatus = true) + { + $return_var = 1; + //system + if (\function_exists('system')) { + ob_start(); + system($command, $return_var); + $output = ob_get_contents(); + ob_end_clean(); + // passthru + } elseif (\function_exists('passthru')) { + ob_start(); + passthru($command, $return_var); + $output = ob_get_contents(); + ob_end_clean(); + //exec + } else { + if (\function_exists('exec')) { + exec($command, $output, $return_var); + $output = implode("\n", $output); + //shell_exec + } else { + if (\function_exists('shell_exec')) { + $output = shell_exec($command); + } else { + $output = 'Command execution not possible on this system'; + $return_var = 0; + } + } + } + if ($returnStatus) { + return ['output' => trim($output), 'status' => $return_var]; + } + + return trim($output); + } + + /** + * @return string + */ + public static function getTempDir() + { + // @codeCoverageIgnoreStart + if (\function_exists('sys_get_temp_dir')) { + $tmp = sys_get_temp_dir(); + } elseif (!empty($_SERVER['TMP'])) { + $tmp = $_SERVER['TMP']; + } elseif (!empty($_SERVER['TEMP'])) { + $tmp = $_SERVER['TEMP']; + } elseif (!empty($_SERVER['TMPDIR'])) { + $tmp = $_SERVER['TMPDIR']; + } else { + $tmp = getcwd(); + } + + // @codeCoverageIgnoreEnd + return $tmp; + } + + /** + * get screen size + * ```php + * list($width, $height) = Helper::getScreenSize(); + * ``` + * @from Yii2 + * @param boolean $refresh whether to force checking and not re-use cached size value. + * This is useful to detect changing window size while the application is running but may + * not get up to date values on every terminal. + * @return array|boolean An array of ($width, $height) or false when it was not able to determine size. + */ + public static function getScreenSize($refresh = false) + { + static $size; + if ($size !== null && !$refresh) { + return $size; + } + if (self::bashIsAvailable()) { + // try stty if available + $stty = []; + if (exec('stty -a 2>&1', $stty) && preg_match('/rows\\s+(\\d+);\\s*columns\\s+(\\d+);/mi', implode(' ', $stty), $matches)) { + return $size = [$matches[2], $matches[1]]; + } + // fallback to tput, which may not be updated on terminal resize + if (($width = (int)exec('tput cols 2>&1'))> 0 && ($height = (int)exec('tput lines 2>&1'))> 0) { + return $size = [$width, $height]; + } + // fallback to ENV variables, which may not be updated on terminal resize + if (($width = (int)getenv('COLUMNS'))> 0 && ($height = (int)getenv('LINES'))> 0) { + return $size = [$width, $height]; + } + } + if (Helper::isOnWindows()) { + $output = []; + exec('mode con', $output); + if (isset($output[1]) && strpos($output[1], 'CON') !== false) { + return $size = [(int)preg_replace('~\\D~', '', $output[3]), (int)preg_replace('~\\D~', '', $output[4])]; + } + } + + return $size = false; + } + + /** + * @param string $program + * @return int|string + */ + public static function getCpuUsage($program) + { + if (!$program) { + return -1; + } + $info = exec('ps aux | grep ' . $program . ' | grep -v grep | grep -v su | awk {"print 3ドル"}'); + + return $info; + } + + /** + * @param $program + * @return int|string + */ + public static function getMemUsage($program) + { + if (!$program) { + return -1; + } + $info = exec('ps aux | grep ' . $program . ' | grep -v grep | grep -v su | awk {"print 4ドル"}'); + + return $info; + } +} \ No newline at end of file diff --git a/src/Utils/ArgumentOptionParse.php b/src/Utils/CommandLine.php similarity index 84% rename from src/Utils/ArgumentOptionParse.php rename to src/Utils/CommandLine.php index fbcdec20..c44c9dcf 100644 --- a/src/Utils/ArgumentOptionParse.php +++ b/src/Utils/CommandLine.php @@ -1,4 +1,5 @@ * arg= - * * Supports opts: * -e * -e @@ -44,67 +40,55 @@ final class ArgumentOptionParse * --long-opt * --long-opt * --long-opt= - * * @link http://php.net/manual/zh/function.getopt.php#83414 * @param array $params * @param array $noValues List of parameters without values(bool option keys) * @param bool $mergeOpts Whether merge short-opts and long-opts * @return array */ - public static function byArgv(array $params, array $noValues = [], $mergeOpts = false): array + public static function parseByArgv(array $params, array $noValues = [], $mergeOpts = false) { $args = $sOpts = $lOpts = []; - // each() will deprecated at 7.2. so,there use current and next instead it. // while (list(,$p) = each($params)) { while (false !== ($p = current($params))) { next($params); - // is options - if ($p{0} === '-') { + if ($p[0] === '-') { $isLong = false; $opt = substr($p, 1); $value = true; - // long-opt: (--) - if ($opt{0} === '-') { + if ($opt[0] === '-') { $isLong = true; $opt = substr($opt, 1); - // long-opt: value specified inline (--=) if (strpos($opt, '=') !== false) { list($opt, $value) = explode('=', $opt, 2); } - // short-opt: value specified inline (-=) - } elseif (\strlen($opt)> 2 && $opt{1} === '=') { + } elseif (\strlen($opt)> 2 && $opt[1] === '=') { list($opt, $value) = explode('=', $opt, 2); } - // check if next parameter is a descriptor or a value $nxp = current($params); - // fix: allow empty string '' - if ($value === true && $nxp !== false && (!$nxp || $nxp{0} !== '-') && !\in_array($opt, $noValues, true)) { + if ($value === true && $nxp !== false && (!$nxp || $nxp[0] !== '-') && !\in_array($opt, $noValues, true)) { // list(,$value) = each($params); $value = current($params); next($params); - // short-opt: bool opts. like -e -abc } elseif (!$isLong && $value === true) { foreach (str_split($opt) as $char) { $sOpts[$char] = true; } - continue; } - if ($isLong) { $lOpts[$opt] = self::filterBool($value); } else { $sOpts[$opt] = self::filterBool($value); } - // arguments: param doesn't belong to any option, define it is args } else { // value specified inline (=) @@ -116,7 +100,6 @@ public static function byArgv(array $params, array $noValues = [], $mergeOpts = } } } - if ($mergeOpts) { return [$args, array_merge($sOpts, $lOpts)]; } @@ -124,29 +107,29 @@ public static function byArgv(array $params, array $noValues = [], $mergeOpts = return [$args, $sOpts, $lOpts]; } + public static function parseByDefinition(array $tokens, array $allowArray = [], array $noValues = []) + { + } + /** * parse custom array params - * * ```php - * $result = CommandLineParse::byArray([ + * $result = CommandLine::parseByArray([ * 'arg' => 'val', * '--lp' => 'val2', * '--s' => 'val3', * ]); * ``` - * * @param array $params * @return array */ - public static function byArray(array $params) + public static function parseByArray(array $params) { $args = $sOpts = $lOpts = []; - foreach ($params as $key => $value) { if ($key === '--' || $key === '-') { continue; } - if (0 === strpos($key, '--')) { $lOpts[substr($key, 2)] = $value; } elseif ('-' === $key[0]) { @@ -160,16 +143,14 @@ public static function byArray(array $params) } /** - * * ```php - * $result = CommandLineParse::byString('foo --bar="foobar"'); + * $result = CommandLine::parseByString('foo --bar="foobar"'); * ``` * @todo ... * @param string $string */ - public static function byString(string $string) + public static function parseByString($string) { - } /** @@ -183,15 +164,12 @@ public static function filterBool($val, $enable = true) if (\is_bool($val) || is_numeric($val)) { return $val; } - $tVal = strtolower($val); - // check it is a bool value. - if (false !== strpos(self::TRUE_WORDS, "|$tVal|")) { + if (false !== strpos(self::TRUE_WORDS, "|{$tVal}|")) { return true; } - - if (false !== strpos(self::FALSE_WORDS, "|$tVal|")) { + if (false !== strpos(self::FALSE_WORDS, "|{$tVal}|")) { return false; } } @@ -201,12 +179,11 @@ public static function filterBool($val, $enable = true) /** * Escapes a token through escapeshellarg if it contains unsafe chars. - * * @param string $token * @return string */ public static function escapeToken($token) { - return preg_match('{^[\w-]+$}', $token) ? $token : escapeshellarg($token); + return preg_match('{^[\\w-]+$}', $token) ? $token : escapeshellarg($token); } -} +} \ No newline at end of file diff --git a/src/Utils/FormatUtil.php b/src/Utils/FormatUtil.php new file mode 100644 index 00000000..5ef3a63c --- /dev/null +++ b/src/Utils/FormatUtil.php @@ -0,0 +1,332 @@ + $line) { + if ($first) { + $first = false; + continue; + } + $lines[$i] = $pad . $line; + } + + return $pad . ' ' . implode("\n", $lines); + } + + /** + * @param string $optsStr + */ + public static function annotationOptions($optsStr) + { + } + + /** + * this is a command's description message + * the second line text + * @format + * @usage usage message + * @arguments(format=true) + * arg1 argument description 1 + * the second line + * a2,arg2 argument description 2 + * the second line + * @arguments( + * arg1="argument description 1 + * the second line", + * "a2,arg2"="argument description 2 + * the second line" + * ) + * @options + * -s, --long LONG option description 1 + * --opt OPT option description 2 + * @example example text one + * the second line example + * @param string $argsStr + */ + public static function annotationArguments($argsStr) + { + } + + /** + * @param array $options + * @return array + */ + public static function alignmentOptions(array $options) + { + // e.g '-h, --help' + $hasShort = (bool)strpos(implode(array_keys($options), ''), ','); + if (!$hasShort) { + return $options; + } + $formatted = []; + foreach ($options as $name => $des) { + if (!($name = trim($name, ', '))) { + continue; + } + if (!strpos($name, ',')) { + // padding length equals to '-h, ' + $name = ' ' . $name; + } else { + $name = str_replace([' ', ','], ['', ', '], $name); + } + $formatted[$name] = $des; + } + + return $formatted; + } + + /** + * 计算并格式化资源消耗 + * @param int $startTime + * @param int|float $startMem + * @param array $info + * @return array + */ + public static function runtime($startTime, $startMem, array $info = []) + { + $info['startTime'] = $startTime; + $info['endTime'] = microtime(true); + $info['endMemory'] = memory_get_usage(true); + // 计算运行时间 + $info['runtime'] = number_format(($info['endTime'] - $startTime) * 1000, 3) . 'ms'; + if ($startMem) { + $startMem = array_sum(explode(' ', $startMem)); + $endMem = array_sum(explode(' ', $info['endMemory'])); + $info['memory'] = number_format(($endMem - $startMem) / 1024, 3) . 'kb'; + } + $peakMem = memory_get_peak_usage() / 1024 / 1024; + $info['peakMemory'] = number_format($peakMem, 3) . 'Mb'; + + return $info; + } + + /** + * @param float $memory + * @return string + * ``` + * FormatUtil::memoryUsage(memory_get_usage(true)); + * ``` + */ + public static function memoryUsage($memory) + { + if ($memory>= 1024 * 1024 * 1024) { + return sprintf('%.1f GiB', $memory / 1024 / 1024 / 1024); + } + if ($memory>= 1024 * 1024) { + return sprintf('%.1f MiB', $memory / 1024 / 1024); + } + if ($memory>= 1024) { + return sprintf('%d KiB', $memory / 1024); + } + + return sprintf('%d B', $memory); + } + + /** + * format Timestamp + * @param int $secs + * @return string + */ + public static function timestamp($secs) + { + static $timeFormats = [[0, '< 1 sec'], [1, '1 sec'], [2, 'secs', 1], [60, '1 min'], [120, 'mins', 60], [3600, '1 hr'], [7200, 'hrs', 3600], [86400, '1 day'], [172800, 'days', 86400]]; + foreach ($timeFormats as $index => $format) { + if ($secs>= $format[0]) { + if (isset($timeFormats[$index + 1]) && ($secs < $timeFormats[$index + 1][0] || $index === \count($timeFormats) - 1)) { + if (2 === \count($format)) { + return $format[1]; + } + + return floor($secs / $format[2]) . ' ' . $format[1]; + } + } + } + + return date('Y-m-d H:i:s', $secs); + } + + /** + * @param $string + * @param $width + * @return array + */ + public static function splitStringByWidth($string, $width) + { + // str_split is not suitable for multi-byte characters, we should use preg_split to get char array properly. + // additionally, array_slice() is not enough as some character has doubled width. + // we need a function to split string not by character count but by string width + if (false === ($encoding = mb_detect_encoding($string, null, true))) { + return str_split($string, $width); + } + $utf8String = mb_convert_encoding($string, 'utf8', $encoding); + $lines = array(); + $line = ''; + foreach (preg_split('//u', $utf8String) as $char) { + // test if $char could be appended to current line + if (mb_strwidth($line . $char, 'utf8') <= $width) { + $line .= $char; + continue; + } + // if not, push current line to array and make new line + $lines[] = str_pad($line, $width); + $line = $char; + } + if ('' !== $line) { + $lines[] = \count($lines) ? str_pad($line, $width) : $line; + } + mb_convert_variables($encoding, 'utf8', $lines); + + return $lines; + } + + /** + * splice Array + * @param array $data + * e.g [ + * 'system' => 'Linux', + * 'version' => '4.4.5', + * ] + * @param array $opts + * @return string + */ + public static function spliceKeyValue(array $data, array $opts = []) + { + $text = ''; + $opts = array_merge([ + 'leftChar' => '', + // e.g ' ', ' * ' + 'sepChar' => ' ', + // e.g ' | ' OUT: key | value + 'keyStyle' => '', + // e.g 'info','comment' + 'valStyle' => '', + // e.g 'info','comment' + 'keyMinWidth' => 8, + 'keyMaxWidth' => null, + // if not set, will automatic calculation + 'ucFirst' => true, + ], $opts); + if (!is_numeric($opts['keyMaxWidth'])) { + $opts['keyMaxWidth'] = Helper::getKeyMaxWidth($data); + } + // compare + if ((int)$opts['keyMinWidth']> $opts['keyMaxWidth']) { + $opts['keyMaxWidth'] = $opts['keyMinWidth']; + } + $keyStyle = trim($opts['keyStyle']); + foreach ($data as $key => $value) { + $hasKey = !\is_int($key); + $text .= $opts['leftChar']; + if ($hasKey && $opts['keyMaxWidth']) { + $key = str_pad($key, $opts['keyMaxWidth'], ' '); + $text .= Helper::wrapTag($key, $keyStyle) . $opts['sepChar']; + } + // if value is array, translate array to string + if (\is_array($value)) { + $temp = ''; + /** @var array $value */ + foreach ($value as $k => $val) { + if (\is_bool($val)) { + $val = $val ? 'True' : 'False'; + } else { + $val = is_scalar($val) ? (string)$val : \gettype($val); + } + $temp .= (!is_numeric($k) ? "{$k}: " : '') . "{$val}, "; + } + $value = rtrim($temp, ' ,'); + } else { + if (\is_bool($value)) { + $value = $value ? 'True' : 'False'; + } else { + $value = (string)$value; + } + } + $value = $hasKey && $opts['ucFirst'] ? ucfirst($value) : $value; + $text .= Helper::wrapTag($value, $opts['valStyle']) . "\n"; + } + + return $text; + } +} \ No newline at end of file diff --git a/src/Utils/Helper.php b/src/Utils/Helper.php index 85c92d0c..f2f9d0fa 100644 --- a/src/Utils/Helper.php +++ b/src/Utils/Helper.php @@ -1,4 +1,5 @@ /dev/null &'); - } - } - - /** - * @param string $command - * @param null|string $logfile - * @param null|string $user - * @return mixed - * @throws \RuntimeException - */ - public static function exec($command, $logfile = null, $user = null) - { - // If should run as another user, we must be on *nix and must have sudo privileges. - $suDo = ''; - - if ($user && self::isUnix() && self::isRoot()) { - $suDo = "sudo -u $user"; - } - - // Start execution. Run in foreground (will block). - $logfile = $logfile ?: self::getNullDevice(); - - // Start execution. Run in foreground (will block). - exec("$suDo $command 1>> \"$logfile\" 2>&1", $dummy, $retVal); - - if ($retVal !== 0) { - throw new \RuntimeException("command exited with status '$retVal'."); - } - - return $dummy; - } - - /** - * Method to execute a command in the sys - * Uses : - * 1. system - * 2. passthru - * 3. exec - * 4. shell_exec - * @param $command - * @param bool $returnStatus - * @return array|string - */ - public static function runCommand($command, $returnStatus = true) - { - $return_var = 1; - - //system - if (\function_exists('system')) { - ob_start(); - system($command, $return_var); - $output = ob_get_contents(); - ob_end_clean(); - - // passthru - } elseif (\function_exists('passthru')) { - ob_start(); - passthru($command, $return_var); - $output = ob_get_contents(); - ob_end_clean(); - //exec - } else if (\function_exists('exec')) { - exec($command, $output, $return_var); - $output = implode("\n", $output); - - //shell_exec - } else if (\function_exists('shell_exec')) { - $output = shell_exec($command); - } else { - $output = 'Command execution not possible on this system'; - $return_var = 0; - } - - if ($returnStatus) { - return ['output' => trim($output), 'status' => $return_var]; - } - - return trim($output); - } - - /** - * @return string - */ - public static function getTempDir() - { - // @codeCoverageIgnoreStart - if (\function_exists('sys_get_temp_dir')) { - $tmp = sys_get_temp_dir(); - } elseif (!empty($_SERVER['TMP'])) { - $tmp = $_SERVER['TMP']; - } elseif (!empty($_SERVER['TEMP'])) { - $tmp = $_SERVER['TEMP']; - } elseif (!empty($_SERVER['TMPDIR'])) { - $tmp = $_SERVER['TMPDIR']; - } else { - $tmp = getcwd(); - } - // @codeCoverageIgnoreEnd - - return $tmp; - } - - /** - * @param string $program - * @return int|string - */ - public static function getCpuUsage($program) - { - if (!$program) { - return -1; - } - - $info = exec('ps aux | grep ' . $program . ' | grep -v grep | grep -v su | awk {"print 3ドル"}'); - - return $info; - } - - /** - * @param $program - * @return int|string - */ - public static function getMemUsage($program) - { - if (!$program) { - return -1; - } - - $info = exec('ps aux | grep ' . $program . ' | grep -v grep | grep -v su | awk {"print 4ドル"}'); - - return $info; - } - /** * 给对象设置属性值 * @param $object @@ -268,7 +124,7 @@ public static function getMemUsage($program) public static function init($object, array $options) { foreach ($options as $property => $value) { - $object->$property = $value; + $object->{$property} = $value; } } @@ -278,18 +134,25 @@ public static function init($object, array $options) * @return \RecursiveIteratorIterator * @throws \InvalidArgumentException */ - public static function recursiveDirectoryIterator(string $srcDir, callable $filter) + public static function recursiveDirectoryIterator($srcDir, callable $filter) { if (!$srcDir || !file_exists($srcDir)) { throw new \InvalidArgumentException('Please provide a exists source directory.'); } - $directory = new \RecursiveDirectoryIterator($srcDir); $filterIterator = new \RecursiveCallbackFilterIterator($directory, $filter); return new \RecursiveIteratorIterator($filterIterator); } + /** + * @param string $command + * @param array $map + */ + public static function commandSearch($command, array $map) + { + } + /** * wrap a style tag * @param string $string @@ -301,12 +164,11 @@ public static function wrapTag($string, $tag) if (!$string) { return ''; } - if (!$tag) { return $string; } - return "<$tag>$string"; + return "<{$tag}>{$string}"; } /** @@ -316,7 +178,7 @@ public static function wrapTag($string, $tag) */ public static function stripAnsiCode($string) { - return preg_replace('/033円\[[\d;?]*\w/', '', $string); + return preg_replace('/\033円\\[[\\d;?]*\\w/', '', $string); } /** @@ -335,31 +197,13 @@ public static function strUtf8Len($string) */ public static function strLen($string) { - if (false === $encoding = mb_detect_encoding($string, null, true)) { + if (false === ($encoding = mb_detect_encoding($string, null, true))) { return \strlen($string); } return mb_strwidth($string, $encoding); } - /** - * to camel - * @param string $name - * @return mixed|string - */ - public static function camelCase($name) - { - $name = trim($name, '-_'); - - // convert 'first-second' to 'firstSecond' - if (strpos($name, '-')) { - $name = ucwords(str_replace('-', ' ', $name)); - $name = str_replace(' ', '', lcfirst($name)); - } - - return $name; - } - /** * findValueByNodes * @param array $data @@ -370,7 +214,6 @@ public static function camelCase($name) public static function findValueByNodes(array $data, array $nodes, $default = null) { $temp = $data; - foreach ($nodes as $name) { if (isset($temp[$name])) { $temp = $temp[$name]; @@ -384,104 +227,31 @@ public static function findValueByNodes(array $data, array $nodes, $default = nu } /** - * @param $string - * @param $width + * find similar text from an array|Iterator + * @param string $need + * @param \Iterator|array $iterator + * @param int $similarPercent * @return array */ - public static function splitStringByWidth($string, $width) + public static function findSimilar($need, $iterator, $similarPercent = 45) { - // str_split is not suitable for multi-byte characters, we should use preg_split to get char array properly. - // additionally, array_slice() is not enough as some character has doubled width. - // we need a function to split string not by character count but by string width - if (false === $encoding = mb_detect_encoding($string, null, true)) { - return str_split($string, $width); + // find similar command names by similar_text() + $similar = []; + if (!$need) { + return $similar; } - - $utf8String = mb_convert_encoding($string, 'utf8', $encoding); - $lines = array(); - $line = ''; - foreach (preg_split('//u', $utf8String) as $char) { - // test if $char could be appended to current line - if (mb_strwidth($line . $char, 'utf8') <= $width) { - $line .= $char; - continue; + foreach ($iterator as $name) { + similar_text($need, $name, $percent); + if ($similarPercent <= (int)$percent) { + $similar[] = $name; } - // if not, push current line to array and make new line - $lines[] = str_pad($line, $width); - $line = $char; - } - if ('' !== $line) { - $lines[] = \count($lines) ? str_pad($line, $width) : $line; - } - - mb_convert_variables($encoding, 'utf8', $lines); - - return $lines; - } - - /** - * @param $memory - * @return string - * ``` - * Helper::formatMemory(memory_get_usage(true)); - * ``` - */ - public static function formatMemory($memory) - { - if ($memory>= 1024 * 1024 * 1024) { - return sprintf('%.1f GiB', $memory / 1024 / 1024 / 1024); - } - - if ($memory>= 1024 * 1024) { - return sprintf('%.1f MiB', $memory / 1024 / 1024); } - if ($memory>= 1024) { - return sprintf('%d KiB', $memory / 1024); - } - - return sprintf('%d B', $memory); - } - - /** - * formatTime - * @param int $secs - * @return string - */ - public static function formatTime($secs) - { - static $timeFormats = [ - [0, '< 1 sec'], - [1, '1 sec'], - [2, 'secs', 1], - [60, '1 min'], - [120, 'mins', 60], - [3600, '1 hr'], - [7200, 'hrs', 3600], - [86400, '1 day'], - [172800, 'days', 86400], - ]; - - foreach ($timeFormats as $index => $format) { - if ($secs>= $format[0]) { - if ((isset($timeFormats[$index + 1]) && $secs < $timeFormats[$index + 1][0]) - || $index === \count($timeFormats) - 1 - ) { - if (2 === \count($format)) { - return $format[1]; - } - - return floor($secs / $format[2]) . ' ' . $format[1]; - } - } - } - - return date('Y-m-d H:i:s', $secs); + return $similar; } /** * get key Max Width - * * @param array $data * [ * 'key1' => 'value1', @@ -493,7 +263,6 @@ public static function formatTime($secs) public static function getKeyMaxWidth(array $data, $expectInt = false) { $keyMaxWidth = 0; - foreach ($data as $key => $value) { // key is not a integer if (!$expectInt || !is_numeric($key)) { @@ -505,207 +274,6 @@ public static function getKeyMaxWidth(array $data, $expectInt = false) return $keyMaxWidth; } - /** - * spliceArray - * @param array $data - * e.g [ - * 'system' => 'Linux', - * 'version' => '4.4.5', - * ] - * @param array $opts - * @return string - */ - public static function spliceKeyValue(array $data, array $opts = []) - { - $text = ''; - $opts = array_merge([ - 'leftChar' => '', // e.g ' ', ' * ' - 'sepChar' => ' ', // e.g ' | ' OUT: key | value - 'keyStyle' => '', // e.g 'info','comment' - 'valStyle' => '', // e.g 'info','comment' - 'keyMinWidth' => 8, - 'keyMaxWidth' => null, // if not set, will automatic calculation - 'ucFirst' => true, // upper first char - ], $opts); - - if (!is_numeric($opts['keyMaxWidth'])) { - $opts['keyMaxWidth'] = self::getKeyMaxWidth($data); - } - - // compare - if ((int)$opts['keyMinWidth']> $opts['keyMaxWidth']) { - $opts['keyMaxWidth'] = $opts['keyMinWidth']; - } - - $keyStyle = trim($opts['keyStyle']); - - foreach ($data as $key => $value) { - $hasKey = !\is_int($key); - $text .= $opts['leftChar']; - - if ($hasKey && $opts['keyMaxWidth']) { - $key = str_pad($key, $opts['keyMaxWidth'], ' '); - // $text .= ($keyStyle ? "<{$keystyle}>$key " : $key) . $opts['sepChar']; - $text .= self::wrapTag($key, $keyStyle) . $opts['sepChar']; - } - - // if value is array, translate array to string - if (\is_array($value)) { - $temp = ''; - - /** @var array $value */ - foreach ($value as $k => $val) { - if (\is_bool($val)) { - $val = $val ? 'True' : 'False'; - } else { - $val = is_scalar($val) ? (string)$val : \gettype($val); - } - - $temp .= (!is_numeric($k) ? "$k: " : '') . "$val, "; - } - - $value = rtrim($temp, ' ,'); - } else if (\is_bool($value)) { - $value = $value ? 'True' : 'False'; - } else { - $value = (string)$value; - } - - $value = $hasKey && $opts['ucFirst'] ? ucfirst($value) : $value; - $text .= self::wrapTag($value, $opts['valStyle']) . "\n"; - } - - return $text; - } - - // next: form yii2 - - /** - * Usage: list($width, $height) = ConsoleHelper::getScreenSize(); - * - * @param boolean $refresh whether to force checking and not re-use cached size value. - * This is useful to detect changing window size while the application is running but may - * not get up to date values on every terminal. - * @return array|boolean An array of ($width, $height) or false when it was not able to determine size. - */ - public static function getScreenSize($refresh = false) - { - static $size; - if ($size !== null && !$refresh) { - return $size; - } - - if (self::isOnWindows()) { - $output = []; - exec('mode con', $output); - - if (isset($output[1]) && strpos($output[1], 'CON') !== false) { - return ($size = [ - (int)preg_replace('~\D~', '', $output[3]), - (int)preg_replace('~\D~', '', $output[4]) - ]); - } - } else { - // try stty if available - $stty = []; - if (exec('stty -a 2>&1', $stty) && preg_match('/rows\s+(\d+);\s*columns\s+(\d+);/mi', implode(' ', $stty), $matches)) { - return ($size = [$matches[2], $matches[1]]); - } - - // fallback to tput, which may not be updated on terminal resize - if (($width = (int)exec('tput cols 2>&1'))> 0 && ($height = (int)exec('tput lines 2>&1'))> 0) { - return ($size = [$width, $height]); - } - - // fallback to ENV variables, which may not be updated on terminal resize - if (($width = (int)getenv('COLUMNS'))> 0 && ($height = (int)getenv('LINES'))> 0) { - return ($size = [$width, $height]); - } - } - - return ($size = false); - } - - /** - * Word wrap text with indentation to fit the screen size - * - * If screen size could not be detected, or the indentation is greater than the screen size, the text will not be wrapped. - * - * The first line will **not** be indented, so `Console::wrapText("Lorem ipsum dolor sit amet.", 4)` will result in the - * following output, given the screen width is 16 characters: - * - * ``` - * Lorem ipsum - * dolor sit - * amet. - * ``` - * - * @param string $text the text to be wrapped - * @param integer $indent number of spaces to use for indentation. - * @param integer $width - * @return string the wrapped text. - * @from yii2 - */ - public static function wrapText($text, $indent = 0, $width = 0) - { - if (!$text) { - return $text; - } - - if ((int)$width <= 0) { - $size = static::getScreenSize(); - - if ($size === false || $size[0] <= $indent) { - return $text; - } - - $width = $size[0]; - } - - $pad = str_repeat(' ', $indent); - $lines = explode("\n", wordwrap($text, $width - $indent, "\n", true)); - $first = true; - - foreach ($lines as $i => $line) { - if ($first) { - $first = false; - continue; - } - $lines[$i] = $pad . $line; - } - - return $pad . ' ' . implode("\n", $lines); - } - - /** - * 获取资源消耗 - * @param int $startTime - * @param int|float $startMem - * @param array $info - * @return array - */ - public static function runtime($startTime, $startMem, array $info = []) - { - $info['startTime'] = $startTime; - $info['endTime'] = microtime(true); - $info['endMemory'] = memory_get_usage(true); - - // 计算运行时间 - $info['runtime'] = number_format(($info['endTime'] - $startTime) * 1000, 3) . 'ms'; - - if ($startMem) { - $startMem = array_sum(explode(' ', $startMem)); - $endMem = array_sum(explode(' ', $info['endMemory'])); - - $info['memory'] = number_format(($endMem - $startMem) / 1024, 3) . 'kb'; - } - - $peakMem = memory_get_peak_usage() / 1024 / 1024; - $info['peakMemory'] = number_format($peakMem, 3) . 'Mb'; - - return $info; - } - /** * dump vars * @param array ...$args @@ -717,7 +285,7 @@ public static function dumpVars(...$args) var_dump(...$args); $string = ob_get_clean(); - return preg_replace("/=>\n\s+/", '=> ', $string); + return preg_replace("/=>\n\\s+/", '=> ', $string); } /** @@ -728,11 +296,10 @@ public static function dumpVars(...$args) public static function printVars(...$args) { $string = ''; - foreach ($args as $arg) { $string .= print_r($arg, 1) . PHP_EOL; } - return preg_replace("/Array\n\s+\(/", 'Array (', $string); + return preg_replace("/Array\n\\s+\\(/", 'Array (', $string); } -} +} \ No newline at end of file diff --git a/src/Utils/Interact.php b/src/Utils/Interact.php index 0addf21b..b41948f9 100644 --- a/src/Utils/Interact.php +++ b/src/Utils/Interact.php @@ -1,4 +1,5 @@ $description"; + $text = "{$description}"; foreach ($options as $key => $value) { - $text .= "\n $key) $value"; + $text .= "\n {$key}) {$value}"; } - $defaultText = $default ? "[default:{$default}]" : ''; - $r = self::read($text . "\n You choice{$defaultText} : "); - + self::write($text); + beginChoice: + $r = self::read("Your choice{$defaultText} : "); // error, allow try again once. if (!array_key_exists($r, $options)) { goto beginChoice; } - // exit if ($r === 'q') { self::write("\n Quit,ByeBye.", true, true); @@ -118,14 +110,68 @@ public static function choice($description, $options, $default = null, $allowExi return $r; } + /** + * alias of the `multiSelect()` + * @param string $description + * @param string|array $options + * @param null|mixed $default + * @param bool $allowExit + * @return array + */ public static function checkbox($description, $options, $default = null, $allowExit = true) { return self::multiSelect($description, $options, $default, $allowExit); } + /** + * @param string $description + * @param string|array $options + * @param null|mixed $default + * @param bool $allowExit + * @return array + */ public static function multiSelect($description, $options, $default = null, $allowExit = true) { - return []; + if (!($description = trim($description))) { + self::error('Please provide a description text!', 1); + } + $sep = ','; + // ',' ' ' + $options = \is_array($options) ? $options : explode(',', $options); + // If default option is error + if (null !== $default && !isset($options[$default])) { + self::error("The default option [{$default}] don't exists.", true); + } + if ($allowExit) { + $options['q'] = 'quit'; + } + $text = "{$description}"; + foreach ($options as $key => $value) { + $text .= "\n {$key}) {$value}"; + } + self::write($text); + $defaultText = $default ? "[default:{$default}]" : ''; + $filter = function ($val) use ($options) { + return $val !== 'q' && isset($options[$val]); + }; + beginChoice: + $r = self::read("Your choice{$defaultText} : "); + $r = $r !== '' ? str_replace(' ', '', trim($r, $sep)) : ''; + // empty + if ($r === '') { + goto beginChoice; + } + // exit + if ($r === 'q') { + self::write("\n Quit,ByeBye.", true, true); + } + $rs = strpos($r, $sep) ? array_filter(explode($sep, $r), $filter) : [$r]; + // error, try again + if (!$rs) { + goto beginChoice; + } + + return $rs; } /** @@ -134,28 +180,23 @@ public static function multiSelect($description, $options, $default = null, $all * @param bool $default Default value * @return bool */ - public static function confirm($question, $default = true): bool + public static function confirm($question, $default = true) { - if (!$question = trim($question)) { - self::error('Please provide a question text!', 1); + if (!($question = trim($question))) { + self::warning('Please provide a question message!', 1); } - $question = ucfirst(trim($question, '?')); $default = (bool)$default; $defaultText = $default ? 'yes' : 'no'; - $message = "$question ?\nPlease confirm (yes|no)[default:$defaultText]: "; - + $message = "{$question} ?\nPlease confirm (yes|no)[default:{$defaultText}]: "; while (true) { $answer = self::read($message); - if (empty($answer)) { return $default; } - if (0 === stripos($answer, 'y')) { return true; } - if (0 === stripos($answer, 'n')) { return false; } @@ -166,20 +207,9 @@ public static function confirm($question, $default = true): bool /** * alias of the `question()` - * 询问,提出问题;返回 输入的结果 * @param string $question 问题 * @param null|string $default 默认值 * @param \Closure $validator The validate callback. It must return bool. - * @example This is an example - * ```php - * $answer = Interact::ask('Please input your name?', null, function ($answer) { - * if (!preg_match('/\w+/', $answer)) { - * Interact::error('The name must match "/\w+/"'); - * return false; - * } - * return true; - * }); - * ``` * @return string */ public static function ask($question, $default = null, \Closure $validator = null) @@ -189,32 +219,62 @@ public static function ask($question, $default = null, \Closure $validator = nul /** * 询问,提出问题;返回 输入的结果 + * @example This is an example + * ```php + * $answer = Interact::ask('Please input your name?', null, function ($answer) { + * if (!preg_match('/\w{2,}/', $answer)) { + * // output error tips. + * Interact::error('The name must match "/\w{2,}/"'); + * return false; + * } + * return true; + * }); + * echo "Your input: $answer"; + * ``` + * ```php + * // use the second arg in the validator. + * $answer = Interact::ask('Please input your name?', null, function ($answer, &$err) { + * if (!preg_match('/\w{2,}/', $answer)) { + * // setting error message. + * $err = 'The name must match "/\w{2,}/"'; + * return false; + * } + * return true; + * }); + * echo "Your input: $answer"; + * ``` * @param string $question - * @param null $default - * @param \Closure|null $validator + * @param null|mixed $default + * @param \Closure|null $validator Validator, must return bool. * @return null|string */ public static function question($question, $default = null, \Closure $validator = null) { - if (!$question = trim($question)) { + if (!($question = trim($question))) { self::error('Please provide a question text!', 1); } - - $defaultText = null !== $default ? "(default: $default)" : ''; - $answer = self::read('' . ucfirst($question) . "$defaultText "); - + $defText = null !== $default ? "(default: {$default})" : ''; + $message = '' . ucfirst($question) . "{$defText} "; + askQuestion: + $answer = self::read($message); if ('' === $answer) { if (null === $default) { self::error('A value is required.'); - - return static::question($question, $default, $validator); + goto askQuestion; } return $default; } - + // has answer validator if ($validator) { - return $validator($answer) ? $answer : static::question($question, $default, $validator); + $error = null; + if ($validator($answer, $error)) { + return $answer; + } + if ($error) { + Show::warning($error); + } + goto askQuestion; } return $answer; @@ -228,7 +288,7 @@ public static function question($question, $default = null, \Closure $validator * @param null|string $default 默认值 * @param \Closure $validator (默认验证输入是否为空)自定义回调验证输入是否符合要求; 验证成功返回true 否则 可返回错误消息 * @example This is an example - * ``` + * ```php * // no default value * Interact::limitedAsk('please entry you age?', null, function($age) * { @@ -253,59 +313,54 @@ public static function question($question, $default = null, \Closure $validator */ public static function limitedAsk($question, $default = null, \Closure $validator = null, $times = 3) { - if (!$question = trim($question)) { + if (!($question = trim($question))) { self::error('Please provide a question text!', 1); } - $result = false; $answer = ''; $question = ucfirst($question); - $back = $times = ((int)$times> 6 || $times < 1) ? 3 : (int)$times; - $defaultText = null !== $default ? "(default: $default)" : ''; - + $hasDefault = null !== $default; + $back = $times = (int)$times> 6 || $times < 1 ? 3 : (int)$times; + if ($hasDefault) { + $message = "{$question}(default: {$default}) "; + } else { + $message = "{$question}"; + Show::write($message); + } while ($times--) { - if ($defaultText) { - $answer = self::read("{$question}{$defaultText} "); - + if ($hasDefault) { + $answer = self::read($message); if ('' === $answer) { $answer = $default; $result = true; - break; } } else { $num = $times + 1; - $answer = self::read("{$question}\n(You have a [$num] chance to enter!) "); + $answer = self::read(sprintf('(You have [%s] chances to enter!) ', $num)); } - // If setting verify callback if ($validator && ($result = $validator($answer)) === true) { break; } - // no setting verify callback if (!$validator && $answer !== '') { $result = true; - break; } } - if (!$result) { if (null !== $default) { return $default; } - - self::write("\n You've entered incorrectly $back times in a row. exit!\n", true, 1); + self::write("\n You've entered incorrectly {$back} times in a row. exit!", true, 1); } return $answer; } - /************************************************************************************************** * password ask **************************************************************************************************/ - /** * Interactively prompts for input without echoing to the terminal. * Requires a bash shell or Windows and won't work with @@ -314,41 +369,32 @@ public static function limitedAsk($question, $default = null, \Closure $validato * @return string * @link https://stackoverflow.com/questions/187736/command-line-password-prompt-in-php * @link http://www.sitepoint.com/blogs/2009/05/01/interactive-cli-password-prompt-in-php + * @throws \RuntimeException */ - public static function promptSilent(string $prompt = 'Enter Password:') + public static function promptSilent($prompt = 'Enter Password:') { $prompt = $prompt ? addslashes($prompt) : 'Enter:'; - // $checkCmd = "/usr/bin/env bash -c 'echo OK'"; // $shell = 'echo 0ドル'; - $checkCmd = "bash -c 'echo OK'"; - // linux, unix, git-bash - if (Helper::runCommand($checkCmd, false) === 'OK') { + if (CliUtil::bashIsAvailable()) { // COMMAND: bash -c 'read -p "Enter Password:" -s user_input && echo $user_input' $command = sprintf('bash -c "read -p \'%s\' -s user_input && echo $user_input"', $prompt); - $password = Helper::runCommand($command, false); - + $password = CliUtil::runCommand($command, false); echo "\n"; + return $password; } - // at windows cmd. if (Helper::isWindows()) { - $vbScript = sys_get_temp_dir() . 'prompt_password.vbs'; - - file_put_contents( - $vbScript, - 'wscript.echo(InputBox("' . $prompt . '", "", "password here"))' - ); - + $vbScript = CliUtil::getTempDir() . '/hidden_prompt_input.vbs'; + file_put_contents($vbScript, 'wscript.echo(InputBox("' . $prompt . '", "", "password here"))'); $command = 'cscript //nologo ' . escapeshellarg($vbScript); $password = rtrim(shell_exec($command)); unlink($vbScript); return $password; } - throw new \RuntimeException('Can not invoke bash shell env'); } @@ -356,8 +402,9 @@ public static function promptSilent(string $prompt = 'Enter Password:') * alias of the method `promptSilent()` * @param string $prompt * @return string + * @throws \RuntimeException */ - public static function askHiddenInput(string $prompt = 'Enter Password:') + public static function askHiddenInput($prompt = 'Enter Password:') { return self::promptSilent($prompt); } @@ -366,9 +413,10 @@ public static function askHiddenInput(string $prompt = 'Enter Password:') * alias of the method `promptSilent()` * @param string $prompt * @return string + * @throws \RuntimeException */ - public static function askPassword(string $prompt = 'Enter Password:') + public static function askPassword($prompt = 'Enter Password:') { return self::promptSilent($prompt); } -} +} \ No newline at end of file diff --git a/src/Utils/ProcessUtil.php b/src/Utils/ProcessUtil.php new file mode 100644 index 00000000..6eba65a4 --- /dev/null +++ b/src/Utils/ProcessUtil.php @@ -0,0 +1,508 @@ + /dev/null &'); + } + + return $ret; + } + + /** + * @see ProcessUtil::forks() + * @param int $number + * @param callable|null $onStart + * @param callable|null $onError + * @return array|false + */ + public static function multi($number, callable $onStart = null, callable $onError = null) + { + return self::forks($number, $onStart, $onError); + } + + /** + * fork/create multi child processes. + * @param int $number + * @param callable|null $onStart Will running on the child processes. + * @param callable|null $onError + * @return array|false + */ + public static function forks($number, callable $onStart = null, callable $onError = null) + { + if ($number <= 0) { + return false; + } + if (!self::isSupported()) { + return false; + } + $pidAry = []; + for ($id = 0; $id < $number; $id++) { + $info = self::fork($onStart, $onError, $id); + $pidAry[$info['pid']] = $info; + } + + return $pidAry; + } + + /** + * @see ProcessUtil::fork() + * @param callable|null $onStart + * @param callable|null $onError + * @param int $id + * @return array|false + */ + public static function create(callable $onStart = null, callable $onError = null, $id = 0) + { + return self::fork($onStart, $onError, $id); + } + + /** + * fork/create a child process. + * @param callable|null $onStart Will running on the child process start. + * @param callable|null $onError + * @param int $id The process index number. will use `forks()` + * @return array|false + */ + public static function fork(callable $onStart = null, callable $onError = null, $id = 0) + { + if (!self::isSupported()) { + return false; + } + $info = []; + $pid = pcntl_fork(); + // at parent, get forked child info + if ($pid> 0) { + $info = ['id' => $id, 'pid' => $pid, 'startTime' => time()]; + } elseif ($pid === 0) { + // at child + $pid = getmypid(); + if ($onStart) { + $onStart($pid, $id); + } + } else { + if ($onError) { + $onError($pid); + } + Show::error('Fork child process failed! exiting.'); + } + + return $info; + } + + /** + * wait child exit. + * @param callable $onExit + * @return bool + */ + public static function wait(callable $onExit) + { + if (!self::isSupported()) { + return false; + } + $status = null; + //pid<0:子进程都没了 + //pid>0:捕获到一个子进程退出的情况 + //pid=0:没有捕获到退出的子进程 + while (($pid = pcntl_waitpid(-1, $status, WNOHANG))>= 0) { + if ($pid) { + // ... (callback, pid, exitCode, status) + $onExit($pid, pcntl_wexitstatus($status), $status); + } else { + usleep(50000); + } + } + + return true; + } + + /** + * Stops all running children + * @param array $children + * [ + * 'pid' => [ + * 'id' => worker id + * ], + * ... ... + * ] + * @param int $signal + * @param array $events + * [ + * 'beforeStops' => function ($sigText) { + * echo "Stopping processes({$sigText}) ...\n"; + * }, + * 'beforeStop' => function ($pid, $info) { + * echo "Stopping process(PID:$pid)\n"; + * } + * ] + * @return bool + */ + public static function stopChildren(array $children, $signal = SIGTERM, array $events = []) + { + if (!$children) { + return false; + } + if (!self::isSupported()) { + return false; + } + $events = array_merge(['beforeStops' => null, 'beforeStop' => null], $events); + $signals = [SIGINT => 'SIGINT(Ctrl+C)', SIGTERM => 'SIGTERM', SIGKILL => 'SIGKILL']; + if ($cb = $events['beforeStops']) { + $cb($signal, $signals[$signal]); + } + foreach ($children as $pid => $child) { + if ($cb = $events['beforeStop']) { + $cb($pid, $child); + } + // send exit signal. + self::sendSignal($pid, $signal); + } + + return true; + } + /************************************************************************************** + * basic signal methods + *************************************************************************************/ + /** + * send kill signal to the process + * @param int $pid + * @param bool $force + * @param int $timeout + * @return bool + */ + public static function kill($pid, $force = false, $timeout = 3) + { + return self::sendSignal($pid, $force ? SIGKILL : SIGTERM, $timeout); + } + + /** + * Do shutdown process and wait it exit. + * @param int $pid Master Pid + * @param int $signal + * @param int $waitTime + * @param null $error + * @param string $name + * @return bool + */ + public static function killAndWait($pid, $signal = SIGTERM, $waitTime = 30, &$error = null, $name = 'process') + { + // $opts = array_merge([], $opts); + // do stop + if (!self::kill($signal)) { + $error = "Send stop signal to the {$name}(PID:{$pid}) failed!"; + + return false; + } + // not wait, only send signal + if ($waitTime <= 0) { + $error = "The {$name} process stopped"; + + return true; + } + $startTime = time(); + Show::write('Stopping .', false); + // wait exit + while (true) { + if (!self::isRunning($pid)) { + break; + } + if (time() - $startTime> $waitTime) { + $error = "Stop the {$name}(PID:{$pid}) failed(timeout)!"; + break; + } + Show::write('.', false); + sleep(1); + } + + return true; + } + + /** + * 杀死所有进程 + * @param $name + * @param int $sigNo + * @return string + */ + public static function killByName($name, $sigNo = 9) + { + $cmd = 'ps -eaf |grep "' . $name . '" | grep -v "grep"| awk "{print 2ドル}"|xargs kill -' . $sigNo; + + return exec($cmd); + } + + /** + * @param int $pid + * @return bool + */ + public static function isRunning($pid) + { + return $pid> 0 && @posix_kill($pid, 0); + } + + /** + * exit + * @param int $code + */ + public static function quit($code = 0) + { + exit((int)$code); + } + /************************************************************************************** + * process signal handle + *************************************************************************************/ + /** + * send signal to the process + * @param int $pid + * @param int $signal + * @param int $timeout + * @return bool + */ + public static function sendSignal($pid, $signal, $timeout = 0) + { + if ($pid <= 0) { + return false; + } + if (!self::isSupported()) { + return false; + } + // do send + if ($ret = posix_kill($pid, $signal)) { + return true; + } + // don't want retry + if ($timeout <= 0) { + return $ret; + } + // failed, try again ... + $timeout = $timeout> 0 && $timeout < 10 ? $timeout : 3; + $startTime = time(); + // retry stop if not stopped. + while (true) { + // success + if (!($isRunning = @posix_kill($pid, 0))) { + break; + } + // have been timeout + if (time() - $startTime>= $timeout) { + return false; + } + // try again kill + $ret = posix_kill($pid, $signal); + usleep(10000); + } + + return $ret; + } + + /** + * install signal + * @param int $signal e.g: SIGTERM SIGINT(Ctrl+C) SIGUSR1 SIGUSR2 SIGHUP + * @param callable $handler + * @return bool + */ + public static function installSignal($signal, callable $handler) + { + return pcntl_signal($signal, $handler, false); + } + + /** + * dispatch signal + * @return bool + */ + public static function dispatchSignal() + { + // receive and dispatch sig + return pcntl_signal_dispatch(); + } + /************************************************************************************** + * some help method + *************************************************************************************/ + /** + * get current process id + * @return int + */ + public static function getPid() + { + return getmypid(); + // or use posix_getpid() + } + + /** + * get Pid from File + * @param string $file + * @param bool $checkLive + * @return int + */ + public static function getPidByFile($file, $checkLive = false) + { + if ($file && file_exists($file)) { + $pid = (int)file_get_contents($file); + // check live + if ($checkLive && self::isRunning($pid)) { + return $pid; + } + unlink($file); + } + + return 0; + } + + /** + * Get unix user of current process. + * @return array + */ + public static function getCurrentUser() + { + return posix_getpwuid(posix_getuid()); + } + + /** + * @param int $seconds + * @param callable $handler + */ + public static function afterDo($seconds, callable $handler) + { + /* + self::signal(SIGALRM, function () { + static $i = 0; + echo "#{$i}\talarm\n"; + $i++; + if ($i> 20) { + pcntl_alarm(-1); + } + });*/ + self::installSignal(SIGALRM, $handler); + // self::alarm($seconds); + pcntl_alarm($seconds); + } + + /** + * Set process title. + * @param string $title + * @return bool + */ + public static function setName($title) + { + return self::setTitle($title); + } + + /** + * Set process title. + * @param string $title + * @return bool + */ + public static function setTitle($title) + { + if (Helper::isMac()) { + return false; + } + if (\function_exists('cli_set_process_title')) { + cli_set_process_title($title); + } + + return true; + } + + /** + * Set unix user and group for current process script. + * @param string $user + * @param string $group + * @throws \RuntimeException + */ + public static function changeScriptOwner($user, $group = '') + { + $uInfo = posix_getpwnam($user); + if (!$uInfo || !isset($uInfo['uid'])) { + throw new \RuntimeException("User ({$user}) not found."); + } + $uid = (int)$uInfo['uid']; + // Get gid. + if ($group) { + if (!($gInfo = posix_getgrnam($group))) { + throw new \RuntimeException("Group {$group} not exists", -300); + } + $gid = (int)$gInfo['gid']; + } else { + $gid = (int)$uInfo['gid']; + } + if (!posix_initgroups($uInfo['name'], $gid)) { + throw new \RuntimeException("The user [{$user}] is not in the user group ID [GID:{$gid}]", -300); + } + posix_setgid($gid); + if (posix_geteuid() !== $gid) { + throw new \RuntimeException("Unable to change group to {$user} (UID: {$gid}).", -300); + } + posix_setuid($uid); + if (posix_geteuid() !== $uid) { + throw new \RuntimeException("Unable to change user to {$user} (UID: {$uid}).", -300); + } + } +} \ No newline at end of file diff --git a/src/Utils/ProgressBar.php b/src/Utils/ProgressBar.php index 3f9b13a7..d66d12ed 100644 --- a/src/Utils/ProgressBar.php +++ b/src/Utils/ProgressBar.php @@ -1,4 +1,5 @@ --------------------------] * 3 [しかくしかくしかく>------------------------] @@ -26,66 +26,56 @@ class ProgressBar { // options private $barWidth = 30; - - private $completeChar = '='; // 已完成的显示字符 - private $progressChar = '>'; // 当前进度的显示字符 - private $remainingChar = '-'; // 剩下的的显示字符 + private $completeChar = '='; + // 已完成的显示字符 + private $progressChar = '>'; + // 当前进度的显示字符 + private $remainingChar = '-'; + // 剩下的的显示字符 private $redrawFreq = 1; private $format; - /** * 已完成百分比 * @var float */ private $percent = 0.0; - /** * maximal steps. * 当前在多少步 * @var int */ private $step = 0; - /** * maximal steps. * 设置多少步就会走完进度条, * @var int */ private $maxSteps; - /** * step Width * 设置步长 * @var int */ private $stepWidth; - private $startTime; private $finishTime; - private $overwrite = true; private $started = false; private $firstRun = true; - /** * @var OutputInterface */ private $output; - /** * messages * @var array */ private $messages = []; - /** * section parsers * @var \Closure[] */ - private static $parsers = [ - // 'precent' => function () { ... }, - ]; - + private static $parsers = []; const DEFAULT_FORMAT = '[{@bar}] {@percent:3s}%({@current}/{@max}) {@elapsed:6s}/{@estimated:-6s} {@memory:6s}'; /** @@ -93,7 +83,7 @@ class ProgressBar * @param int $maxSteps * @return ProgressBar */ - public static function create(OutputInterface $output = null, int $maxSteps = 0) + public static function create(OutputInterface $output = null, $maxSteps = 0) { return new self($output, $maxSteps); } @@ -102,10 +92,9 @@ public static function create(OutputInterface $output = null, int $maxSteps = 0) * @param OutputInterface $output * @param int $maxSteps */ - public function __construct(OutputInterface $output = null, int $maxSteps = 0) + public function __construct(OutputInterface $output = null, $maxSteps = 0) { - $this->output = $output ?: new Output; - + $this->output = $output ?: new Output(); $this->setMaxSteps($maxSteps); // Helper::loadAttrs($this, $config); } @@ -120,16 +109,13 @@ public function start($maxSteps = null) if ($this->started) { throw new \LogicException('Progress bar already started.'); } - $this->startTime = time(); $this->step = 0; $this->percent = 0.0; $this->started = true; - if (null !== $maxSteps) { $this->setMaxSteps($maxSteps); } - $this->display(); } @@ -138,12 +124,11 @@ public function start($maxSteps = null) * @param int $step 前进几步 * @throws \LogicException */ - public function advance(int $step = 1) + public function advance($step = 1) { if (!$this->started) { throw new \LogicException('Progress indicator has not yet been started.'); } - $this->advanceTo($this->step + $step); } @@ -151,19 +136,17 @@ public function advance(int $step = 1) * 直接前进到第几步 * @param int $step 第几步 */ - public function advanceTo(int $step) + public function advanceTo($step) { if ($this->maxSteps && $step> $this->maxSteps) { $this->maxSteps = $step; } elseif ($step < 0) { $step = 0; } - $prevPeriod = (int)($this->step / $this->redrawFreq); $currPeriod = (int)($step / $this->redrawFreq); $this->step = $step; - $this->percent = $this->maxSteps ? (float)$this->step / $this->maxSteps : 0; - + $this->percent = $this->maxSteps ? (double)$this->step / $this->maxSteps : 0; if ($prevPeriod !== $currPeriod || $this->maxSteps === $step) { $this->display(); } @@ -178,19 +161,15 @@ public function finish() if (!$this->started) { throw new \LogicException('Progress bar has not yet been started.'); } - if (!$this->maxSteps) { $this->maxSteps = $this->step; } - if ($this->step === $this->maxSteps && !$this->overwrite) { // prevent double 100% output return; } - $this->finishTime = time(); $this->advanceTo($this->maxSteps); - $this->output->write(''); } @@ -202,13 +181,11 @@ public function display() if (null === $this->format) { $this->format = self::DEFAULT_FORMAT; } - $this->render($this->buildLine()); } /** * Removes the progress bar from the current line. - * * This is useful if you wish to write some output * while a progress bar is running. * Call display() to show the progress bar again. @@ -218,7 +195,6 @@ public function clear() if (!$this->overwrite) { return; } - $this->render(''); } @@ -226,20 +202,18 @@ public function clear() * render * @param string $text */ - public function render(string $text) + public function render($text) { if ($this->overwrite) { if (!$this->firstRun) { - // \x0D - Move the cursor to the beginning of the line // \x1B[2K - Erase the line - $this->output->write("\x0D\x1B[2K", false); + $this->output->write("\r33円[2K", false); $this->output->write($text, false); } } elseif ($this->step> 0) { $this->output->write(''); } - $this->firstRun = false; } @@ -248,8 +222,8 @@ public function render(string $text) */ protected function buildLine() { -// $regex = "{%([a-z\-_]+)(?:\:([^%]+))?%}i"; - return preg_replace_callback('/{@([\w]+)(?:\:([\w-]+))?}/i', function ($matches) { + // $regex = "{%([a-z\-_]+)(?:\:([^%]+))?%}i"; + return preg_replace_callback('/{@([\\w]+)(?:\\:([\\w-]+))?}/i', function ($matches) { if ($formatter = $this->getParser($matches[1])) { $text = $formatter($this, $this->output); } elseif (isset($this->messages[$matches[1]])) { @@ -257,7 +231,6 @@ protected function buildLine() } else { return $matches[1]; } - if (isset($matches[2])) { $text = sprintf('%' . $matches[2], $text); } @@ -271,7 +244,7 @@ protected function buildLine() * @param string $section * @param callable $handler */ - public function setParser(string $section, callable $handler) + public function setParser($section, callable $handler) { self::$parsers[$section] = $handler; } @@ -283,18 +256,16 @@ public function setParser(string $section, callable $handler) * @return mixed * @throws \RuntimeException */ - public function getParser(string $section, bool $throwException = false) + public function getParser($section, $throwException = false) { if (!self::$parsers) { self::$parsers = self::loadDefaultParsers(); } - if (isset(self::$parsers[$section])) { return self::$parsers[$section]; } - if ($throwException) { - throw new \RuntimeException("The section($section) formatter is not registered!", -500); + throw new \RuntimeException("The section({$section}) formatter is not registered!", -500); } return null; @@ -303,7 +274,7 @@ public function getParser(string $section, bool $throwException = false) /** * @return array */ - public function getMessages(): array + public function getMessages() { return $this->messages; } @@ -321,7 +292,7 @@ public function setMessages(array $messages) * @param string $message The text to associate with the placeholder * @param string $name The name of the placeholder */ - public function setMessage($message, string $name = 'message') + public function setMessage($message, $name = 'message') { $this->messages[$name] = $message; } @@ -330,14 +301,13 @@ public function setMessage($message, string $name = 'message') * @param string $name * @return string */ - public function getMessage(string $name = 'message') + public function getMessage($name = 'message') { return $this->messages[$name]; } /** * Gets the current step position. - * * @return int The progress bar step */ public function getProgress() @@ -352,7 +322,6 @@ public function getStep() /** * Sets the redraw frequency. - * * @param int|float $freq The frequency in steps */ public function setRedrawFreq($freq) @@ -360,7 +329,7 @@ public function setRedrawFreq($freq) $this->redrawFreq = max((int)$freq, 1); } - public function setOverwrite(bool $overwrite) + public function setOverwrite($overwrite) { $this->overwrite = $overwrite; } @@ -369,7 +338,7 @@ public function setOverwrite(bool $overwrite) * Sets the progress bar maximal steps. * @param int $maxSteps The progress bar max steps */ - private function setMaxSteps(int $maxSteps) + private function setMaxSteps($maxSteps) { $this->maxSteps = max(0, $maxSteps); $this->stepWidth = $this->maxSteps ? Helper::strLen($this->maxSteps) : 2; @@ -378,7 +347,7 @@ private function setMaxSteps(int $maxSteps) /** * @return int */ - public function getMaxSteps(): int + public function getMaxSteps() { return $this->maxSteps; } @@ -386,7 +355,7 @@ public function getMaxSteps(): int /** * @return int */ - public function getStepWidth(): int + public function getStepWidth() { return $this->stepWidth; } @@ -394,7 +363,7 @@ public function getStepWidth(): int /** * @param int $stepWidth */ - public function setStepWidth(int $stepWidth) + public function setStepWidth($stepWidth) { $this->stepWidth = $stepWidth; } @@ -402,7 +371,7 @@ public function setStepWidth(int $stepWidth) /** * @return int */ - public function getBarWidth(): int + public function getBarWidth() { return $this->barWidth; } @@ -410,7 +379,7 @@ public function getBarWidth(): int /** * @param int $barWidth */ - public function setBarWidth(int $barWidth) + public function setBarWidth($barWidth) { $this->barWidth = $barWidth; } @@ -418,7 +387,7 @@ public function setBarWidth(int $barWidth) /** * @param mixed $completeChar */ - public function setCompleteChar(string $completeChar) + public function setCompleteChar($completeChar) { $this->completeChar = $completeChar; } @@ -427,7 +396,7 @@ public function setCompleteChar(string $completeChar) * Gets the complete bar character. * @return string A character */ - public function getCompleteChar(): string + public function getCompleteChar() { if (null === $this->completeChar) { return $this->maxSteps ? '=' : $this->completeChar; @@ -439,7 +408,7 @@ public function getCompleteChar(): string /** * @return string */ - public function getProgressChar(): string + public function getProgressChar() { return $this->progressChar; } @@ -447,7 +416,7 @@ public function getProgressChar(): string /** * @param string $progressChar */ - public function setProgressChar(string $progressChar) + public function setProgressChar($progressChar) { $this->progressChar = $progressChar; } @@ -455,7 +424,7 @@ public function setProgressChar(string $progressChar) /** * @return string */ - public function getRemainingChar(): string + public function getRemainingChar() { return $this->remainingChar; } @@ -463,7 +432,7 @@ public function getRemainingChar(): string /** * @param string $remainingChar */ - public function setRemainingChar(string $remainingChar) + public function setRemainingChar($remainingChar) { $this->remainingChar = $remainingChar; } @@ -495,7 +464,7 @@ public function getFinishTime() /** * @return string */ - public function getFormat(): string + public function getFormat() { return $this->format; } @@ -503,7 +472,7 @@ public function getFormat(): string /** * @param string $format */ - public function setFormat(string $format) + public function setFormat($format) { $this->format = $format; } @@ -514,80 +483,48 @@ public function setFormat(string $format) */ private static function loadDefaultParsers() { - return [ - 'bar' => function (self $bar) { - $completeBars = floor($bar->getMaxSteps()> 0 ? $bar->getPercent() * $bar->getBarWidth() : $bar->getProgress() % $bar->getBarWidth()); - $display = str_repeat($bar->getCompleteChar(), $completeBars); - - if ($completeBars < $bar->getBarWidth()) { - $emptyBars = $bar->getBarWidth() - $completeBars; - $display .= $bar->getProgressChar() . str_repeat($bar->getRemainingChar(), $emptyBars); - } - - return $display; - }, - 'elapsed' => function (self $bar) { - return Helper::formatTime(time() - $bar->getStartTime()); - }, - 'remaining' => function (self $bar) { - if (!$bar->getMaxSteps()) { - throw new \LogicException('Unable to display the remaining time if the maximum number of steps is not set.'); - } - - if (!$bar->getProgress()) { - $remaining = 0; - } else { - $remaining = round((time() - $bar->getStartTime()) / $bar->getProgress() * ($bar->getMaxSteps() - $bar->getProgress())); - } - - return Helper::formatTime($remaining); - }, - 'estimated' => function (self $bar) { - if (!$bar->getMaxSteps()) { - return 0; - // throw new \LogicException('Unable to display the estimated time if the maximum number of steps is not set.'); - } - - if (!$bar->getProgress()) { - $estimated = 0; - } else { - $estimated = round((time() - $bar->getStartTime()) / $bar->getProgress() * $bar->getMaxSteps()); - } - - return Helper::formatTime($estimated); - }, - 'memory' => function () { - return Helper::formatMemory(memory_get_usage(true)); - }, - 'current' => function (self $bar) { - return str_pad($bar->getProgress(), $bar->getStepWidth(), ' ', STR_PAD_LEFT); - }, - 'max' => function (self $bar) { - return $bar->getMaxSteps(); - }, - 'percent' => function (self $bar) { - return floor($bar->getPercent() * 100); - }, - ]; - } + return ['bar' => function (self $bar) { + $completeBars = floor($bar->getMaxSteps()> 0 ? $bar->getPercent() * $bar->getBarWidth() : $bar->getProgress() % $bar->getBarWidth()); + $display = str_repeat($bar->getCompleteChar(), $completeBars); + if ($completeBars < $bar->getBarWidth()) { + $emptyBars = $bar->getBarWidth() - $completeBars; + $display .= $bar->getProgressChar() . str_repeat($bar->getRemainingChar(), $emptyBars); + } - /** - * @return array - */ -// private static function defaultFormats() -// { -// return [ -// 'normal' => ' %current%/%max% [%bar%] %percent:3s%%', -// 'normal_nomax' => ' %current% [%bar%]', -// -// 'verbose' => ' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%', -// 'verbose_nomax' => ' %current% [%bar%] %elapsed:6s%', -// -// 'very_verbose' => ' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s%', -// 'very_verbose_nomax' => ' %current% [%bar%] %elapsed:6s%', -// -// 'debug' => ' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s% %memory:6s%', -// 'debug_nomax' => ' %current% [%bar%] %elapsed:6s% %memory:6s%', -// ]; -// } -} + return $display; + }, 'elapsed' => function (self $bar) { + return FormatUtil::timestamp(time() - $bar->getStartTime()); + }, 'remaining' => function (self $bar) { + if (!$bar->getMaxSteps()) { + throw new \LogicException('Unable to display the remaining time if the maximum number of steps is not set.'); + } + if (!$bar->getProgress()) { + $remaining = 0; + } else { + $remaining = round((time() - $bar->getStartTime()) / $bar->getProgress() * ($bar->getMaxSteps() - $bar->getProgress())); + } + + return FormatUtil::timestamp($remaining); + }, 'estimated' => function (self $bar) { + if (!$bar->getMaxSteps()) { + return 0; + // throw new \LogicException('Unable to display the estimated time if the maximum number of steps is not set.'); + } + if (!$bar->getProgress()) { + $estimated = 0; + } else { + $estimated = round((time() - $bar->getStartTime()) / $bar->getProgress() * $bar->getMaxSteps()); + } + + return FormatUtil::timestamp($estimated); + }, 'memory' => function () { + return FormatUtil::memoryUsage(memory_get_usage(true)); + }, 'current' => function (self $bar) { + return str_pad($bar->getProgress(), $bar->getStepWidth(), ' ', STR_PAD_LEFT); + }, 'max' => function (self $bar) { + return $bar->getMaxSteps(); + }, 'percent' => function (self $bar) { + return floor($bar->getPercent() * 100); + }]; + } +} \ No newline at end of file diff --git a/src/Utils/Show.php b/src/Utils/Show.php index 10ca1705..fd21c27d 100644 --- a/src/Utils/Show.php +++ b/src/Utils/Show.php @@ -1,4 +1,5 @@ hasStyle($style)) { $text = sprintf('<%s>%s', $style, $text, $style); } @@ -104,15 +97,12 @@ public static function block($messages, $type = 'MESSAGE', $style = Style::NORMA public static function liteBlock($messages, $type = 'MESSAGE', $style = Style::NORMAL, $quit = false) { $messages = \is_array($messages) ? array_values($messages) : array($messages); - // add type if (null !== $type) { $type = sprintf('[%s]', strtoupper($type)); } - $text = implode(PHP_EOL, $messages); $color = static::getStyle(); - if (\is_string($style) && $color->hasStyle($style)) { $type = sprintf('<%s>%s ', $style, $type, $style); } @@ -133,7 +123,6 @@ public static function liteBlock($messages, $type = 'MESSAGE', $style = Style::N 'warning' => 'warning', 'danger' => 'danger', 'error' => 'error', - // lite style 'liteInfo' => 'info', 'liteNote' => 'note', @@ -155,47 +144,34 @@ public static function __callStatic($method, array $args = []) { if (isset(self::$blockMethods[$method])) { $msg = $args[0]; - $quit = $args[1] ?? false; + $quit = isset($args[1]) ? $args[1] : false; $style = self::$blockMethods[$method]; - if (0 === strpos($method, 'lite')) { return self::liteBlock($msg, $style === 'primary' ? 'IMPORTANT' : $style, $style, $quit); } return self::block($msg, $style === 'primary' ? 'IMPORTANT' : $style, $style, $quit); } - - throw new \LogicException("Call a not exists method: $method"); + throw new \LogicException("Call a not exists method: {$method}"); } - /************************************************************************************************** * Output Format Message(section/list/helpPanel/panel/table) **************************************************************************************************/ - /** * @param string $title The title text * @param array $opts */ public static function title($title, array $opts = []) { - $opts = array_merge([ - 'width' => 80, - 'char' => self::CHAR_EQUAL, - 'titlePos' => self::POS_LEFT, - 'indent' => 2, - 'showBorder' => true, - ], $opts); - + $opts = array_merge(['width' => 80, 'char' => self::CHAR_EQUAL, 'titlePos' => self::POS_LEFT, 'indent' => 2, 'showBorder' => true], $opts); // list($sW, $sH) = Helper::getScreenSize(); $width = (int)$opts['width']; $char = trim($opts['char']); $indent = (int)$opts['indent']> 0 ? $opts['indent'] : 2; $indentStr = str_pad(self::CHAR_SPACE, $indent, self::CHAR_SPACE); - $title = ucwords(trim($title)); $tLength = Helper::strLen($title); $width = $width> 10 ? $width : 80; - // title position if ($tLength>= $width) { $titleIndent = str_pad(self::CHAR_SPACE, $indent, self::CHAR_SPACE); @@ -206,10 +182,8 @@ public static function title($title, array $opts = []) } else { $titleIndent = str_pad(self::CHAR_SPACE, $indent, self::CHAR_SPACE); } - - $titleLine = "$titleIndent$title\n"; + $titleLine = "{$titleIndent}{$title}\n"; $border = $indentStr . str_pad($char, $width, $char); - self::write($titleLine . $border); } @@ -220,25 +194,15 @@ public static function title($title, array $opts = []) */ public static function section($title, $body, array $opts = []) { - $opts = array_merge([ - 'width' => 80, - 'char' => self::CHAR_HYPHEN, - 'titlePos' => self::POS_LEFT, - 'indent' => 2, - 'topBorder' => true, - 'bottomBorder' => true, - ], $opts); - + $opts = array_merge(['width' => 80, 'char' => self::CHAR_HYPHEN, 'titlePos' => self::POS_LEFT, 'indent' => 2, 'topBorder' => true, 'bottomBorder' => true], $opts); // list($sW, $sH) = Helper::getScreenSize(); $width = (int)$opts['width']; $char = trim($opts['char']); $indent = (int)$opts['indent']> 0 ? $opts['indent'] : 2; $indentStr = str_pad(self::CHAR_SPACE, $indent, self::CHAR_SPACE); - $title = ucwords(trim($title)); $tLength = Helper::strLen($title); $width = $width> 10 ? $width : 80; - // title position if ($tLength>= $width) { $titleIndent = str_pad(self::CHAR_SPACE, $indent, self::CHAR_SPACE); @@ -249,29 +213,23 @@ public static function section($title, $body, array $opts = []) } else { $titleIndent = str_pad(self::CHAR_SPACE, $indent, self::CHAR_SPACE); } - - $tpl = "%s\n%s%s\n%s";// title topBorder body bottomBorder + $tpl = "%s\n%s%s\n%s"; + // title topBorder body bottomBorder $topBorder = $bottomBorder = ''; - $titleLine = "$titleIndent$title"; - + $titleLine = "{$titleIndent}{$title}"; $showTBorder = (bool)$opts['topBorder']; $showBBorder = (bool)$opts['bottomBorder']; - if ($showTBorder || $showBBorder) { $border = str_pad($char, $width, $char); - if ($showTBorder) { - $topBorder = "{$indentStr}$border\n"; + $topBorder = "{$indentStr}{$border}\n"; } - if ($showBBorder) { - $bottomBorder = "{$indentStr}$border\n"; + $bottomBorder = "{$indentStr}{$border}\n"; } } - $body = \is_array($body) ? implode(PHP_EOL, $body) : $body; - $body = Helper::wrapText($body, 4, $opts['width']); - + $body = FormatUtil::wrapText($body, 4, $opts['width']); self::write(sprintf($tpl, $titleLine, $topBorder, $body, $bottomBorder)); } @@ -287,28 +245,19 @@ public static function section($title, $body, array $opts = []) * @param string|null $title * @param array $opts */ - public static function padding(array $data, string $title = null, array $opts = []) + public static function padding(array $data, $title = null, array $opts = []) { if (!$data) { return; } - $string = $title ? Helper::wrapTag(ucfirst($title), 'comment') . ":\n" : ''; - $opts = array_merge([ - 'char' => '.', - 'indent' => ' ', - 'padding' => 10, - 'valueStyle' => 'info', - ], $opts); - + $opts = array_merge(['char' => '.', 'indent' => ' ', 'padding' => 10, 'valueStyle' => 'info'], $opts); $keyMaxLen = Helper::getKeyMaxWidth($data); $paddingLen = $keyMaxLen> $opts['padding'] ? $keyMaxLen : $opts['padding']; - foreach ($data as $label => $value) { $value = Helper::wrapTag((string)$value, $opts['valueStyle']); - $string .= $opts['indent'] . str_pad($label, $paddingLen, $opts['char']) . " $value\n"; + $string .= $opts['indent'] . str_pad($label, $paddingLen, $opts['char']) . " {$value}\n"; } - self::write(trim($string)); } @@ -323,7 +272,7 @@ public static function padding(array $data, string $title = null, array $opts = * ``` * @param array $data * @param string $title - * @param array $opts More @see Helper::spliceKeyValue() + * @param array $opts More {@see FormatUtil::spliceKeyValue()} * @return int|string */ public static function aList($data, $title = null, array $opts = []) @@ -331,26 +280,19 @@ public static function aList($data, $title = null, array $opts = []) $string = ''; $opts = array_merge([ 'leftChar' => ' ', + // 'sepChar' => ' ', 'keyStyle' => 'info', 'keyMinWidth' => 8, 'titleStyle' => 'comment', 'returned' => false, ], $opts); - // title if ($title) { $title = ucwords(trim($title)); - - if ($style = $opts['titleStyle']) { - $title = "<$style>$title"; - } - - $string .= $title . PHP_EOL; + $string .= Helper::wrapTag($title, $opts['titleStyle']) . PHP_EOL; } - // handle item list - $string .= Helper::spliceKeyValue((array)$data, $opts); - + $string .= FormatUtil::spliceKeyValue((array)$data, $opts); if ($opts['returned']) { return $string; } @@ -399,12 +341,10 @@ public static function mList(array $data, array $opts = []) { $buffer = []; $opts['returned'] = true; - foreach ($data as $title => $list) { $buffer[] = self::aList($list, $title, $opts); } - - self::write($buffer); + self::write(implode("\n", $buffer)); } /** @@ -440,59 +380,53 @@ public static function mList(array $data, array $opts = []) */ public static function helpPanel(array $config, $showAfterQuit = true) { - $help = ''; + $parts = []; + $option = ['indentDes' => ' ']; $config = array_merge([ 'description' => '', 'usage' => '', - 'commands' => [], 'arguments' => [], 'options' => [], - 'examples' => [], - // extra 'extras' => [], + '_opts' => [], ], $config); - + // some option for show. + if (isset($config['_opts'])) { + $option = array_merge($option, $config['_opts']); + unset($config['_opts']); + } // description if ($config['description']) { - $help .= " {$config['description']}\n\n"; + $parts[] = "{$option['indentDes']}{$config['description']}\n"; unset($config['description']); } - // now, render usage,commands,arguments,options,examples ... foreach ($config as $section => $value) { if (!$value) { continue; } - // if $value is array, translate array to string if (\is_array($value)) { // is natural key ['text1', 'text2'](like usage,examples) if (isset($value[0])) { $value = implode(PHP_EOL . ' ', $value); - // is key-value [ 'key1' => 'text1', 'key2' => 'text2'] } else { - $value = Helper::spliceKeyValue($value, [ - 'leftChar' => ' ', - 'keyStyle' => 'info', - ]); + $value = FormatUtil::spliceKeyValue($value, ['leftChar' => ' ', 'sepChar' => ' ', 'keyStyle' => 'info']); } } - if (\is_string($value)) { $value = trim($value); $section = ucfirst($section); - $help .= "$section:\n {$value}\n\n"; + $parts[] = "{$section}:\n {$value}\n"; } } - - if ($help) { - self::write($help, false); + if ($parts) { + self::write(implode("\n", $parts), false); } - if ($showAfterQuit) { exit(0); } @@ -505,38 +439,32 @@ public static function helpPanel(array $config, $showAfterQuit = true) * @param array $opts * @return int */ - public static function panel($data, $title = 'Information Panel', array $opts = []): int + public static function panel($data, $title = 'Information Panel', array $opts = []) { if (!$data) { self::write('No data to display!'); return -404; } - - $opts = array_merge([ - 'borderChar' => '*', - 'ucFirst' => true, - ], $opts); - + $opts = array_merge(['borderChar' => '*', 'ucFirst' => true], $opts); $borderChar = $opts['borderChar']; $data = \is_array($data) ? array_filter($data) : [trim($data)]; $title = trim($title); - - $panelData = []; // [ 'label' => 'value' ] - $labelMaxWidth = 0; // if label exists, label max width - $valueMaxWidth = 0; // value max width - + $panelData = []; + // [ 'label' => 'value' ] + $labelMaxWidth = 0; + // if label exists, label max width + $valueMaxWidth = 0; + // value max width foreach ($data as $label => $value) { // label exists if (!is_numeric($label)) { $width = mb_strlen($label, 'UTF-8'); $labelMaxWidth = $width> $labelMaxWidth ? $width : $labelMaxWidth; } - // translate array to string if (\is_array($value)) { $temp = ''; - /** @var array $value */ foreach ($value as $key => $val) { if (\is_bool($val)) { @@ -544,29 +472,27 @@ public static function panel($data, $title = 'Information Panel', array $opts = } else { $val = (string)$val; } - - $temp .= (!is_numeric($key) ? "$key: " : '') . "$val, "; + $temp .= (!is_numeric($key) ? "{$key}: " : '') . "{$val}, "; } - $value = rtrim($temp, ' ,'); - } else if (\is_bool($value)) { - $value = $value ? 'True' : 'False'; } else { - $value = trim((string)$value); + if (\is_bool($value)) { + $value = $value ? 'True' : 'False'; + } else { + $value = trim((string)$value); + } } - // get value width /** @var string $value */ $value = trim($value); - $width = mb_strlen(strip_tags($value), 'UTF-8'); // must clear style tag + $width = mb_strlen(strip_tags($value), 'UTF-8'); + // must clear style tag $valueMaxWidth = $width> $valueMaxWidth ? $width : $valueMaxWidth; - $panelData[$label] = $value; } - $border = null; $panelWidth = $labelMaxWidth + $valueMaxWidth; - + self::startBuffer(); // output title if ($title) { $title = ucwords($title); @@ -575,34 +501,34 @@ public static function panel($data, $title = 'Information Panel', array $opts = $indentSpace = str_pad(' ', ceil($panelWidth / 2) - ceil($titleLength / 2) + 2 * 2, ' '); self::write(" {$indentSpace}{$title}"); } - // output panel top border if ($borderChar) { - $border = str_pad($borderChar, $panelWidth + (3 * 3), $borderChar); + $border = str_pad($borderChar, $panelWidth + 3 * 3, $borderChar); self::write(' ' . $border); } - // output panel body - $panelStr = Helper::spliceKeyValue($panelData, [ - 'leftChar' => " $borderChar ", - 'sepChar' => ' | ', - 'keyMaxWidth' => $labelMaxWidth, - 'ucFirst' => $opts['ucFirst'], - ]); - + $panelStr = FormatUtil::spliceKeyValue($panelData, ['leftChar' => " {$borderChar} ", 'sepChar' => ' | ', 'keyMaxWidth' => $labelMaxWidth, 'ucFirst' => $opts['ucFirst']]); // already exists "\n" self::write($panelStr, false); - // output panel bottom border if ($border) { - self::write(" $border\n"); + self::write(" {$border}\n"); } - + self::flushBuffer(); unset($panelData); return 0; } + /** + * @todo un-completed + * @param array $data + * @param array $opts + */ + public static function tree(array $data, array $opts = []) + { + } + /** * 表格数据信息展示 * @param array $data @@ -631,12 +557,11 @@ public static function panel($data, $title = 'Information Panel', array $opts = * ``` * @return int */ - public static function table(array $data, $title = 'Data Table', array $opts = []): int + public static function table(array $data, $title = 'Data Table', array $opts = []) { if (!$data) { return -404; } - $buf = new StrBuffer(); $opts = array_merge([ 'showBorder' => true, @@ -644,52 +569,50 @@ public static function table(array $data, $title = 'Data Table', array $opts = [ 'titlePos' => self::POS_LEFT, 'titleStyle' => 'bold', 'headStyle' => 'comment', - 'headBorderChar' => self::CHAR_EQUAL, // default is '=' + 'headBorderChar' => self::CHAR_EQUAL, + // default is '=' 'bodyStyle' => '', - 'rowBorderChar' => self::CHAR_HYPHEN, // default is '-' - 'colBorderChar' => self::CHAR_VERTICAL, // default is '|' - 'columns' => [], // custom column names + 'rowBorderChar' => self::CHAR_HYPHEN, + // default is '-' + 'colBorderChar' => self::CHAR_VERTICAL, + // default is '|' + 'columns' => [], ], $opts); - $hasHead = false; $rowIndex = 0; - $head = $table = []; + $head = []; $tableHead = $opts['columns']; $leftIndent = $opts['leftIndent']; $showBorder = $opts['showBorder']; $rowBorderChar = $opts['rowBorderChar']; $colBorderChar = $opts['colBorderChar']; - $info = [ 'rowCount' => \count($data), - 'columnCount' => 0, // how many column in the table. - 'columnMaxWidth' => [], // table column max width - 'tableWidth' => 0, // table width. equals to all max column width's sum. + 'columnCount' => 0, + // how many column in the table. + 'columnMaxWidth' => [], + // table column max width + 'tableWidth' => 0, ]; - // parse table data foreach ($data as $row) { // collection all field name if ($rowIndex === 0) { $head = $tableHead ?: array_keys($row); $info['columnCount'] = \count($row); - foreach ($head as $index => $name) { - if (\is_string($name)) {// maybe no column name. + if (\is_string($name)) { + // maybe no column name. $hasHead = true; } - $info['columnMaxWidth'][$index] = mb_strlen($name, 'UTF-8'); } } - $colIndex = 0; - foreach ((array)$row as $value) { // collection column max width if (isset($info['columnMaxWidth'][$colIndex])) { $colWidth = mb_strlen($value, 'UTF-8'); - // If current column width gt old column width. override old width. if ($colWidth> $info['columnMaxWidth'][$colIndex]) { $info['columnMaxWidth'][$colIndex] = $colWidth; @@ -697,61 +620,49 @@ public static function table(array $data, $title = 'Data Table', array $opts = [ } else { $info['columnMaxWidth'][$colIndex] = mb_strlen($value, 'UTF-8'); } - $colIndex++; } - $rowIndex++; } - $tableWidth = $info['tableWidth'] = array_sum($info['columnMaxWidth']); $columnCount = $info['columnCount']; - // output title if ($title) { $tStyle = $opts['titleStyle'] ?: 'bold'; $title = ucwords(trim($title)); $titleLength = mb_strlen($title, 'UTF-8'); - $indentSpace = str_pad(' ', ceil($tableWidth / 2) - ceil($titleLength / 2) + ($columnCount * 2), ' '); - $buf->write(" {$indentSpace}<$tstyle>{$title}\n"); + $indentSpace = str_pad(' ', ceil($tableWidth / 2) - ceil($titleLength / 2) + $columnCount * 2, ' '); + $buf->write(" {$indentSpace}<{$tstyle}>{$title}\n"); } - - $border = $leftIndent . str_pad($rowBorderChar, $tableWidth + ($columnCount * 3) + 2, $rowBorderChar); - + $border = $leftIndent . str_pad($rowBorderChar, $tableWidth + $columnCount * 3 + 2, $rowBorderChar); // output table top border if ($showBorder) { $buf->write($border . "\n"); } else { - $colBorderChar = '';// clear column border char + $colBorderChar = ''; + // clear column border char } - // output table head if ($hasHead) { $headStr = "{$leftIndent}{$colBorderChar} "; - foreach ($head as $index => $name) { $colMaxWidth = $info['columnMaxWidth'][$index]; $name = str_pad($name, $colMaxWidth, ' '); $name = Helper::wrapTag($name, $opts['headStyle']); $headStr .= " {$name} {$colBorderChar}"; } - $buf->write($headStr . "\n"); - // head border: split head and body if ($headBorderChar = $opts['headBorderChar']) { - $headBorder = $leftIndent . str_pad($headBorderChar, $tableWidth + ($columnCount * 3) + 2, $headBorderChar); + $headBorder = $leftIndent . str_pad($headBorderChar, $tableWidth + $columnCount * 3 + 2, $headBorderChar); $buf->write($headBorder . "\n"); } } - $rowIndex = 0; - // output table info foreach ($data as $row) { $colIndex = 0; - $rowStr = " $colBorderChar "; - + $rowStr = " {$colBorderChar} "; foreach ((array)$row as $value) { $colMaxWidth = $info['columnMaxWidth'][$colIndex]; $value = str_pad($value, $colMaxWidth, ' '); @@ -759,21 +670,100 @@ public static function table(array $data, $title = 'Data Table', array $opts = [ $rowStr .= " {$value} {$colBorderChar}"; $colIndex++; } - $buf->write($rowStr . "\n"); - $rowIndex++; } - // output table bottom border if ($showBorder) { $buf->write($border . "\n"); } - self::write($buf); return 0; } + /*********************************************************************************** + * Output progress message + ***********************************************************************************/ + /** + * show a spinner icon message + * ```php + * $total = 5000; + * while ($total--) { + * Show::spinner(); + * usleep(100); + * } + * Show::spinner('Done', true); + * ``` + * @param string $msg + * @param bool $ended + */ + public static function spinner($msg = '', $ended = false) + { + static $chars = '-\\|/'; + static $counter = 0; + static $lastTime = null; + $tpl = (Helper::supportColor() ? "\r33円[2K" : "\r\r") . '%s'; + if ($ended) { + printf($tpl, $msg); + + return; + } + $now = microtime(true); + if (null === $lastTime || $lastTime < $now - 0.1) { + $lastTime = $now; + // echo $chars[$counter]; + printf($tpl, $chars[$counter] . $msg); + $counter++; + if ($counter> \strlen($chars) - 1) { + $counter = 0; + } + } + } + + /** + * alias of the pending() + * @param string $msg + * @param bool $ended + */ + public static function loading($msg = 'Loading ', $ended = false) + { + self::pending($msg, $ended); + } + + /** + * show a pending message + * ```php + * $total = 8000; + * while ($total--) { + * Show::pending(); + * usleep(200); + * } + * Show::pending('Done', true); + * ``` + * @param string $msg + * @param bool $ended + */ + public static function pending($msg = 'Pending ', $ended = false) + { + static $counter = 0; + static $lastTime = null; + static $chars = ['', '.', '..', '...']; + $tpl = (Helper::supportColor() ? "\r33円[2K" : "\r\r") . '%s'; + if ($ended) { + printf($tpl, $msg); + + return; + } + $now = microtime(true); + if (null === $lastTime || $lastTime < $now - 0.8) { + $lastTime = $now; + printf($tpl, $msg . $chars[$counter]); + $counter++; + if ($counter> \count($chars) - 1) { + $counter = 0; + } + } + } /** * 与文本进度条相比,没有 total @@ -785,17 +775,14 @@ public static function counterTxt($msg, $doneMsg = null) { $counter = 0; $finished = false; - $tpl = (Helper::isSupportColor() ? "\x0D\x1B[2K" : "\x0D\r") . '%d %s'; + $tpl = (Helper::supportColor() ? "\r33円[2K" : "\r\r") . '%d %s'; $msg = self::getStyle()->render($msg); $doneMsg = $doneMsg ? self::getStyle()->render($doneMsg) : null; - while (true) { if ($finished) { return; } - $step = yield; - if ((int)$step === self::FINISHED) { $counter++; $finished = true; @@ -804,22 +791,15 @@ public static function counterTxt($msg, $doneMsg = null) if ((int)$step <= 0) { $step = 1; } - $counter += $step; } - - // printf("\r%d%% %s", $percent, $msg); - // printf("\x0D\x2K %d%% %s", $percent, $msg); - // printf("\x0D\r%'2d%% %s", $percent, $msg); printf($tpl, $counter, $msg); - if ($finished) { echo "\n"; break; } } - - yield false; + (yield false); } /** @@ -832,42 +812,34 @@ public static function progressTxt($total, $msg, $doneMsg = null) { $current = 0; $finished = false; - $tpl = (Helper::isSupportColor() ? "\x0D\x1B[2K" : "\x0D\r") . "%' 3d%% %s"; + $tpl = (Helper::supportColor() ? "\r33円[2K" : "\r\r") . "%' 3d%% %s"; $msg = self::getStyle()->render($msg); $doneMsg = $doneMsg ? self::getStyle()->render($doneMsg) : null; - while (true) { if ($finished) { return; } - $step = yield; - if ((int)$step <= 0) { $step = 1; } - $current += $step; - $percent = ceil(($current / $total) * 100); - + $percent = ceil($current / $total * 100); if ($percent>= 100) { $percent = 100; $finished = true; $msg = $doneMsg ?: $msg; } - // printf("\r%d%% %s", $percent, $msg); // printf("\x0D\x2K %d%% %s", $percent, $msg); // printf("\x0D\r%'2d%% %s", $percent, $msg); printf($tpl, $percent, $msg); - if ($finished) { echo "\n"; break; } } - - yield false; + (yield false); } /** @@ -895,39 +867,26 @@ public static function progressBar($total, array $opts = []) { $current = 0; $finished = false; - $tplPrefix = Helper::isSupportColor() ? "\x0D\x1B[2K" : "\x0D\r"; - $opts = array_merge([ - 'doneChar' => '=', - 'waitChar' => ' ', - 'signChar' => '>', - 'msg' => '', - 'doneMsg' => '', - ], $opts); - + $tplPrefix = Helper::supportColor() ? "\r33円[2K" : "\r\r"; + $opts = array_merge(['doneChar' => '=', 'waitChar' => ' ', 'signChar' => '>', 'msg' => '', 'doneMsg' => ''], $opts); $msg = self::getStyle()->render($opts['msg']); $doneMsg = self::getStyle()->render($opts['doneMsg']); $waitChar = $opts['waitChar']; - while (true) { if ($finished) { return; } - $step = yield; - if ((int)$step <= 0) { $step = 1; } - $current += $step; - $percent = ceil(($current / $total) * 100); - + $percent = ceil($current / $total * 100); if ($percent>= 100) { $msg = $doneMsg ?: $msg; $percent = 100; $finished = true; } - /** * \r, \x0D 回车,到行首 * \x1B ESC @@ -935,24 +894,18 @@ public static function progressBar($total, array $opts = []) */ // printf("\r[%'--100s] %d%% %s", // printf("\x0D\x1B[2K[%'{$waitChar}-100s] %d%% %s", - printf("{$tplPrefix}[%'{$waitChar}-100s] %' 3d%% %s", - str_repeat($opts['doneChar'], $percent) . ($finished ? '' : $opts['signChar']), - $percent, - $msg - );// ♥ しかく ☺ ☻ = # - + printf("{$tplPrefix}[%'{$waitChar}-100s] %' 3d%% %s", str_repeat($opts['doneChar'], $percent) . ($finished ? '' : $opts['signChar']), $percent, $msg); + // ♥ しかく ☺ ☻ = # if ($finished) { echo "\n"; break; } } - - yield false; + (yield false); } /** * create ProgressBar - * * ```php * $max = 200; * $bar = Show::createProgressBar($max); @@ -963,7 +916,6 @@ public static function progressBar($total, array $opts = []) * } * $bar->finish(); * ``` - * * @param int $max * @param bool $start * @return ProgressBar @@ -972,22 +924,97 @@ public static function progressBar($total, array $opts = []) public static function createProgressBar($max = 0, $start = true) { $bar = new ProgressBar(null, $max); - if ($start) { $bar->start(); } return $bar; } + /*********************************************************************************** + * Output buffer + ***********************************************************************************/ + /** + * @return bool + */ + public static function isBuffering() + { + return self::$buffering; + } + + /** + * @return string + */ + public static function getBuffer() + { + return self::$buffer; + } + + /** + * @param string $buffer + */ + public static function setBuffer($buffer) + { + self::$buffer = $buffer; + } + + /** + * start buffering + */ + public static function startBuffer() + { + self::$buffering = true; + } + + /** + * start buffering + */ + public static function clearBuffer() + { + self::$buffer = null; + } + + /** + * stop buffering + * @see Show::write() + * @param bool $flush Whether flush buffer to output stream + * @param bool $nl Default is False, because the last write() have been added "\n" + * @param bool $quit + * @param array $opts + * @return null|string If flush = False, will return all buffer text. + */ + public static function stopBuffer($flush = true, $nl = false, $quit = false, array $opts = []) + { + self::$buffering = false; + if ($flush && self::$buffer) { + // all text have been rendered by Style::render() in every write(); + $opts['color'] = false; + // flush to stream + self::write(self::$buffer, $nl, $quit, $opts); + // clear buffer + self::$buffer = null; + } -///////////////////////////////////////////////////////////////// -/// Helper Method -///////////////////////////////////////////////////////////////// + return self::$buffer; + } + /** + * stop buffering and flush buffer text + * @see Show::write() + * @param bool $nl + * @param bool $quit + * @param array $opts + */ + public static function flushBuffer($nl = false, $quit = false, array $opts = []) + { + self::stopBuffer(true, $nl, $quit, $opts); + } + /*********************************************************************************** + * Helper methods + ***********************************************************************************/ /** * @return Style */ - public static function getStyle(): Style + public static function getStyle() { return Style::create(); } @@ -1001,30 +1028,35 @@ public static function getStyle(): Style * [ * 'color' => bool, // whether render color, default is: True. * 'stream' => resource, // the stream resource, default is: STDOUT - * 'flush' => flush, // flush the stream data, default is: True + * 'flush' => bool, // flush the stream data, default is: True * ] * @return int */ - public static function write($messages, $nl = true, $quit = false, array $opts = []): int + public static function write($messages, $nl = true, $quit = false, array $opts = []) { if (\is_array($messages)) { $messages = implode($nl ? PHP_EOL : '', $messages); } - $messages = (string)$messages; - if (!isset($opts['color']) || $opts['color']) { $messages = static::getStyle()->render($messages); } - - $stream = $opts['stream'] ?? STDOUT; - - fwrite($stream, $messages . ($nl ? PHP_EOL : '')); - + // if open buffering + if (self::isBuffering()) { + self::$buffer .= $messages . ($nl ? PHP_EOL : ''); + if (!$quit) { + return 0; + } + // if will quit. + $messages = self::$buffer; + self::clearBuffer(); + } else { + $messages .= $nl ? PHP_EOL : ''; + } + fwrite($stream = isset($opts['stream']) ? $opts['stream'] : \STDOUT, $messages); if (!isset($opts['flush']) || $opts['flush']) { fflush($stream); } - if ($quit !== false) { $code = true === $quit ? 0 : (int)$quit; exit($code); @@ -1033,6 +1065,21 @@ public static function write($messages, $nl = true, $quit = false, array $opts = return 0; } + /** + * write raw data to stdout + * @param string|array $text + * @param bool $nl + * @param bool|int $quit + * @param array $opts + * @return int + */ + public static function writeRaw($text, $nl = true, $quit = false, array $opts = []) + { + $opts['color'] = false; + + return self::write($text, $nl, $quit, $opts); + } + /** * Logs data to stdout * @param string|array $text @@ -1064,17 +1111,15 @@ public static function stdout($text, $nl = true, $quit = false) */ public static function stderr($text, $nl = true, $quit = -200) { - self::write($text, $nl, $quit, [ - 'stream' => STDERR, - ]); + self::write($text, $nl, $quit, ['stream' => STDERR]); } /** * @param bool $onlyKey * @return array */ - public static function getBlockMethods($onlyKey = true): array + public static function getBlockMethods($onlyKey = true) { return $onlyKey ? array_keys(self::$blockMethods) : self::$blockMethods; } -} +} \ No newline at end of file diff --git a/tests/Components/TextTemplateTest.php b/tests/Components/TextTemplateTest.php new file mode 100644 index 00000000..9f1e364a --- /dev/null +++ b/tests/Components/TextTemplateTest.php @@ -0,0 +1,45 @@ + 'test', + 'date' => $date, + 'map' => [ + 'VAL0', + 'key1' => 'VAL1', + ], + ]); + + $ret = $tt->render($tpl); + $this->assertNotEmpty($ret); + $this->assertTrue((bool)strpos($ret, $date)); + $this->assertTrue((bool)strpos($ret, 'VAL0')); + $this->assertStringEndsWith('VAL1', $ret); + } +} diff --git a/tests/boot.php b/tests/boot.php index 25657855..567adcbd 100644 --- a/tests/boot.php +++ b/tests/boot.php @@ -1,6 +1,28 @@

AltStyle によって変換されたページ (->オリジナル) /