Browse Source

Enable subcommands to use spaces.

Right now explicit subcommands must use a non-space character. For
example `bake:test` and `bake.test` work, however `bake test` will not
work. This change enables commands up with upto 3 spaces to work.
I limited to 3 as I don't think more than 3 will be frequently used and
I wanted to cut down on wasted work.
Mark Story 7 years ago
parent
commit
0c8ce430a2
2 changed files with 75 additions and 3 deletions
  1. 32 3
      src/Console/CommandRunner.php
  2. 43 0
      tests/TestCase/Console/CommandRunnerTest.php

+ 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)
     {

+ 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