diff --git a/src/ArgvParser.php b/src/ArgvParser.php index f6c2837..a105718 100644 --- a/src/ArgvParser.php +++ b/src/ArgvParser.php @@ -36,7 +36,7 @@ class ArgvParser extends Parser * @param string $desc * @param bool $allowUnknown */ - public function __construct(string $name, string $desc = null, bool $allowUnknown = false) + public function __construct(string $name, string $desc = '', bool $allowUnknown = false) { $this->_name = $name; $this->_desc = $desc; @@ -44,6 +44,10 @@ public function __construct(string $name, string $desc = null, bool $allowUnknow $this->option('-h, --help', 'Show help')->on([$this, 'showHelp']); $this->option('-V, --version', 'Show version')->on([$this, 'showVersion']); + + $this->onExit(function () { + exit(0); + }); } /** @@ -60,6 +64,16 @@ public function version(string $version): self return $this; } + public function getName(): string + { + return $this->_name; + } + + public function getDesc(): string + { + return $this->_desc; + } + /** * Registers argument definitions (all at once). Only last one can be variadic. * @@ -137,6 +151,20 @@ public function on(callable $fn): self return $this; } + /** + * Register exit handler. + * + * @param callable $fn + * + * @return self + */ + public function onExit(callable $fn): self + { + $this->_events['_exit'] = $fn; + + return $this; + } + protected function handleUnknown(string $arg, string $value = null) { if ($this->_allowUnknown) { @@ -197,22 +225,61 @@ public function args(): array protected function showHelp() { - echo "{$this->_name}, version {$this->_version}" . PHP_EOL; + $args = $this->_arguments ? ' [ARGUMENTS]' : ''; + $opts = $this->_options ? ' [OPTIONS]' : ''; + + ($w = new Writer) + ->bold("Command {$this->_name}, version {$this->_version}", true)->eol() + ->comment($this->_desc, true)->eol() + ->bold('Usage: ')->yellow("{$this->_name}{$args}{$opts}", true); + + if ($args) { + $this->showArguments($w); + } + + if ($opts) { + $this->showOptions($w); + } + + $w->eol()->yellow('Note: [optional]')->eol(); + + return $this->emit('_exit'); + } + + protected function showArguments(Writer $w) + { + $w->eol()->boldGreen('Arguments:', true); + + $maxLen = \max(\array_map('strlen', \array_keys($this->_arguments))); + + foreach ($this->_arguments as $arg) { + $name = $arg->name(); + $name = $arg->required() ? "<$name>" : "[$name]"; + $w->bold(' ' . \str_pad($name, $maxLen + 4))->comment($arg->desc(), true); + } + } + + protected function showOptions(Writer $w) + { + $w->eol()->boldGreen('Options:', true); - // @todo: build help msg! - echo "help\n"; + $maxLen = \max(\array_map('strlen', \array_keys($this->_options))); - _exit(); + foreach ($this->_options as $opt) { + $name = $opt->short() . '|' . $opt->long(); + $name = $opt->required() ? "<$name>" : "[$name]"; + $w->bold(' ' . \str_pad($name, $maxLen + 9))->comment($opt->desc(), true); + } } protected function showVersion() { - echo $this->_version . PHP_EOL; + (new Writer)->bold($this->_version, true); - _exit(); + return $this->emit('_exit'); } - protected function emit(string $event) + public function emit(string $event) { if (empty($this->_events[$event])) { return; @@ -220,15 +287,6 @@ protected function emit(string $event) $callback = $this->_events[$event]; - $callback(); - } -} - -// @codeCoverageIgnoreStart -if (!\function_exists(__NAMESPACE__ . '\\_exit')) { - function _exit($code = 0) - { - exit($code); + return $callback(); } } -// @codeCoverageIgnoreEnd diff --git a/src/Option.php b/src/Option.php index e6a9877..6a293a3 100644 --- a/src/Option.php +++ b/src/Option.php @@ -18,7 +18,7 @@ class Option extends Parameter protected $filter; - public function __construct(string $raw, string $desc = null, $default = null, callable $filter = null) + public function __construct(string $raw, string $desc = '', $default = null, callable $filter = null) { $this->filter = $filter; diff --git a/src/Parameter.php b/src/Parameter.php index e57406f..2e91d64 100644 --- a/src/Parameter.php +++ b/src/Parameter.php @@ -28,7 +28,7 @@ abstract class Parameter protected $variadic = false; - public function __construct(string $raw, string $desc = null, $default = null) + public function __construct(string $raw, string $desc = '', $default = null) { $this->raw = $raw; $this->desc = $desc; @@ -52,6 +52,11 @@ public function name(): string return $this->name; } + public function desc(): string + { + return $this->desc; + } + public function attributeName(): string { return $this->toCamelCase($this->name); diff --git a/src/Writer.php b/src/Writer.php index d1ceebc..c43e9b3 100644 --- a/src/Writer.php +++ b/src/Writer.php @@ -45,14 +45,35 @@ public function __get(string $name): self * @param string $text * @param bool $eol * - * @return void + * @return self */ - public function write(string $text, bool $eol = false) + public function write(string $text, bool $eol = false): self { list($method, $this->method) = [$this->method ?: 'line', '']; $stream = \stripos($method, 'error') !== false ? \STDERR : \STDOUT; - \fwrite($stream, $this->colorizer->{$method}($text, [], $eol)); + if ($method === 'eol') { + \fwrite($stream, PHP_EOL); + } else { + \fwrite($stream, $this->colorizer->{$method}($text, [], $eol)); + } + + return $this; + } + + /** + * Write to stdout or stderr magically. + * + * @param string $method + * @param array $arguments + * + * @return self + */ + public function __call(string $method, array $arguments): self + { + $this->method = $method; + + return $this->write($arguments[0] ?? '', $arguments[1] ?? false); } } diff --git a/tests/ArgvParserTest.php b/tests/ArgvParserTest.php index 2346cad..978088e 100644 --- a/tests/ArgvParserTest.php +++ b/tests/ArgvParserTest.php @@ -97,10 +97,6 @@ public function test_options_unknown() $p = $this->newParser('', '', true)->parse(['php', '--hot-path', '/path']); $this->assertSame('/path', $p->hotPath, 'Allow unknown'); - ob_start(); - $p = $this->newParser()->parse(['php', '--unknown', '1']); - $this->assertContains('help', ob_get_clean(), 'Show help'); - $this->expectException(\RuntimeException::class); $this->expectExceptionMessage('Option "--random" not registered'); @@ -180,22 +176,6 @@ public function test_event() $this->assertSame('hello event', ob_get_clean()); } - public function test_default_options() - { - ob_start(); - $p = $this->newParser('v1.0.1')->parse(['php', '--version']); - $this->assertContains('v1.0.1', ob_get_clean(), 'Long'); - - ob_start(); - $p = $this->newParser('v2.0.1')->parse(['php', '-V']); - $this->assertContains('v2.0.1', ob_get_clean(), 'Short'); - - ob_start(); - $p = $this->newParser()->parse(['php', '--help']); - $this->assertContains('ArgvParserTest', $buffer = ob_get_clean()); - $this->assertContains('help', $buffer); - } - public function test_no_value() { $p = $this->newParser()->option('-x --xyz')->parse(['php', '-x']); @@ -211,7 +191,7 @@ public function test_args() $this->assertSame(['a' => 'A', 'b' => 'B', 'C', 'D'], $p->args()); } - protected function newParser(string $version = '0.0.1', string $desc = null, bool $allowUnknown = false) + protected function newParser(string $version = '0.0.1', string $desc = '', bool $allowUnknown = false) { $p = new ArgvParser('ArgvParserTest', $desc, $allowUnknown); diff --git a/tests/CliTestCase.php b/tests/CliTestCase.php new file mode 100644 index 0000000..68b06e4 --- /dev/null +++ b/tests/CliTestCase.php @@ -0,0 +1,49 @@ +data; + } + + return PSFS_PASS_ON; + } +} diff --git a/tests/DefaultOptionTest.php b/tests/DefaultOptionTest.php new file mode 100644 index 0000000..663fdea --- /dev/null +++ b/tests/DefaultOptionTest.php @@ -0,0 +1,47 @@ +newParser('v1.0.1')->parse(['php', '--version']); + $this->assertContains('v1.0.1', $this->buffer(), 'Long'); + } + + public function test_V() + { + $p = $this->newParser('v2.0.1')->parse(['php', '-V']); + $this->assertContains('v2.0.1', $this->buffer(), 'Short'); + } + + public function test_help() + { + $p = $this->newParser() + ->arguments('[arg]') + ->option('-o --option') + ->parse(['php', '--help']); + + $this->assertContains('ArgvParserTest', $buffer = $this->buffer()); + $this->assertContains('[arg]', $buffer); + $this->assertContains('[-o|--option]', $buffer); + } + + public function test_help_unknown() + { + $p = $this->newParser()->arguments('[apple]')->parse(['php', '--unknown', '1']); + $this->assertContains('[apple]', $this->buffer(), 'Show help'); + } + + protected function newParser(string $version = '0.0.1', string $desc = '', bool $allowUnknown = false) + { + $p = new ArgvParser('ArgvParserTest', $desc, $allowUnknown); + + return $p->version($version)->onExit(function () { + return false; + }); + } +} diff --git a/tests/WriterTest.php b/tests/WriterTest.php index 9116b10..dff8594 100644 --- a/tests/WriterTest.php +++ b/tests/WriterTest.php @@ -3,66 +3,38 @@ namespace Ahc\Cli\Test; use Ahc\Cli\Writer; -use PHPUnit\Framework\TestCase; -class WriterTest extends TestCase +class WriterTest extends CliTestCase { - public static function setUpBeforeClass() - { - // Thanks: https://stackoverflow.com/a/39785995 - stream_filter_register('intercept', StreamInterceptor::class); - stream_filter_append(\STDOUT, 'intercept'); - stream_filter_append(\STDERR, 'intercept'); - } - - public function setUp() - { - StreamInterceptor::$buffer = ''; - } - public function test_simple_write() { (new Writer)->write('Hey'); - $this->assertContains('Hey', StreamInterceptor::$buffer); - $this->assertSame("\033[0;37mHey\033[0m", StreamInterceptor::$buffer); + $this->assertContains('Hey', $this->buffer()); + $this->assertSame("\033[0;37mHey\033[0m", $this->buffer()); } public function test_write_error() { (new Writer)->error->write('Something wrong'); - $this->assertContains('Something wrong', StreamInterceptor::$buffer); - $this->assertSame("\033[0;31mSomething wrong\033[0m", StreamInterceptor::$buffer); + $this->assertContains('Something wrong', $this->buffer()); + $this->assertSame("\033[0;31mSomething wrong\033[0m", $this->buffer()); } public function test_write_with_newline() { (new Writer)->write('Hello', true); - $this->assertContains('Hello', StreamInterceptor::$buffer); - $this->assertSame("\033[0;37mHello\033[0m" . PHP_EOL, StreamInterceptor::$buffer); + $this->assertContains('Hello', $this->buffer()); + $this->assertSame("\033[0;37mHello\033[0m" . PHP_EOL, $this->buffer()); } public function test_write_bold_red_bggreen() { (new Writer)->bold->red->bgGreen->write('bold->red->bgGreen'); - $this->assertContains('bold->red->bgGreen', StreamInterceptor::$buffer); - $this->assertSame("\033[1;31;42mbold->red->bgGreen\033[0m", StreamInterceptor::$buffer); - } -} - -class StreamInterceptor extends \php_user_filter -{ - public static $buffer = ''; - - public function filter($in, $out, &$consumed, $closing) - { - while ($bucket = stream_bucket_make_writeable($in)) { - static::$buffer .= $bucket->data; - } - - return PSFS_PASS_ON; + $this->assertContains('bold->red->bgGreen', $this->buffer()); + $this->assertSame("\033[1;31;42mbold->red->bgGreen\033[0m", $this->buffer()); } } diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 385ce9c..6c8c4f5 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -1,10 +1,3 @@