diff --git a/.travis.yml b/.travis.yml index b898e6c..9be86c8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,9 +2,6 @@ language: php sudo: false dist: trusty php: - - 5.4 - - 5.5 - - 5.6 - 7.0 - 7.1 - 7.2 diff --git a/README.md b/README.md index 5e42a55..6b26bb2 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ It is lightweight and has **no 3rd party dependencies**. Note: this is for non-i Use composer: -```php composer.phar require splitbrain/php-cli``` +```composer require splitbrain/php-cli``` ## Usage and Examples @@ -78,11 +78,11 @@ for further info. ## Exceptions By default the CLI class registers an exception handler and will print the exception's message to the end user and -exit the programm with a non-zero exit code. You can disable this behaviour and catch all exceptions yourself by +exit the program with a non-zero exit code. You can disable this behaviour and catch all exceptions yourself by passing false to the constructor. You can use the provided ``splitbrain\phpcli\Exception`` to signal any problems within your main code yourself. The -exceptions's code will be used as the exit code then. +exceptions' code will be used as the exit code then. Stacktraces will be printed on log level `debug`. @@ -93,7 +93,7 @@ then uses terminal colors. You can always suppress colored output by passing ``- Disabling colors will also disable the emoticon prefixes. Simple colored log messages can be printed by you using the convinence methods ``success()`` (green), ``info()`` (cyan), -``error()`` (red) or ``fatal()`` (red). The latter will also exit the programm with a non-zero exit code. +``error()`` (red) or ``fatal()`` (red). The latter will also exit the program with a non-zero exit code. For more complex coloring you can access the color class through ``$this->colors`` in your script. The ``wrap()`` method is probably what you want to use. @@ -156,3 +156,99 @@ Messages from `warning` level onwards are printed to `STDERR` all below are prin The default log level of your script can be set by overwriting the `$logdefault` member. See `example/logging.php` for an example. + +## Command Example +```php +#!/usr/bin/php +registerCommand( cliCmds::create, 'Create a new entry' ); + $options->registerCommand( cliCmds::delete, 'Delete an entry' ); + + // showing registering an option for two commands + $options->registerOption( + cliOpts::name, + 'The entry name', + cliOpts::nameShort, + cliOpts::nameArg, + [cliCmds::create, cliCmds::delete] + ); + + $options->setHelp( 'An example showing commands' ); + } + + + protected function main( Options $options ) + { + // show the name of the program and its version + $this->info( $this->options->getBin() . ' v1.0.0' ); + + switch ( $options->getCmd() ) { + case cliCmds::create: + $this->create( $name ); + break; + + case cliCmds::delete: + $this->delete( $name ); + break; + + default: + echo $options->help(); + } + } + + + protected function delete( $name ) + { + if( ! $name = $this->options->getOpt( cliOpts::name ) ) + { + echo $this->options->help(); + $this->fatal( 'Missing --' . cliOpts::name ); + } + $this->success( 'Deleted an entry for ' . $name ); + } + + + protected function create( $name ) + { + if( ! $name = $this->options->getOpt( cliOpts::name ) ) + { + echo $this->options->help(); + $this->fatal( 'Missing --' . cliOpts::name ); + } + $this->success( 'Created an entry for ' . $name ); + } +} + +// execute it +$cli = new app(); +$cli->run(); +``` diff --git a/composer.json b/composer.json index 79e2502..60b521e 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,7 @@ "psr/log": "Allows you to make the CLI available as PSR-3 logger" }, "require-dev": { - "phpunit/phpunit": "4.5.*" + "phpunit/phpunit": "^6" }, "autoload": { "psr-4": { diff --git a/examples/commands.php b/examples/commands.php new file mode 100755 index 0000000..6abb88d --- /dev/null +++ b/examples/commands.php @@ -0,0 +1,92 @@ +#!/usr/bin/php +registerCommand( cliCmds::create, 'Create a new entry' ); + $options->registerCommand( cliCmds::delete, 'Delete an entry' ); + + // showing registering an option for two commands + $options->registerOption( + cliOpts::name, + 'The entry name', + cliOpts::nameShort, + cliOpts::nameArg, + [cliCmds::create, cliCmds::delete] + ); + + $options->setHelp( 'An example showing commands' ); + } + + + protected function main( Options $options ) + { + // show the name of the program and its version + $this->info( $this->options->getBin() . ' v1.0.0' ); + + switch ( $options->getCmd() ) { + case cliCmds::create: + $this->create( $name ); + break; + + case cliCmds::delete: + $this->delete( $name ); + break; + + default: + echo $options->help(); + } + } + + + protected function delete( $name ) + { + if( ! $name = $this->options->getOpt( cliOpts::name ) ) + { + echo $this->options->help(); + $this->fatal( 'Missing --' . cliOpts::name ); + } + $this->success( 'Deleted an entry for ' . $name ); + } + + + protected function create( $name ) + { + if( ! $name = $this->options->getOpt( cliOpts::name ) ) + { + echo $this->options->help(); + $this->fatal( 'Missing --' . cliOpts::name ); + } + $this->success( 'Created an entry for ' . $name ); + } +} + +// execute it +$cli = new app(); +$cli->run(); \ No newline at end of file diff --git a/phpunit.xml b/phpunit.xml index d7e1f24..5dac60e 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -11,7 +11,7 @@ syntaxCheck="false"> - ./tests/ + ./tests/ - \ No newline at end of file + diff --git a/src/Options.php b/src/Options.php index 76733aa..fe30896 100644 --- a/src/Options.php +++ b/src/Options.php @@ -25,6 +25,8 @@ class Options /** @var array passed non-option arguments */ protected $args = array(); + protected $parsePass = 1; + /** @var string the executed script */ protected $bin; @@ -58,7 +60,7 @@ public function __construct(Colors $colors = null) $this->options = array(); } - + /** * Gets the bin value */ @@ -77,6 +79,29 @@ public function setHelp($help) $this->setup['']['help'] = $help; } + /** + * Returns true if a command has been registered in the main setup class property + * or throws an exception if the command or one of an array of commands is not registered + * @param string/array of strings The command name + * @throws Exception + **/ + public function commandRegistered( $commands ) + { + if( is_array( $commands ) ) + { + foreach( $commands as $command ) { + $this->commandRegistered( $command ); + } + return true; + } + + if(!isset($this->setup[$commands])) { + throw new Exception("Command $commands not registered"); + } + + return true; + } + /** * Register the names of arguments for help generation and number checking * @@ -90,15 +115,22 @@ public function setHelp($help) */ public function registerArgument($arg, $help, $required = true, $command = '') { - if (!isset($this->setup[$command])) { - throw new Exception("Command $command not registered"); + $commands = (array)$command; + $this->commandRegistered($commands); + + foreach( $commands as $command ) { + $this->setup[$command]['args'][] = array( + 'name' => $arg, + 'help' => $help, + 'required' => $required + ); } + } - $this->setup[$command]['args'][] = array( - 'name' => $arg, - 'help' => $help, - 'required' => $required - ); + + public function getArgument( $index ) + { + return $this->args[$index]; } /** @@ -121,7 +153,6 @@ public function registerCommand($command, $help) 'args' => array(), 'help' => $help ); - } /** @@ -136,22 +167,23 @@ public function registerCommand($command, $help) */ public function registerOption($long, $help, $short = null, $needsarg = false, $command = '') { - if (!isset($this->setup[$command])) { - throw new Exception("Command $command not registered"); + $commands = (array)$command; + $this->commandRegistered($commands); + + if ($short && strlen($short) > 1) { + throw new Exception("Short options should be exactly one ASCII character"); } - $this->setup[$command]['opts'][$long] = array( - 'needsarg' => $needsarg, - 'help' => $help, - 'short' => $short - ); + foreach( $commands as $command ) { + $this->setup[$command]['opts'][$long] = array( + 'needsarg' => $needsarg, + 'help' => $help, + 'short' => $short + ); - if ($short) { - if (strlen($short) > 1) { - throw new Exception("Short options should be exactly one ASCII character"); + if ($short) { + $this->setup[$command]['short'][$short] = $long; } - - $this->setup[$command]['short'][$short] = $long; } } @@ -185,7 +217,7 @@ public function checkArguments() * Parses the given arguments for known options and command * * The given $args array should NOT contain the executed file as first item anymore! The $args - * array is stripped from any options and possible command. All found otions can be accessed via the + * array is stripped from any options and possible command. All found options can be accessed via the * getOpt() function * * Note that command options will overwrite any global options with the same name @@ -196,6 +228,12 @@ public function checkArguments() */ public function parseOptions() { + if( $this->parsePass == 1 && substr($this->args[0],0,1) != '-' ) + { + // throw an exception if the supplied command is not a registered command + $this->commandRegistered($this->args[0]); + } + $non_opts = array(); $argc = count($this->args); @@ -279,6 +317,8 @@ public function parseOptions() if (!$this->command && $this->args && isset($this->setup[$this->args[0]])) { // it is a command! $this->command = array_shift($this->args); + $this->commandRegistered($this->command); + $this->parsePass++; $this->parseOptions(); // second pass } } diff --git a/src/TableFormatter.php b/src/TableFormatter.php index 23bb894..bff1f94 100644 --- a/src/TableFormatter.php +++ b/src/TableFormatter.php @@ -29,7 +29,7 @@ class TableFormatter public function __construct(Colors $colors = null) { // try to get terminal width - $width = $this->getTerminalWidth(); + $width = trim( $this->getTerminalWidth() ); if ($width) { $this->max = $width - 1; } @@ -89,20 +89,19 @@ public function setMaxWidth($max) * * @return int terminal width, 0 if unknown */ - protected function getTerminalWidth() + protected function getTerminalWidth() : int { // from environment if (isset($_SERVER['COLUMNS'])) return (int)$_SERVER['COLUMNS']; // via tput - $process = proc_open('tput cols', array( - 1 => array('pipe', 'w'), - 2 => array('pipe', 'w'), - ), $pipes); - $width = (int)stream_get_contents($pipes[1]); - proc_close($process); - - return $width; + $width = shell_exec( 'tput cols' ); + if( $width != (int)$width ) + { + return 80; + } + + return (int)$width; } /** @@ -116,7 +115,7 @@ protected function getTerminalWidth() * @return int[] * @throws Exception */ - protected function calculateColLengths($columns) + protected function calculateColLengths(array $columns) : array { $idx = 0; $border = $this->strlen($this->border); diff --git a/tests/ArgumentTest.php b/tests/ArgumentTest.php new file mode 100644 index 0000000..3ed9ffc --- /dev/null +++ b/tests/ArgumentTest.php @@ -0,0 +1,72 @@ +registerCommand('status', 'display status info'); + $options->registerCommand('execute', 'execute an action'); + + // test an option only for status command + $options->registerOption('server', 'server property', 's', 'server', 'status'); + + // register an argument for both commands + $options->registerArgument('flag', 'display a flag', true, ['status','execute']); + + // register an argument for status command + $options->registerArgument('flavor', 'latte flavor', true, 'status'); + + // register an argument for execute command + $options->registerArgument('size', 'size of latte', false, 'execute'); + + return $options; + } + + + public function test_args() + { + $options = $this->getOptions(); + + // command, flag, flavor, size + $options->args = array('status', '--server=123', '1', 'pumpkin-spice', 'grande' ); + $options->parseOptions(); + + $this->assertEquals('status', $options->getCmd()); + $this->assertEquals('123', $options->getOpt('server' ) ); + $this->assertEquals('1', $options->getArgs()[0]); + $this->assertEquals('pumpkin-spice', $options->getArgs()[1]); + $this->assertEquals('grande', $options->getArgs()[2]); + } + + + public function test_args_exception() + { + $this->expectException( 'splitbrain\phpcli\Exception' ); + $options = $this->getOptions(); + + // command, flag, flavor, size + $options->args = array('status', '--notset=123', '1', 'pumpkin-spice', 'grande' ); + $options->parseOptions(); + } + + + public function test_args_command_exception() + { + // in my opinion, this test should fail because we pass arguments that aren't defined + $options = $this->getOptions(); + + // command, flag, flavor, size + $options->args = array('execute', '1', 'pumpkin-spice', 'grande', 'adsf' ); + $options->parseOptions(); + $this->assertEquals('1', $options->getArgs()[0]); + } +} \ No newline at end of file diff --git a/tests/CommandsTest.php b/tests/CommandsTest.php new file mode 100644 index 0000000..56c3284 --- /dev/null +++ b/tests/CommandsTest.php @@ -0,0 +1,78 @@ +registerCommand('status', 'display status info'); + $options->registerCommand('execute', 'execute an action'); + $options->registerOption('long', 'display long lines', 'l', false, ['status','execute']); + + // test an option only for status command + $options->registerOption('server', 'server property', 's', 'server', 'status'); + + // test an option only for execute command + $options->registerOption('username', 'username property', 'u', 'username', 'execute'); + + return $options; + } + + + public function test_command() + { + $options = $this->getOptions(); + + $options->args = array('status', '--long', 'foo'); + $options->parseOptions(); + + $this->assertEquals('status', $options->getCmd()); + $this->assertTrue($options->getOpt('long')); + $this->assertEquals(array('foo'), $options->args); + } + + + public function test_command_again() + { + $options = $this->getOptions(); + + $options->args = array('execute', '--long', 'foo'); + $options->parseOptions(); + + $this->assertEquals('execute', $options->getCmd()); + $this->assertTrue($options->getOpt('long')); + $this->assertEquals(array('foo'), $options->args); + } + + + public function test_command_exception() + { + $this->expectException( 'splitbrain\phpcli\Exception' ); + $options = $this->getOptions(); + + // should throw exception for an unregistered command + $options->args = array('foo', '--long', 'foo', '--username=123'); + $options->parseOptions(); + } + + + public function test_argument_exception() + { + $this->expectException( 'splitbrain\phpcli\Exception' ); + $options = $this->getOptions(); + + // should throw exception + $options->args = array('status', '-u','123' ); + $options->parseOptions(); + } + + +} \ No newline at end of file diff --git a/tests/OptionsTest.php b/tests/OptionsTest.php index 47d440b..7adaa01 100644 --- a/tests/OptionsTest.php +++ b/tests/OptionsTest.php @@ -8,7 +8,7 @@ class Options extends \splitbrain\phpcli\Options public $args; } -class OptionsTest extends \PHPUnit_Framework_TestCase +class OptionsTest extends \PHPUnit\Framework\TestCase { function test_simpleshort() @@ -66,4 +66,4 @@ function test_complex() $this->assertTrue($options->getOpt('long')); $this->assertEquals(array('foo'), $options->args); } -} \ No newline at end of file +} diff --git a/tests/TableFormatterTest.php b/tests/TableFormatterTest.php index ae2ac95..3ba986a 100644 --- a/tests/TableFormatterTest.php +++ b/tests/TableFormatterTest.php @@ -1,4 +1,4 @@ -format([5, '*'], [$col1, $col2]); $this->assertEquals($expect, $result); } -} \ No newline at end of file +}