diff --git a/lib/Cake/Console/Command/CommandListShell.php b/lib/Cake/Console/Command/CommandListShell.php index 6dfe1d699..40cc8fc28 100644 --- a/lib/Cake/Console/Command/CommandListShell.php +++ b/lib/Cake/Console/Command/CommandListShell.php @@ -24,6 +24,13 @@ App::uses('Inflector', 'Utility'); */ class CommandListShell extends AppShell { +/** + * Contains tasks to load and instantiate + * + * @var array + */ + public $tasks = array('Command'); + /** * startup * @@ -55,7 +62,7 @@ class CommandListShell extends AppShell { $this->out(__d('cake_console', "Available Shells:"), 2); } - $shellList = $this->_getShellList(); + $shellList = $this->Command->getShellList(); if (empty($shellList)) { return; } @@ -67,48 +74,6 @@ class CommandListShell extends AppShell { } } -/** - * Gets the shell command listing. - * - * @return array - */ - protected function _getShellList() { - $skipFiles = array('AppShell'); - - $plugins = CakePlugin::loaded(); - $shellList = array_fill_keys($plugins, null) + array('CORE' => null, 'app' => null); - - $corePath = App::core('Console/Command'); - $shells = App::objects('file', $corePath[0]); - $shells = array_diff($shells, $skipFiles); - $this->_appendShells('CORE', $shells, $shellList); - - $appShells = App::objects('Console/Command', null, false); - $appShells = array_diff($appShells, $shells, $skipFiles); - $this->_appendShells('app', $appShells, $shellList); - - foreach ($plugins as $plugin) { - $pluginShells = App::objects($plugin . '.Console/Command'); - $this->_appendShells($plugin, $pluginShells, $shellList); - } - - return array_filter($shellList); - } - -/** - * Scan the provided paths for shells, and append them into $shellList - * - * @param string $type - * @param array $shells - * @param array $shellList - * @return void - */ - protected function _appendShells($type, $shells, &$shellList) { - foreach ($shells as $shell) { - $shellList[$type][] = Inflector::underscore(str_replace('Shell', '', $shell)); - } - } - /** * Output text. * diff --git a/lib/Cake/Console/Command/CompletionShell.php b/lib/Cake/Console/Command/CompletionShell.php new file mode 100644 index 000000000..94e809efa --- /dev/null +++ b/lib/Cake/Console/Command/CompletionShell.php @@ -0,0 +1,155 @@ +out($this->getOptionParser()->help()); + } + +/** + * list commands + * + * @return void + */ + public function commands() { + $options = $this->Command->commands(); + return $this->_output($options); + } + +/** + * list options for the named command + * + * @return void + */ + public function options() { + $commandName = ''; + if (!empty($this->args[0])) { + $commandName = $this->args[0]; + } + $options = $this->Command->options($commandName); + + return $this->_output($options); + } + +/** + * list subcommands for the named command + * + * @return void + */ + public function subCommands() { + if (!$this->args) { + return $this->_output(); + } + + $options = $this->Command->subCommands($this->args[0]); + return $this->_output($options); + } + +/** + * Guess autocomplete from the whole argument string + * + * @return void + */ + public function fuzzy() { + return $this->_output(); + } + +/** + * getOptionParser for _this_ shell + * + * @return ConsoleOptionParser + */ + public function getOptionParser() { + $parser = parent::getOptionParser(); + + $parser->description(__d('cake_console', 'Used by shells like bash to autocomplete command name, options and arguments')) + ->addSubcommand('commands', array( + 'help' => __d('cake_console', 'Output a list of available commands'), + 'parser' => array( + 'description' => __d('cake_console', 'List all availables'), + 'arguments' => array( + ) + ) + ))->addSubcommand('subcommands', array( + 'help' => __d('cake_console', 'Output a list of available subcommands'), + 'parser' => array( + 'description' => __d('cake_console', 'List subcommands for a command'), + 'arguments' => array( + 'command' => array( + 'help' => __d('cake_console', 'The command name'), + 'required' => true, + ) + ) + ) + ))->addSubcommand('options', array( + 'help' => __d('cake_console', 'Output a list of available options'), + 'parser' => array( + 'description' => __d('cake_console', 'List options'), + 'arguments' => array( + 'command' => array( + 'help' => __d('cake_console', 'The command name'), + 'required' => false, + ) + ) + ) + ))->epilog( + array( + __d('cake_console', 'This command is not intended to be called manually'), + ) + ); + return $parser; + } + +/** + * Emit results as a string, space delimited + * + * @param array $options + * @return void + */ + protected function _output($options = array()) { + if ($options) { + return $this->out(implode($options, ' ')); + } + } +} diff --git a/lib/Cake/Console/Command/Task/CommandTask.php b/lib/Cake/Console/Command/Task/CommandTask.php new file mode 100644 index 000000000..607d318d1 --- /dev/null +++ b/lib/Cake/Console/Command/Task/CommandTask.php @@ -0,0 +1,183 @@ + null, 'app' => null); + + $corePath = App::core('Console/Command'); + $shells = App::objects('file', $corePath[0]); + $shells = array_diff($shells, $skipFiles); + $this->_appendShells('CORE', $shells, $shellList); + + $appShells = App::objects('Console/Command', null, false); + $appShells = array_diff($appShells, $shells, $skipFiles); + $this->_appendShells('app', $appShells, $shellList); + + foreach ($plugins as $plugin) { + $pluginShells = App::objects($plugin . '.Console/Command'); + $this->_appendShells($plugin, $pluginShells, $shellList); + } + + return array_filter($shellList); + } + +/** + * Scan the provided paths for shells, and append them into $shellList + * + * @param string $type + * @param array $shells + * @param array $shellList + * @return void + */ + protected function _appendShells($type, $shells, &$shellList) { + foreach ($shells as $shell) { + $shellList[$type][] = Inflector::underscore(str_replace('Shell', '', $shell)); + } + } + +/** + * Return a list of all commands + * + * @return array + */ + public function commands() { + $shellList = $this->getShellList(); + + $options = array(); + foreach ($shellList as $type => $commands) { + $prefix = ''; + if (!in_array(strtolower($type), array('app', 'core'))) { + $prefix = $type . '.'; + } + + foreach ($commands as $shell) { + $options[] = $prefix . $shell; + } + } + + return $options; + } + +/** + * Return a list of subcommands for a given command + * + * @param string $commandName + * @return array + */ + public function subCommands($commandName) { + $Shell = $this->getShell($commandName); + + if (!$Shell) { + return array(); + } + + $taskMap = TaskCollection::normalizeObjectArray((array)$Shell->tasks); + $return = array_keys($taskMap); + $return = array_map('Inflector::underscore', $return); + + $ShellReflection = new ReflectionClass('AppShell'); + $shellMethods = $ShellReflection->getMethods(ReflectionMethod::IS_PUBLIC); + $shellMethodNames = array('main', 'help'); + foreach ($shellMethods as $method) { + $shellMethodNames[] = $method->getName(); + } + + $Reflection = new ReflectionClass($Shell); + $methods = $Reflection->getMethods(ReflectionMethod::IS_PUBLIC); + $methodNames = array(); + foreach ($methods as $method) { + $methodNames[] = $method->getName(); + } + + $return += array_diff($methodNames, $shellMethodNames); + sort($return); + + return $return; + } + +/** + * Get Shell instance for the given command + * + * @param mixed $commandName + * @return mixed + */ + public function getShell($commandName) { + list($pluginDot, $name) = pluginSplit($commandName, true); + + if (in_array(strtolower($pluginDot), array('app.', 'core.'))) { + $commandName = $name; + $pluginDot = ''; + } + + if (!in_array($commandName, $this->commands())) { + return false; + } + + $name = Inflector::camelize($name); + $pluginDot = Inflector::camelize($pluginDot); + $class = $name . 'Shell'; + APP::uses($class, $pluginDot . 'Console/Command'); + + $Shell = new $class(); + $Shell->plugin = trim($pluginDot, '.'); + $Shell->initialize(); + + return $Shell; + } + +/** + * Get Shell instance for the given command + * + * @param mixed $commandName + * @return array + */ + public function options($commandName) { + $Shell = $this->getShell($commandName); + if (!$Shell) { + $parser = new ConsoleOptionParser(); + } else { + $parser = $Shell->getOptionParser(); + } + + $options = array(); + $array = $parser->options(); + foreach ($array as $name => $obj) { + $options[] = "--$name"; + $short = $obj->short(); + if ($short) { + $options[] = "-$short"; + } + } + return $options; + } + +} diff --git a/lib/Cake/Test/Case/Console/Command/CommandListShellTest.php b/lib/Cake/Test/Case/Console/Command/CommandListShellTest.php index ea81b3db1..05d145c7f 100644 --- a/lib/Cake/Test/Case/Console/Command/CommandListShellTest.php +++ b/lib/Cake/Test/Case/Console/Command/CommandListShellTest.php @@ -22,6 +22,7 @@ App::uses('CommandListShell', 'Console/Command'); App::uses('ConsoleOutput', 'Console'); App::uses('ConsoleInput', 'Console'); App::uses('Shell', 'Console'); +App::uses('CommandTask', 'Console/Command/Task'); /** * Class TestStringOutput @@ -70,6 +71,12 @@ class CommandListShellTest extends CakeTestCase { array('in', '_stop', 'clear'), array($out, $out, $in) ); + + $this->Shell->Command = $this->getMock( + 'CommandTask', + array('in', '_stop', 'clear'), + array($out, $out, $in) + ); } /** @@ -98,7 +105,7 @@ class CommandListShellTest extends CakeTestCase { $expected = "/\[.*TestPluginTwo.*\] example, welcome/"; $this->assertRegExp($expected, $output); - $expected = "/\[.*CORE.*\] acl, api, bake, command_list, console, i18n, schema, server, test, testsuite, upgrade/"; + $expected = "/\[.*CORE.*\] acl, api, bake, command_list, completion, console, i18n, schema, server, test, testsuite, upgrade/"; $this->assertRegExp($expected, $output); $expected = "/\[.*app.*\] sample/"; diff --git a/lib/Cake/Test/Case/Console/Command/CompletionShellTest.php b/lib/Cake/Test/Case/Console/Command/CompletionShellTest.php new file mode 100644 index 000000000..7ef3ef53a --- /dev/null +++ b/lib/Cake/Test/Case/Console/Command/CompletionShellTest.php @@ -0,0 +1,261 @@ +output .= $message; + } + +} + +/** + * Class CompletionShellTest + * + * @package Cake.Test.Case.Console.Command + */ +class CompletionShellTest extends CakeTestCase { + +/** + * setUp method + * + * @return void + */ + public function setUp() { + parent::setUp(); + App::build(array( + 'Plugin' => array( + CAKE . 'Test' . DS . 'test_app' . DS . 'Plugin' . DS + ), + 'Console/Command' => array( + CAKE . 'Test' . DS . 'test_app' . DS . 'Console' . DS . 'Command' . DS + ) + ), App::RESET); + CakePlugin::load(array('TestPlugin', 'TestPluginTwo')); + + $out = new TestCompletionStringOutput(); + $in = $this->getMock('ConsoleInput', array(), array(), '', false); + + $this->Shell = $this->getMock( + 'CompletionShell', + array('in', '_stop', 'clear'), + array($out, $out, $in) + ); + + $this->Shell->Command = $this->getMock( + 'CommandTask', + array('in', '_stop', 'clear'), + array($out, $out, $in) + ); + } + +/** + * tearDown + * + * @return void + */ + public function tearDown() { + parent::tearDown(); + unset($this->Shell); + CakePlugin::unload(); + } + +/** + * test that the startup method supresses the shell header + * + * @return void + */ + public function testStartup() { + $this->Shell->runCommand('main', array()); + $output = $this->Shell->stdout->output; + + $needle = 'Welcome to CakePHP'; + $this->assertTextNotContains($needle, $output); + } + +/** + * test that main displays a warning + * + * @return void + */ + public function testMain() { + $this->Shell->runCommand('main', array()); + $output = $this->Shell->stdout->output; + + $expected = "/This command is not intended to be called manually/"; + $this->assertRegExp($expected, $output); + } + +/** + * test commands method that list all available commands + * + * @return void + */ + public function testCommands() { + $this->Shell->runCommand('commands', array()); + $output = $this->Shell->stdout->output; + + $expected = "TestPlugin.example TestPluginTwo.example TestPluginTwo.welcome acl api bake command_list completion console i18n schema server test testsuite upgrade sample\n"; + $this->assertEquals($expected, $output); + } + +/** + * test that options without argument returns the default options + * + * @return void + */ + public function testOptionsNoArguments() { + $this->Shell->runCommand('options', array()); + $output = $this->Shell->stdout->output; + + $expected = "--help -h --verbose -v --quiet -q\n"; + $this->assertEquals($expected, $output); + } + +/** + * test that options with a nonexisting command returns the default options + * + * @return void + */ + public function testOptionsNonExistingCommand() { + $this->Shell->runCommand('options', array('options', 'foo')); + $output = $this->Shell->stdout->output; + + $expected = "--help -h --verbose -v --quiet -q\n"; + $this->assertEquals($expected, $output); + } + +/** + * test that options with a existing command returns the proper options + * + * @return void + */ + public function testOptions() { + $this->Shell->runCommand('options', array('options', 'bake')); + $output = $this->Shell->stdout->output; + + $expected = "--help -h --verbose -v --quiet -q --connection -c --theme -t\n"; + $this->assertEquals($expected, $output); + } + +/** + * test that subCommands with a existing CORE command returns the proper sub commands + * + * @return void + */ + public function testSubCommandsCorePlugin() { + $this->Shell->runCommand('subCommands', array('subCommands', 'CORE.bake')); + $output = $this->Shell->stdout->output; + + $expected = "controller db_config fixture model plugin project test view\n"; + $this->assertEquals($expected, $output); + } + +/** + * test that subCommands with a existing APP command returns the proper sub commands (in this case none) + * + * @return void + */ + public function testSubCommandsAppPlugin() { + $this->Shell->runCommand('subCommands', array('subCommands', 'app.sample')); + $output = $this->Shell->stdout->output; + + $expected = ''; + $this->assertEquals($expected, $output); + } + +/** + * test that subCommands with a existing plugin command returns the proper sub commands + * + * @return void + */ + public function testSubCommandsPlugin() { + $this->Shell->runCommand('subCommands', array('subCommands', 'TestPluginTwo.welcome')); + $output = $this->Shell->stdout->output; + + $expected = "say_hello\n"; + $this->assertEquals($expected, $output); + } + +/** + * test that subcommands without arguments returns nothing + * + * @return void + */ + public function testSubCommandsNoArguments() { + $this->Shell->runCommand('subCommands', array()); + $output = $this->Shell->stdout->output; + + $expected = ''; + $this->assertEquals($expected, $output); + } + +/** + * test that subcommands with a nonexisting command returns nothing + * + * @return void + */ + public function testSubCommandsNonExistingCommand() { + $this->Shell->runCommand('subCommands', array('subCommands', 'foo')); + $output = $this->Shell->stdout->output; + + $expected = ''; + $this->assertEquals($expected, $output); + } + +/** + * test that subcommands returns the available subcommands for the given command + * + * @return void + */ + public function testSubCommands() { + $this->Shell->runCommand('subCommands', array('subCommands', 'bake')); + $output = $this->Shell->stdout->output; + + $expected = "controller db_config fixture model plugin project test view\n"; + $this->assertEquals($expected, $output); + } + +/** + * test that fuzzy returns nothing + * + * @return void + */ + public function testFuzzy() { + $this->Shell->runCommand('fuzzy', array()); + $output = $this->Shell->stdout->output; + + $expected = ''; + $this->assertEquals($expected, $output); + } +} diff --git a/lib/Cake/Test/Case/Console/Command/Task/CommandTaskTest.php b/lib/Cake/Test/Case/Console/Command/Task/CommandTaskTest.php new file mode 100644 index 000000000..6a1293259 --- /dev/null +++ b/lib/Cake/Test/Case/Console/Command/Task/CommandTaskTest.php @@ -0,0 +1,240 @@ + array( + CAKE . 'Test' . DS . 'test_app' . DS . 'Plugin' . DS + ), + 'Console/Command' => array( + CAKE . 'Test' . DS . 'test_app' . DS . 'Console' . DS . 'Command' . DS + ) + ), App::RESET); + CakePlugin::load(array('TestPlugin', 'TestPluginTwo')); + + $out = $this->getMock('ConsoleOutput', array(), array(), '', false); + $in = $this->getMock('ConsoleInput', array(), array(), '', false); + + $this->CommandTask = $this->getMock( + 'CommandTask', + array('in', '_stop', 'clear'), + array($out, $out, $in) + ); + } + +/** + * tearDown + * + * @return void + */ + public function tearDown() { + parent::tearDown(); + unset($this->CommandTask); + CakePlugin::unload(); + } + +/** + * Test the resulting list of shells + * + * @return void + */ + public function testGetShellList() { + $result = $this->CommandTask->getShellList(); + + $expected = array( + 'CORE' => array( + 'acl', + 'api', + 'bake', + 'command_list', + 'completion', + 'console', + 'i18n', + 'schema', + 'server', + 'test', + 'testsuite', + 'upgrade' + ), + 'TestPlugin' => array( + 'example' + ), + 'TestPluginTwo' => array( + 'example', + 'welcome' + ), + 'app' => array( + 'sample' + ), + ); + $this->assertEquals($expected, $result); + } + +/** + * Test the resulting list of commands + * + * @return void + */ + public function testCommands() { + $result = $this->CommandTask->commands(); + + $expected = array( + 'TestPlugin.example', + 'TestPluginTwo.example', + 'TestPluginTwo.welcome', + 'acl', + 'api', + 'bake', + 'command_list', + 'completion', + 'console', + 'i18n', + 'schema', + 'server', + 'test', + 'testsuite', + 'upgrade', + 'sample' + ); + $this->assertEquals($expected, $result); + } + +/** + * Test the resulting list of subcommands for the given command + * + * @return void + */ + public function testSubCommands() { + $result = $this->CommandTask->subCommands('acl'); + + $expected = array( + 'check', + 'create', + 'db_config', + 'delete', + 'deny', + 'getPath', + 'grant', + 'inherit', + 'initdb', + 'nodeExists', + 'parseIdentifier', + 'setParent', + 'view' + ); + $this->assertEquals($expected, $result); + } + +/** + * Test that unknown commands return an empty array + * + * @return void + */ + public function testSubCommandsUnknownCommand() { + $result = $this->CommandTask->subCommands('yoghurt'); + + $expected = array(); + $this->assertEquals($expected, $result); + } + +/** + * Test that getting a existing shell returns the shell instance + * + * @return void + */ + public function testGetShell() { + $result = $this->CommandTask->getShell('acl'); + $this->assertInstanceOf('AclShell', $result); + } + +/** + * Test that getting a non-existing shell returns false + * + * @return void + */ + public function testGetShellNonExisting() { + $result = $this->CommandTask->getShell('strawberry'); + $this->assertFalse($result); + } + +/** + * Test that getting a existing core shell with 'core.' prefix returns the correct shell instance + * + * @return void + */ + public function testGetShellCore() { + $result = $this->CommandTask->getShell('core.bake'); + $this->assertInstanceOf('BakeShell', $result); + } + +/** + * Test the options array for a known command + * + * @return void + */ + public function testOptions() { + $result = $this->CommandTask->options('bake'); + + $expected = array( + '--help', + '-h', + '--verbose', + '-v', + '--quiet', + '-q', + '--connection', + '-c', + '--theme', + '-t' + ); + $this->assertEquals($expected, $result); + } + +/** + * Test the options array for an unknown command + * + * @return void + */ + public function testOptionsUnknownCommand() { + $result = $this->CommandTask->options('pie'); + + $expected = array( + '--help', + '-h', + '--verbose', + '-v', + '--quiet', + '-q' + ); + $this->assertEquals($expected, $result); + } + +}