diff --git a/README.md b/README.md index 2637924..b2262f4 100644 --- a/README.md +++ b/README.md @@ -4,16 +4,16 @@ [![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 @@ -34,12 +34,20 @@ ## 安装 -- 使用 composer +- 使用 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 ``` @@ -84,10 +92,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 +114,7 @@ $app->command('demo', function (Input $in, Output $out) { }, 'this is message for the command'); ``` -### 继承 `Inhere\Console\Command` +### 独立命令 通过继承 `Inhere\Console\Command` 添加独立命令 @@ -155,7 +165,7 @@ $app->command(TestCommand::class); // $app->command('test1', TestCommand::class); ``` -### 继承 `Inhere\Console\Controller` +### 命令组 通过继承 `Inhere\Console\Controller` 添加一组命令. 即是命令行的控制器 @@ -171,13 +181,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 +218,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 +387,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 +442,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 +477,9 @@ $data = [ Show::aList($data, $title); ``` -> 渲染效果请看下面的预览 +> 渲染效果 + +![fmt-list](docs/screenshots/fmt-list.png) ### 多列表数据展示输出 @@ -467,7 +508,9 @@ $data = [ Show::mList($data); ``` -> 渲染效果请看下面的预览 +> 渲染效果 + +![fmt-multi-list](docs/screenshots/fmt-multi-list.png) ### 面板展示信息输出 @@ -489,7 +532,9 @@ $data = [ Show::panel($data, 'panel show', '#'); ``` -> 渲染效果请看下面的预览 +> 渲染效果 + +![fmt-panel](docs/screenshots/fmt-panel.png) ### 数据表格信息输出 @@ -531,7 +576,7 @@ Show::table($data, 'a table', $opts); > 渲染效果请看下面的预览 -![table-show](images/table-show.png) +![table-show](docs/screenshots/table-show.png) ### 快速的渲染一个帮助信息面板 @@ -558,9 +603,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 15352c3..d86d303 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 @@ -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/docs/catelog.md b/docs/catelog.md new file mode 100644 index 0000000..f9e9bef --- /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 0000000..c9cb977 --- /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 0000000..6a66f06 --- /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 0000000..5b78818 --- /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 0000000..05e9d68 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 0000000..660c068 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 0000000..a3658af 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 0000000..5facf09 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 0000000..f89f37d 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 0000000..7bcd6b1 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 0000000..c5c5216 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 0000000..a988f8d 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 0000000..88599d6 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 0000000..0a1cb72 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 0000000..61b7f31 --- /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 0000000..4bd4748 --- /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 9ae6ab5..83a9517 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 03e93a8..76a4bf9 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 f9f56e1..113e9b5 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 0000000..162c12f --- /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 b174373..13ed035 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 fa34bd3..2461130 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 4c3db46..5e62115 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 e988fc2..95f6087 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 6b5f5cc..5d1fd80 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 8f42815..0000000 --- a/examples/s-autoload.php +++ /dev/null @@ -1,31 +0,0 @@ -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,6 +73,7 @@ public function addController($name, $class = null) /** * @param array $controllers + * @throws \InvalidArgumentException */ public function controllers(array $controllers) { @@ -72,11 +84,11 @@ 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($name, $handler = null, $description = null) + public function command($name, $handler = null, $option = null) { if (!$handler && class_exists($name)) { /** @var Command $name */ @@ -102,8 +114,15 @@ public function command($name, $handler = null, $description = null) } // 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; @@ -111,6 +130,7 @@ public function command($name, $handler = null, $description = null) /** * @param array $commands + * @throws \InvalidArgumentException */ public function commands(array $commands) { @@ -208,34 +228,29 @@ protected function getFileFilter() */ 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); @@ -261,6 +276,11 @@ public function runCommand($name, $believable = false) /** @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)) { diff --git a/src/Base/AbstractApplication.php b/src/Base/AbstractApplication.php index 3bf531f..c4ced37 100644 --- a/src/Base/AbstractApplication.php +++ b/src/Base/AbstractApplication.php @@ -12,8 +12,9 @@ use Inhere\Console\IO\Input; use Inhere\Console\IO\Output; use Inhere\Console\Style\Style; -use Inhere\Console\Traits\InputOutputTrait; +use Inhere\Console\Traits\InputOutputAwareTrait; use Inhere\Console\Traits\SimpleEventTrait; +use Inhere\Console\Utils\FormatUtil; use Inhere\Console\Utils\Helper; /** @@ -22,21 +23,17 @@ */ abstract class AbstractApplication implements ApplicationInterface { - use InputOutputTrait, SimpleEventTrait; - /** - * @var array - */ + 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 - */ + /** @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', @@ -47,20 +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 = []; + /** @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. @@ -151,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); } @@ -295,7 +296,7 @@ public function showHelpInfo($quit = true, $command = null) } $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); } /** @@ -304,14 +305,18 @@ public function showHelpInfo($quit = true, $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(); } @@ -326,20 +331,23 @@ public function showCommandList($quit = true) $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; @@ -347,20 +355,16 @@ public function showCommandList($quit = true) /** @var AbstractCommand $command */ if (is_subclass_of($command, CommandInterface::class)) { $desc = $command::getDescription() ?: $desPlaceholder; - } else { - if ($msg = $this->getCommandMessage($name)) { - $desc = $msg; - } else { - if (\is_string($command)) { - $desc = 'A handler : ' . $command; - } else { - if (\is_object($command)) { - $desc = 'A handler by ' . \get_class($command); - } - } - } + } elseif ($msg = $this->getCommandMessage($name)) { + $desc = $msg; + } elseif (\is_string($command)) { + $desc = 'A handler : ' . $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)'; @@ -368,21 +372,9 @@ public function showCommandList($quit = true) // 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), - ]); - // $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"); $quit && $this->stop(); @@ -401,11 +393,43 @@ public function getCommandMessage($name, $default = null) /** * @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 @@ -428,6 +452,7 @@ public function getCommandNames() /** * @param array $controllers + * @throws \InvalidArgumentException */ public function setControllers(array $controllers) { @@ -459,6 +484,7 @@ public function isController($name) /** * @param array $commands + * @throws \InvalidArgumentException */ public function setCommands(array $commands) { @@ -488,6 +514,50 @@ 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 */ @@ -572,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 aab4def..ff7347c 100644 --- a/src/Base/AbstractCommand.php +++ b/src/Base/AbstractCommand.php @@ -13,18 +13,19 @@ use Inhere\Console\IO\Input; use Inhere\Console\IO\InputDefinition; use Inhere\Console\IO\Output; -use Inhere\Console\Traits\InputOutputTrait; -use Inhere\Console\Traits\UserInteractTrait; +use Inhere\Console\Traits\InputOutputAwareTrait; +use Inhere\Console\Traits\UserInteractAwareTrait; use Inhere\Console\Utils\Annotation; /** * Class AbstractCommand * @package Inhere\Console */ -abstract class AbstractCommand implements CommandInterface +abstract class AbstractCommand implements BaseCommandInterface { - use InputOutputTrait, UserInteractTrait; - // name -> {$name} + use InputOutputAwareTrait, UserInteractAwareTrait; + const OK = 0; + // name -> {name} const ANNOTATION_VAR = '{%s}'; // '{$%s}'; /** @@ -56,6 +57,8 @@ abstract class AbstractCommand implements CommandInterface private $definition; /** @var string */ private $processTitle; + /** @var array */ + private $annotationVars; /** * Command constructor. @@ -71,6 +74,7 @@ public function __construct(Input $input, Output $output, InputDefinition $defin $this->definition = $definition; } $this->init(); + $this->annotationVars = $this->annotationVars(); } protected function init() @@ -104,8 +108,16 @@ protected function createDefinition() */ public function annotationVars() { - // e.g: `more info see {name}/index` - return ['script' => $this->input->getScript(), 'command' => $this->input->getCommand(), 'fullCommand' => $this->input->getScript() . ' ' . $this->input->getCommand(), 'name' => self::getName()]; + // 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(), + ]; } /************************************************************************** * running a command @@ -122,6 +134,7 @@ public function run($command = '') if ($this->input->sameOpt(['h', 'help'])) { return $this->showHelp(); } + // some prepare check if (true !== $this->prepare()) { return -1; } @@ -164,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; } @@ -177,17 +193,16 @@ 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')) { @@ -268,29 +283,65 @@ public function validateInput() * 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(); @@ -306,25 +357,20 @@ protected function showHelpByMethodAnnotation($method, $action = null) 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; } @@ -371,6 +417,16 @@ 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 @@ -396,6 +452,14 @@ public function setDefinition(InputDefinition $definition) $this->definition = $definition; } + /** + * @return array + */ + public function getAnnotationVars() + { + return $this->annotationVars; + } + /** * @return ApplicationInterface */ diff --git a/src/Base/ApplicationInterface.php b/src/Base/ApplicationInterface.php index fe7cea8..ed6be6a 100644 --- a/src/Base/ApplicationInterface.php +++ b/src/Base/ApplicationInterface.php @@ -47,9 +47,33 @@ public function runCommand($name, $believable = false); */ public function runAction($name, $action, $believable = false, $standAlone = false); - public function controller($name, $controller = null); + /** + * Register a app group command(by controller) + * @param string $name The controller name + * @param string $class The controller class + * @param null|array|string $option + * string: define the description message. + * array: + * - aliases The command aliases + * - description The description message + * @return static + * @throws \InvalidArgumentException + */ + public function controller($name, $class = null, $option = null); - public function command($name, $handler = null, $description = null); + /** + * Register a app independent console command + * @param string|CommandInterface $name + * @param string|\Closure|CommandInterface $handler + * @param null|array|string $option + * string: define the description message. + * array: + * - aliases The command aliases + * - description The description message + * @return $this + * @throws \InvalidArgumentException + */ + public function command($name, $handler = null, $option = null); public function showCommandList($quit = true); } \ No newline at end of file diff --git a/src/Base/BaseCommandInterface.php b/src/Base/BaseCommandInterface.php new file mode 100644 index 0000000..13535cd --- /dev/null +++ b/src/Base/BaseCommandInterface.php @@ -0,0 +1,46 @@ +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 0000000..a546a84 --- /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 0000000..a6b85aa --- /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 90% rename from src/Components/AutoCompletion.php rename to src/Components/AutoComplete/AutoCompletion.php index 2add048..363de33 100644 --- a/src/Components/AutoCompletion.php +++ b/src/Components/AutoComplete/AutoCompletion.php @@ -7,12 +7,12 @@ * Time: 17:56 */ -namespace Inhere\Console\Components; +namespace Inhere\Console\Components\AutoComplete; /** * Class AutoCompletion - a simple command auto-completion tool * @todo not available - * @package Inhere\Console\Components + * @package Inhere\Console\Components\AutoComplete */ class AutoCompletion { @@ -54,9 +54,9 @@ public function register() */ public function completionHandler($input, $index) { - $info = readline_info(); - $line = substr($info['line_buffer'], 0, $info['end']); - $tokens = token_get_all('data; diff --git a/src/Components/AutoComplete/ScriptGenerator.php b/src/Components/AutoComplete/ScriptGenerator.php new file mode 100644 index 0000000..c8ed78f --- /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 86% rename from src/Utils/Download.php rename to src/Components/Download.php index a04dc2b..e23444f 100644 --- a/src/Utils/Download.php +++ b/src/Components/Download.php @@ -7,13 +7,15 @@ * Time: 19:08 */ -namespace Inhere\Console\Utils; +namespace Inhere\Console\Components; + +use Inhere\Console\Utils\Show; /** * Class Download - * @package Inhere\Console\Utils + * @package Inhere\Console\Components */ -class Download +final class Download { const PROGRESS_TEXT = 'text'; const PROGRESS_BAR = 'bar'; @@ -61,10 +63,14 @@ public function __construct($url, $saveAs, $type = self::PROGRESS_TEXT) $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 @@ -75,22 +81,13 @@ public function start() 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 @@ -99,7 +96,7 @@ 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) { @@ -138,7 +135,7 @@ protected function progressShow($notifyCode, $severity, $message, $messageCode, * @param $transferredBytes * @return string */ - protected function showProgressByType($transferredBytes) + public function showProgressByType($transferredBytes) { if ($transferredBytes <= 0) { return ''; diff --git a/src/Components/ExecComparator.php b/src/Components/ExecComparator.php new file mode 100644 index 0000000..4ff1dcc --- /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 0000000..5cd197a --- /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/Progress/Bar.php b/src/Components/Progress/Bar.php new file mode 100644 index 0000000..0379887 --- /dev/null +++ b/src/Components/Progress/Bar.php @@ -0,0 +1,18 @@ +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 95% rename from src/Utils/AnsiCode.php rename to src/Components/Terminal.php index 9a9c723..ab22be3 100644 --- a/src/Utils/AnsiCode.php +++ b/src/Components/Terminal.php @@ -7,16 +7,18 @@ * Time: 9:35 */ -namespace Inhere\Console\Utils; +namespace Inhere\Console\Components; + +use Inhere\Console\Utils\Show; /** - * Class AnsiCode terminal - * @package Inhere\Console\Utils + * Class Terminal - terminal control by ansiCode + * @package Inhere\Console\Components * 2K 清除本行 * \x0D = \r = 13 回车,回到行首 * ESC = \x1B = 27 */ -final class AnsiCode +final class Terminal { const BEGIN_CHAR = "\33["; const END_CHAR = "\33[0m"; @@ -109,8 +111,8 @@ 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 diff --git a/src/Components/TextTemplate.php b/src/Components/TextTemplate.php new file mode 100644 index 0000000..8b5feed --- /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 f57ab1f..07ab70f 100644 --- a/src/Controller.php +++ b/src/Controller.php @@ -14,6 +14,7 @@ use Inhere\Console\IO\Input; use Inhere\Console\IO\Output; use Inhere\Console\Utils\Annotation; +use Inhere\Console\Utils\FormatUtil; use Inhere\Console\Utils\Helper; /** @@ -22,19 +23,30 @@ */ abstract class Controller extends AbstractCommand implements ControllerInterface { + /** @var array */ + private static $aliases; /** @var string */ private $action; /** @var string */ + private $delimiter = ':'; + // '/' ':' + /** @var bool */ + private $standAlone = false; + /** @var string */ private $defaultAction = 'help'; /** @var string */ private $actionSuffix = 'Command'; /** @var string */ protected $notFoundCallback = 'notFound'; - /** @var string */ - protected $delimiter = ':'; - // '/' ':' - /** @var bool */ - private $standAlone = false; + + /** + * define command alias map + * @return array + */ + protected static function commandAliases() + { + return []; + } /** * @param string $command @@ -42,7 +54,8 @@ abstract class Controller extends AbstractCommand implements ControllerInterface */ public function run($command = '') { - if (!($this->action = trim($command))) { + $this->action = $this->getRealCommandName(trim($command, $this->delimiter)); + if (!$this->action) { return $this->showHelp(); } @@ -52,7 +65,7 @@ public function run($command = '') /** * load command configure */ - protected function configure() + protected final function configure() { if ($action = $this->action) { $method = $action . 'Configure'; @@ -67,11 +80,10 @@ 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())) { @@ -84,14 +96,8 @@ protected function execute($input, $output) $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; - } - } + // 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 { @@ -104,7 +110,6 @@ protected function execute($input, $output) /** * @return int - * @throws \ReflectionException */ protected function showHelp() { @@ -117,15 +122,17 @@ 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 */ public final function helpCommand() { @@ -136,29 +143,36 @@ public final function helpCommand() 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 */ 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 @@ -171,26 +185,29 @@ public final function showCommandList() $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'], - ]); + ], ['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; @@ -200,13 +217,47 @@ protected function getAllCommandMethods(\ReflectionClass $ref = null) 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 */ @@ -222,7 +273,7 @@ public function getAction() public function setAction($action) { if ($action) { - $this->action = Helper::camelCase($action); + $this->action = FormatUtil::camelCase($action); } return $this; diff --git a/src/IO/FixedInput.php b/src/IO/FixedInput.php new file mode 100644 index 0000000..9b87201 --- /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 a9b5ba6..d407de9 100644 --- a/src/IO/Input.php +++ b/src/IO/Input.php @@ -9,23 +9,24 @@ namespace Inhere\Console\IO; -use Inhere\Console\Utils\ArgumentOptionParse; +use Inhere\Console\Utils\CommandLine; /** - * Class Input + * Class Input - the input information. by parse global var $argv. * @package Inhere\Console\IO */ class Input implements InputInterface { /** - * @var @resource + * @var resource */ - protected $inputStream = STDIN; + protected $inputStream = \STDIN; /** - * @var + * @var string */ private $pwd; /** + * eg `./examples/app home:useArg status=2 name=john arg0 -s=test --page=23` * @var string */ private $fullScript; @@ -65,19 +66,22 @@ class Input implements InputInterface /** * Input constructor. * @param null|array $argv + * @param bool $parsing */ - public function __construct($argv = null) + public function __construct($argv = null, $parsing = true) { if (null === $argv) { $argv = $_SERVER['argv']; } $this->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; + } } /** @@ -87,10 +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; @@ -107,13 +111,15 @@ public function __toString() */ 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 */ @@ -612,4 +618,12 @@ public function getPwd() 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 197956c..b1bdd7e 100644 --- a/src/IO/InputDefinition.php +++ b/src/IO/InputDefinition.php @@ -16,6 +16,8 @@ */ class InputDefinition { + /** @var array */ + private static $defaultArgOptConfig = ['mode' => null, 'description' => '', 'default' => null]; private $example; private $description; /** @@ -23,17 +25,20 @@ class InputDefinition */ private $arguments; private $requiredCount = 0; - private $hasAnArrayArgument = false; private $hasOptional = false; + private $hasAnArrayArgument = false; /** * @var array[] */ private $options; - /** - * @var array - */ + /** @var array */ private $shortcuts; + /** + * @param array $arguments + * @param array $options + * @return InputDefinition + */ public static function make(array $arguments = [], array $options = []) { return new self($arguments, $options); @@ -68,9 +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']); } @@ -197,9 +201,8 @@ public function setOptions(array $options = []) */ 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']); } } @@ -341,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 简化版显示 @@ -357,7 +369,7 @@ public function getSynopsis($short = false) if ($this->optionIsAcceptValue($option['mode'])) { $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']; @@ -380,7 +392,7 @@ public function getSynopsis($short = false) $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]; } diff --git a/src/IO/Output.php b/src/IO/Output.php index 41b2aa0..797629f 100644 --- a/src/IO/Output.php +++ b/src/IO/Output.php @@ -10,7 +10,7 @@ namespace Inhere\Console\IO; use Inhere\Console\Style\Style; -use Inhere\Console\Traits\FormatOutputTrait; +use Inhere\Console\Traits\FormatOutputAwareTrait; use Inhere\Console\Utils\Helper; use Inhere\Console\Utils\Show; @@ -20,17 +20,17 @@ */ class Output implements OutputInterface { - use FormatOutputTrait; + use FormatOutputAwareTrait; /** * 正常输出流 * Property outStream. */ - protected $outputStream = STDOUT; + protected $outputStream = \STDOUT; /** * 错误输出流 * Property errorStream. */ - protected $errorStream = STDERR; + protected $errorStream = \STDERR; /** * 控制台窗口(字体/背景)颜色添加处理 * window colors @@ -49,9 +49,46 @@ public function __construct($outputStream = null) } $this->getStyle(); } - ///////////////////////////////////////////////////////////////// - /// Output Message - ///////////////////////////////////////////////////////////////// + /*************************************************************************** + * Output buffer + ***************************************************************************/ + /** + * start buffering + */ + public function startBuffer() + { + Show::startBuffer(); + } + + /** + * 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 若不为空,则先输出文本 @@ -64,7 +101,7 @@ public function read($question = null, $nl = false) $this->write($question, $nl); } - return trim(fgets(STDIN)); + return trim(fgets(\STDIN)); } /** @@ -80,9 +117,9 @@ public function stderr($text = '', $nl = true) return $this; } - ///////////////////////////////////////////////////////////////// - /// Getter/Setter - ///////////////////////////////////////////////////////////////// + /*************************************************************************** + * Getter/Setter + ***************************************************************************/ /** * @return Style */ diff --git a/src/LiteApplication.php b/src/LiteApp.php similarity index 92% rename from src/LiteApplication.php rename to src/LiteApp.php index 6f74b4c..3c35258 100644 --- a/src/LiteApplication.php +++ b/src/LiteApp.php @@ -12,14 +12,14 @@ use Inhere\Console\Style\LiteStyle; /** - * Class LiteApplication + * Class LiteApp - Lite Application * @package Inhere\Console */ -class LiteApplication +class LiteApp { - /////////////////////////////////////////////////////////////////// - /// simple cli support - /////////////////////////////////////////////////////////////////// + /**************************************************************************** + * simple cli support + ****************************************************************************/ /** * parse from `name=val var2=val2` * @var array @@ -191,9 +191,9 @@ public function commands(array $commands) $this->addCommand($command, $handler, $des); } } - /////////////////////////////////////////////////////////////////////////////////// - /// helper methods - /////////////////////////////////////////////////////////////////////////////////// + /**************************************************************************** + * helper methods + ****************************************************************************/ /** * @param string $err */ @@ -232,9 +232,9 @@ public function getOpt($name, $default = null) { return isset($this->opts[$name]) ? $this->opts[$name] : $default; } - /////////////////////////////////////////////////////////////////////////////////// - /// getter/setter methods - /////////////////////////////////////////////////////////////////////////////////// + /**************************************************************************** + * getter/setter methods + ****************************************************************************/ /** * @return array */ diff --git a/src/Style/Color.php b/src/Style/Color.php index 8139470..782cf2d 100644 --- a/src/Style/Color.php +++ b/src/Style/Color.php @@ -14,14 +14,14 @@ */ final class Color { - /** - * Foreground base value - */ + /** Foreground base value */ const FG_BASE = 30; - /** - * Background base value - */ + /** Background base value */ const BG_BASE = 40; + /** Extra Foreground base value */ + const FG_EXTRA = 90; + /** Extra Background base value */ + const BG_EXTRA = 100; // color const BLACK = 'black'; const RED = 'red'; @@ -48,9 +48,7 @@ final class Color // 颠倒的 交换背景色与前景色 const CONCEALED = 'concealed'; // 隐匿的 - /** - * Known color list - */ + /** @var array Known color list */ private static $knownColors = array( 'black' => 0, 'red' => 1, @@ -64,10 +62,7 @@ final class Color 'white' => 7, 'normal' => 9, ); - /** - * Known style option - * @var array - */ + /** @var array Known style option */ private static $knownOptions = [ 'bold' => 1, // 22 加粗 @@ -83,41 +78,37 @@ final class Color // 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) { @@ -130,6 +121,9 @@ public static function makeByString($string) case 'bg': $bg = $subParts[1]; break; + case 'extra': + $extra = $subParts[1]; + break; case 'options': $options = explode(',', $subParts[1]); break; @@ -139,7 +133,7 @@ public static function makeByString($string) } } - return new self($fg, $bg, $options); + return new self($fg, $bg, $options, $extra); } /** @@ -147,21 +141,21 @@ 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()))); } - $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()))); } - $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)) { diff --git a/src/Style/Highlighter.php b/src/Style/Highlighter.php index f61e99d..c190eb7 100644 --- a/src/Style/Highlighter.php +++ b/src/Style/Highlighter.php @@ -12,8 +12,263 @@ /** * Class Highlighter * @package Inhere\Console\Style - * @referrer jakub-onderka/php-console-highlighter + * @see jakub-onderka/php-console-highlighter + * @link https://github.com/JakubOnderka/PHP-Console-Highlighter/blob/master/src/Highlighter.php */ class Highlighter { + const TOKEN_DEFAULT = 'token_default'; + const TOKEN_COMMENT = 'token_comment'; + const TOKEN_STRING = 'token_string'; + const TOKEN_HTML = 'token_html'; + const TOKEN_KEYWORD = 'token_keyword'; + const ACTUAL_LINE_MARK = 'actual_line_mark'; + const LINE_NUMBER = 'line_number'; + /** @var Style */ + private $color; + /** @var array */ + private $defaultTheme = [self::TOKEN_STRING => '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 4e3a2a1..1004543 100644 --- a/src/Style/LiteStyle.php +++ b/src/Style/LiteStyle.php @@ -35,7 +35,7 @@ class LiteStyle const FG_LIGHT_BLUE = 94; const FG_LIGHT_MAGENTA = 95; const FG_LIGHT_CYAN = 96; - const FG_WHITE_W = 97; + const FG_WHITE_EXTRA = 97; // Background color const BG_BLACK = 40; const BG_RED = 41; @@ -53,7 +53,7 @@ class LiteStyle const BG_LIGHT_BLUE = 104; const BG_LIGHT_MAGENTA = 105; const BG_LIGHT_CYAN = 106; - const BG_WHITE_W = 107; + const BG_WHITE_EXTRA = 107; // color option const BOLD = 1; // 加粗 @@ -79,12 +79,8 @@ class LiteStyle * @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', @@ -92,6 +88,19 @@ 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', @@ -110,7 +119,7 @@ class LiteStyle ]; /** - * @param $text + * @param string $text * @param string|int|array $style * @return string */ @@ -119,7 +128,7 @@ 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)) { @@ -141,7 +150,7 @@ public static function color($text, $style = null) /** * render color tag to color style - * @param $text + * @param string $text * @return mixed|string */ public static function renderColor($text) @@ -150,7 +159,7 @@ public static function renderColor($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)) { diff --git a/src/Style/Style.php b/src/Style/Style.php index fc153b8..4355f52 100644 --- a/src/Style/Style.php +++ b/src/Style/Style.php @@ -17,6 +17,12 @@ * Class Style * @package Inhere\Console\Style * @link https://github.com/ventoviro/windwalker-IO + * @method string info(string $message) + * @method string comment(string $message) + * @method string success(string $message) + * @method string warning(string $message) + * @method string danger(string $message) + * @method string error(string $message) */ class Style { @@ -40,11 +46,11 @@ class Style * Regex to match tags * @var string */ - const COLOR_TAG = '/<([a-z=;]+)>(.*?)<\\/\\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 */ @@ -86,13 +92,38 @@ public function __construct($fg = '', $bg = '', array $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'])->add(self::NOTE, ['fg' => 'green', 'options' => ['bold']])->add(self::PRIMARY, ['fg' => 'blue'])->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('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)); } /** @@ -159,18 +190,20 @@ 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); @@ -178,7 +211,7 @@ public function add($name, $fg = '', $bg = '', array $options = []) 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; @@ -186,21 +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; } diff --git a/src/Traits/AdvancedFormatOutputTrait.php b/src/Traits/AdvancedFormatOutputTrait.php new file mode 100644 index 0000000..f02b656 --- /dev/null +++ b/src/Traits/AdvancedFormatOutputTrait.php @@ -0,0 +1,18 @@ +fonts[$name] = $content; - } - } - - /** - * @param string $path - */ - public function addPath($path) - { - if (file_exists($path)) { - $this->artPaths[] = $path; - } - } - - /** - * @return array - */ - public function getArtPaths() - { - return $this->artPaths; - } - - /** - * @param array $artPaths - */ - public function setArtPaths(array $artPaths) - { - foreach ($artPaths as $path) { - $this->addPath($path); - } - } - - /** - * @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/Utils/CliUtil.php b/src/Utils/CliUtil.php new file mode 100644 index 0000000..4d806fb --- /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 91% rename from src/Utils/ArgumentOptionParse.php rename to src/Utils/CommandLine.php index bf7a147..c44c9dc 100644 --- a/src/Utils/ArgumentOptionParse.php +++ b/src/Utils/CommandLine.php @@ -10,10 +10,10 @@ namespace Inhere\Console\Utils; /** - * Class ArgumentOptionParse - console argument and option parse + * Class CommandLine - console argument and option parse * @package Inhere\Console\Utils */ -final class ArgumentOptionParse +final class CommandLine { /** * These words will be as a Boolean value @@ -46,7 +46,7 @@ final class ArgumentOptionParse * @param bool $mergeOpts Whether merge short-opts and long-opts * @return array */ - public static function byArgv(array $params, array $noValues = [], $mergeOpts = false) + 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. @@ -107,10 +107,14 @@ 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', @@ -119,7 +123,7 @@ public static function byArgv(array $params, array $noValues = [], $mergeOpts = * @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) { @@ -140,12 +144,12 @@ 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) + public static function parseByString($string) { } diff --git a/src/Utils/FormatUtil.php b/src/Utils/FormatUtil.php new file mode 100644 index 0000000..5ef3a63 --- /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 13f4cf4..f2f9d0f 100644 --- a/src/Utils/Helper.php +++ b/src/Utils/Helper.php @@ -26,6 +26,14 @@ public static function isOnWindows() return DIRECTORY_SEPARATOR === '\\'; } + /** + * @return bool + */ + public static function isMac() + { + return stripos(PHP_OS, 'Darwin') !== false; + } + /** * @return bool */ @@ -56,13 +64,21 @@ public static function isRoot() return getmyuid() === 0; } + /** + * @return bool + */ + public static function isSupportColor() + { + return self::supportColor(); + } + /** * Returns true if STDOUT supports colorization. * This code has been copied and adapted from * \Symfony\Component\Console\Output\OutputStream. * @return boolean */ - public static function isSupportColor() + public static function supportColor() { if (DIRECTORY_SEPARATOR === '\\') { return '10.0.10586' === PHP_WINDOWS_VERSION_MAJOR . '.' . PHP_WINDOWS_VERSION_MINOR . '.' . PHP_WINDOWS_VERSION_BUILD || false !== getenv('ANSICON') || 'ON' === getenv('ConEmuANSI') || 'xterm' === getenv('TERM'); @@ -100,154 +116,6 @@ public static function isInteractive($fileDescriptor) return \function_exists('posix_isatty') && @posix_isatty($fileDescriptor); } - /** - * @return string - */ - public static function getNullDevice() - { - if (self::isUnix()) { - return '/dev/null'; - } - - return 'NUL'; - } - - /** - * run a command in background - * @param string $cmd - */ - public static function execInBackground($cmd) - { - if (self::isWindows()) { - pclose(popen('start /B ' . $cmd, 'r')); - } else { - exec($cmd . ' > /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 @@ -277,6 +145,14 @@ public static function recursiveDirectoryIterator($srcDir, callable $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 @@ -328,23 +204,6 @@ public static function 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 @@ -368,82 +227,27 @@ 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) - { - // 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; - } - - /** - * @param $memory - * @return string - * ``` - * Helper::formatMemory(memory_get_usage(true)); - * ``` - */ - public static function formatMemory($memory) + public static function findSimilar($need, $iterator, $similarPercent = 45) { - 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); + // find similar command names by similar_text() + $similar = []; + if (!$need) { + return $similar; } - - 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]; - } + foreach ($iterator as $name) { + similar_text($need, $name, $percent); + if ($similarPercent <= (int)$percent) { + $similar[] = $name; } } - return date('Y-m-d H:i:s', $secs); + return $similar; } /** @@ -470,182 +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, - ], $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 diff --git a/src/Utils/Interact.php b/src/Utils/Interact.php index dad969d..b41948f 100644 --- a/src/Utils/Interact.php +++ b/src/Utils/Interact.php @@ -51,7 +51,7 @@ public static function read($message = null, $nl = false, array $opts = []) * Interactive method (select/confirm/question/loopAsk) **************************************************************************************************/ /** - * Select one of the options 在多个选项中选择一个 + * alias of the `select()` * @param string $description 说明 * @param mixed $options 选项数据 * e.g @@ -70,9 +70,9 @@ public static function select($description, $options, $default = null, $allowExi } /** - * alias of the `select()` + * choice one of the options 在多个选项中选择一个 * @param $description - * @param $options + * @param string|array $options * @param null $default * @param bool $allowExit * @return string @@ -90,13 +90,14 @@ public static function choice($description, $options, $default = null, $allowExi if ($allowExit) { $options['q'] = 'quit'; } - beginChoice: - $text = " {$description}"; + $text = "{$description}"; foreach ($options as $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; @@ -109,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; } /** @@ -128,7 +183,7 @@ public static function multiSelect($description, $options, $default = null, $all public static function confirm($question, $default = true) { if (!($question = trim($question))) { - self::error('Please provide a question text!', 1); + self::warning('Please provide a question message!', 1); } $question = ucfirst(trim($question, '?')); $default = (bool)$default; @@ -152,20 +207,9 @@ public static function confirm($question, $default = true) /** * 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) @@ -175,9 +219,33 @@ 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) @@ -185,19 +253,28 @@ public static function question($question, $default = null, \Closure $validator 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; @@ -211,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) * { @@ -242,11 +319,17 @@ public static function limitedAsk($question, $default = null, \Closure $validato $result = false; $answer = ''; $question = ucfirst($question); + $hasDefault = null !== $default; $back = $times = (int)$times > 6 || $times < 1 ? 3 : (int)$times; - $defaultText = null !== $default ? "(default: {$default})" : ''; + 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; @@ -254,7 +337,7 @@ public static function limitedAsk($question, $default = null, \Closure $validato } } 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) { @@ -270,7 +353,7 @@ public static function limitedAsk($question, $default = null, \Closure $validato 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; @@ -286,25 +369,25 @@ 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($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'; + $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)); @@ -319,6 +402,7 @@ public static function promptSilent($prompt = 'Enter Password:') * alias of the method `promptSilent()` * @param string $prompt * @return string + * @throws \RuntimeException */ public static function askHiddenInput($prompt = 'Enter Password:') { @@ -329,6 +413,7 @@ public static function askHiddenInput($prompt = 'Enter Password:') * alias of the method `promptSilent()` * @param string $prompt * @return string + * @throws \RuntimeException */ public static function askPassword($prompt = 'Enter Password:') { diff --git a/src/Utils/ProcessUtil.php b/src/Utils/ProcessUtil.php new file mode 100644 index 0000000..6eba65a --- /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 dab8c42..d66d12e 100644 --- a/src/Utils/ProgressBar.php +++ b/src/Utils/ProgressBar.php @@ -493,7 +493,7 @@ private static function loadDefaultParsers() return $display; }, 'elapsed' => function (self $bar) { - return Helper::formatTime(time() - $bar->getStartTime()); + 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.'); @@ -504,7 +504,7 @@ private static function loadDefaultParsers() $remaining = round((time() - $bar->getStartTime()) / $bar->getProgress() * ($bar->getMaxSteps() - $bar->getProgress())); } - return Helper::formatTime($remaining); + return FormatUtil::timestamp($remaining); }, 'estimated' => function (self $bar) { if (!$bar->getMaxSteps()) { return 0; @@ -516,9 +516,9 @@ private static function loadDefaultParsers() $estimated = round((time() - $bar->getStartTime()) / $bar->getProgress() * $bar->getMaxSteps()); } - return Helper::formatTime($estimated); + return FormatUtil::timestamp($estimated); }, 'memory' => function () { - return Helper::formatMemory(memory_get_usage(true)); + 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) { diff --git a/src/Utils/Show.php b/src/Utils/Show.php index 0922315..fd21c27 100644 --- a/src/Utils/Show.php +++ b/src/Utils/Show.php @@ -9,6 +9,7 @@ namespace Inhere\Console\Utils; +use Inhere\Console\Components\StrBuffer; use Inhere\Console\Style\Style; /** @@ -54,9 +55,11 @@ class Show const HELP_OPTIONS = 'options'; const HELP_EXAMPLES = 'examples'; const HELP_EXTRAS = 'extras'; - /** - * @var array - */ + /** @var string */ + private static $buffer; + /** @var bool */ + private static $buffering = false; + /** @var array */ public static $defaultBlocks = ['block', 'primary', 'info', 'notice', 'success', 'warning', 'danger', 'error']; /************************************************************************************************** * Output block Message @@ -226,7 +229,7 @@ public static function section($title, $body, array $opts = []) } } $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)); } @@ -269,23 +272,27 @@ public static function padding(array $data, $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 = []) { $string = ''; - $opts = array_merge(['leftChar' => ' ', 'keyStyle' => 'info', 'keyMinWidth' => 8, 'titleStyle' => 'comment', 'returned' => false], $opts); + $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; } @@ -337,7 +344,7 @@ public static function mList(array $data, array $opts = []) foreach ($data as $title => $list) { $buffer[] = self::aList($list, $title, $opts); } - self::write($buffer); + self::write(implode("\n", $buffer)); } /** @@ -373,7 +380,8 @@ 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' => '', @@ -383,10 +391,16 @@ public static function helpPanel(array $config, $showAfterQuit = true) '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 ... @@ -401,17 +415,17 @@ public static function helpPanel(array $config, $showAfterQuit = true) $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); @@ -478,6 +492,7 @@ public static function panel($data, $title = 'Information Panel', array $opts = } $border = null; $panelWidth = $labelMaxWidth + $valueMaxWidth; + self::startBuffer(); // output title if ($title) { $title = ucwords($title); @@ -492,18 +507,28 @@ public static function panel($data, $title = 'Information Panel', array $opts = 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::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 @@ -555,7 +580,7 @@ public static function table(array $data, $title = 'Data Table', array $opts = [ ], $opts); $hasHead = false; $rowIndex = 0; - $head = $table = []; + $head = []; $tableHead = $opts['columns']; $leftIndent = $opts['leftIndent']; $showBorder = $opts['showBorder']; @@ -656,6 +681,89 @@ public static function table(array $data, $title = 'Data Table', array $opts = [ 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() ? "\r\33[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() ? "\r\33[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 @@ -667,7 +775,7 @@ public static function counterTxt($msg, $doneMsg = null) { $counter = 0; $finished = false; - $tpl = (Helper::isSupportColor() ? "\r\33[2K" : "\r\r") . '%d %s'; + $tpl = (Helper::supportColor() ? "\r\33[2K" : "\r\r") . '%d %s'; $msg = self::getStyle()->render($msg); $doneMsg = $doneMsg ? self::getStyle()->render($doneMsg) : null; while (true) { @@ -685,9 +793,6 @@ public static function counterTxt($msg, $doneMsg = null) } $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"; @@ -707,7 +812,7 @@ public static function progressTxt($total, $msg, $doneMsg = null) { $current = 0; $finished = false; - $tpl = (Helper::isSupportColor() ? "\r\33[2K" : "\r\r") . "%' 3d%% %s"; + $tpl = (Helper::supportColor() ? "\r\33[2K" : "\r\r") . "%' 3d%% %s"; $msg = self::getStyle()->render($msg); $doneMsg = $doneMsg ? self::getStyle()->render($doneMsg) : null; while (true) { @@ -762,7 +867,7 @@ public static function progressBar($total, array $opts = []) { $current = 0; $finished = false; - $tplPrefix = Helper::isSupportColor() ? "\r\33[2K" : "\r\r"; + $tplPrefix = Helper::supportColor() ? "\r\33[2K" : "\r\r"; $opts = array_merge(['doneChar' => '=', 'waitChar' => ' ', 'signChar' => '>', 'msg' => '', 'doneMsg' => ''], $opts); $msg = self::getStyle()->render($opts['msg']); $doneMsg = self::getStyle()->render($opts['doneMsg']); @@ -825,9 +930,87 @@ public static function createProgressBar($max = 0, $start = true) return $bar; } - ///////////////////////////////////////////////////////////////// - /// Helper Method - ///////////////////////////////////////////////////////////////// + /*********************************************************************************** + * 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; + } + + 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 */ @@ -845,7 +1028,7 @@ public static function getStyle() * [ * '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 */ @@ -858,8 +1041,19 @@ public static function write($messages, $nl = true, $quit = false, array $opts = if (!isset($opts['color']) || $opts['color']) { $messages = static::getStyle()->render($messages); } - $stream = isset($opts['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); } @@ -871,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 diff --git a/tests/Components/TextTemplateTest.php b/tests/Components/TextTemplateTest.php new file mode 100644 index 0000000..9f1e364 --- /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 2565785..567adcb 100644 --- a/tests/boot.php +++ b/tests/boot.php @@ -1,6 +1,28 @@