Browse Source

Merge pull request #12824 from cakephp/command-runner-space

Enable subcommands to use spaces.
Mark Story 7 years ago
parent
commit
e91b96a3ec

+ 5 - 0
src/Console/CommandCollection.php

@@ -64,6 +64,11 @@ class CommandCollection implements IteratorAggregate, Countable
                 "Cannot use '$class' for command '$name' it is not a subclass of Cake\Console\Shell or Cake\Console\Command."
             );
         }
+        if (!preg_match('/^[^\s]+(?:(?: [^\s]+){1,2})?$/ui', $name)) {
+            throw new InvalidArgumentException(
+                "The command name `{$name}` is invalid. Names can only be a maximum of three words."
+            );
+        }
 
         $this->commands[$name] = $command;
 

+ 32 - 3
src/Console/CommandRunner.php

@@ -156,7 +156,9 @@ class CommandRunner implements EventDispatcherInterface
         array_shift($argv);
 
         $io = $io ?: new ConsoleIo();
-        $name = $this->resolveName($commands, $io, array_shift($argv));
+
+        list($name, $argv) = $this->longestCommandName($commands, $argv);
+        $name = $this->resolveName($commands, $io, $name);
 
         $result = Shell::CODE_ERROR;
         $shell = $this->getShell($io, $commands, $name);
@@ -296,14 +298,41 @@ class CommandRunner implements EventDispatcherInterface
     }
 
     /**
+     * Build the longest command name that exists in the collection
+     *
+     * Build the longest command name that matches a
+     * defined command. This will traverse a maximum of 3 tokens.
+     *
+     * @param \Cake\Console\CommandCollection $commands The command collection to check.
+     * @param array $argv The CLI arguments.
+     * @return array An array of the resolved name and modified argv.
+     */
+    protected function longestCommandName($commands, $argv)
+    {
+        for ($i = 3; $i > 1; $i--) {
+            $parts = array_slice($argv, 0, $i);
+            $name = implode(' ', $parts);
+            if ($commands->has($name)) {
+                return [$name, array_slice($argv, $i)];
+            }
+        }
+        $name = array_shift($argv);
+
+        return [$name, $argv];
+    }
+
+    /**
      * Resolve the command name into a name that exists in the collection.
      *
      * Apply backwards compatible inflections and aliases.
+     * Will step forward up to 3 tokens in $argv to generate
+     * a command name in the CommandCollection. More specific
+     * command names take precedence over less specific ones.
      *
      * @param \Cake\Console\CommandCollection $commands The command collection to check.
      * @param \Cake\Console\ConsoleIo $io ConsoleIo object for errors.
-     * @param string $name The name from the CLI args.
-     * @return string The resolved name.
+     * @param string $name The name
+     * @return string The resolved class name
      */
     protected function resolveName($commands, $io, $name)
     {

+ 34 - 0
tests/TestCase/Console/CommandCollectionTest.php

@@ -20,6 +20,7 @@ use Cake\Core\Plugin;
 use Cake\Shell\I18nShell;
 use Cake\Shell\RoutesShell;
 use Cake\TestSuite\TestCase;
+use InvalidArgumentException;
 use stdClass;
 use TestApp\Command\DemoCommand;
 
@@ -137,6 +138,39 @@ class CommandCollectionTest extends TestCase
     }
 
     /**
+     * Provider for invalid names.
+     *
+     * @return array
+     */
+    public function invalidNameProvider()
+    {
+        return [
+            // Empty
+            [''],
+            // Leading spaces
+            [' spaced'],
+            // Trailing spaces
+            ['spaced '],
+            // Too many words
+            ['one two three four'],
+        ];
+    }
+
+    /**
+     * test adding a command instance.
+     *
+     * @dataProvider invalidNameProvider
+     * @return void
+     */
+    public function testAddCommandInvalidName($name)
+    {
+        $this->expectException(InvalidArgumentException::class);
+        $this->expectExceptionMessage("The command name `$name` is invalid.");
+        $collection = new CommandCollection();
+        $collection->add($name, DemoCommand::class);
+    }
+
+    /**
      * Class names that are not shells should fail
      *
      */

+ 43 - 0
tests/TestCase/Console/CommandRunnerTest.php

@@ -27,6 +27,7 @@ use Cake\Routing\Router;
 use Cake\TestSuite\Stub\ConsoleOutput;
 use Cake\TestSuite\TestCase;
 use InvalidArgumentException;
+use TestApp\Command\AbortCommand;
 use TestApp\Command\DemoCommand;
 use TestApp\Shell\SampleShell;
 
@@ -380,6 +381,48 @@ class CommandRunnerTest extends TestCase
     }
 
     /**
+     * Test running a valid command with spaces in the name
+     *
+     * @return void
+     */
+    public function testRunValidCommandSubcommandName()
+    {
+        $app = $this->makeAppWithCommands([
+            'tool build' => DemoCommand::class,
+            'tool' => AbortCommand::class
+        ]);
+        $output = new ConsoleOutput();
+
+        $runner = new CommandRunner($app, 'cake');
+        $result = $runner->run(['cake', 'tool', 'build'], $this->getMockIo($output));
+        $this->assertSame(Shell::CODE_SUCCESS, $result);
+
+        $messages = implode("\n", $output->messages());
+        $this->assertContains('Demo Command!', $messages);
+    }
+
+    /**
+     * Test running a valid command with spaces in the name
+     *
+     * @return void
+     */
+    public function testRunValidCommandNestedName()
+    {
+        $app = $this->makeAppWithCommands([
+            'tool build assets' => DemoCommand::class,
+            'tool' => AbortCommand::class
+        ]);
+        $output = new ConsoleOutput();
+
+        $runner = new CommandRunner($app, 'cake');
+        $result = $runner->run(['cake', 'tool', 'build', 'assets'], $this->getMockIo($output));
+        $this->assertSame(Shell::CODE_SUCCESS, $result);
+
+        $messages = implode("\n", $output->messages());
+        $this->assertContains('Demo Command!', $messages);
+    }
+
+    /**
      * Test using a custom factory
      *
      * @return void