Skip to content

Commit

Permalink
Merge pull request #12824 from cakephp/command-runner-space
Browse files Browse the repository at this point in the history
Enable subcommands to use spaces.
  • Loading branch information
markstory committed Dec 19, 2018
2 parents 8e5a24b + b05bc8d commit e91b96a
Show file tree
Hide file tree
Showing 4 changed files with 114 additions and 3 deletions.
5 changes: 5 additions & 0 deletions src/Console/CommandCollection.php
Expand Up @@ -64,6 +64,11 @@ public function add($name, $command)
"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;

Expand Down
35 changes: 32 additions & 3 deletions src/Console/CommandRunner.php
Expand Up @@ -156,7 +156,9 @@ public function run(array $argv, ConsoleIo $io = null)
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);
Expand Down Expand Up @@ -295,15 +297,42 @@ protected function getShell(ConsoleIo $io, CommandCollection $commands, $name)
return $instance;
}

/**
* 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)
{
Expand Down
34 changes: 34 additions & 0 deletions tests/TestCase/Console/CommandCollectionTest.php
Expand Up @@ -20,6 +20,7 @@
use Cake\Shell\I18nShell;
use Cake\Shell\RoutesShell;
use Cake\TestSuite\TestCase;
use InvalidArgumentException;
use stdClass;
use TestApp\Command\DemoCommand;

Expand Down Expand Up @@ -136,6 +137,39 @@ public function testAddInvalidInstance()
$collection->add('routes', $shell);
}

/**
* 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
*
Expand Down
43 changes: 43 additions & 0 deletions tests/TestCase/Console/CommandRunnerTest.php
Expand Up @@ -27,6 +27,7 @@
use Cake\TestSuite\Stub\ConsoleOutput;
use Cake\TestSuite\TestCase;
use InvalidArgumentException;
use TestApp\Command\AbortCommand;
use TestApp\Command\DemoCommand;
use TestApp\Shell\SampleShell;

Expand Down Expand Up @@ -379,6 +380,48 @@ public function testRunValidCommandClass()
$this->assertContains('Demo Command!', $messages);
}

/**
* 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
*
Expand Down

0 comments on commit e91b96a

Please sign in to comment.