diff --git a/Application.php b/Application.php
index 2ea7e4469..9cdcef88c 100644
--- a/Application.php
+++ b/Application.php
@@ -287,6 +287,10 @@ public function doRun(InputInterface $input, OutputInterface $output)
$command = $this->find($alternative);
}
+ if ($command instanceof LazyCommand) {
+ $command = $command->getCommand();
+ }
+
$this->runningCommand = $command;
$exitCode = $this->doRunCommand($command, $input, $output);
$this->runningCommand = null;
@@ -798,7 +802,7 @@ public function renderThrowable(\Throwable $e, OutputInterface $output): void
$this->doRenderThrowable($e, $output);
if (null !== $this->runningCommand) {
- $output->writeln(sprintf('%s', sprintf($this->runningCommand->getSynopsis(), $this->getName())), OutputInterface::VERBOSITY_QUIET);
+ $output->writeln(sprintf('%s', OutputFormatter::escape(sprintf($this->runningCommand->getSynopsis(), $this->getName()))), OutputInterface::VERBOSITY_QUIET);
$output->writeln('', OutputInterface::VERBOSITY_QUIET);
}
}
diff --git a/Command/Command.php b/Command/Command.php
index e35ae51eb..71bf5d7c3 100644
--- a/Command/Command.php
+++ b/Command/Command.php
@@ -101,7 +101,18 @@ public function __construct(string $name = null)
{
$this->definition = new InputDefinition();
- if (null !== $name || null !== $name = static::getDefaultName()) {
+ if (null === $name && null !== $name = static::getDefaultName()) {
+ $aliases = explode('|', $name);
+
+ if ('' === $name = array_shift($aliases)) {
+ $this->setHidden(true);
+ $name = array_shift($aliases);
+ }
+
+ $this->setAliases($aliases);
+ }
+
+ if (null !== $name) {
$this->setName($name);
}
diff --git a/Helper/ProgressBar.php b/Helper/ProgressBar.php
index 19479271c..91fba2b58 100644
--- a/Helper/ProgressBar.php
+++ b/Helper/ProgressBar.php
@@ -206,7 +206,7 @@ public function getProgressPercent(): float
public function getBarOffset(): float
{
- return floor($this->max ? $this->percent * $this->barWidth : (null === $this->redrawFreq ? min(5, $this->barWidth / 15) * $this->writeCount : $this->step) % $this->barWidth);
+ return floor($this->max ? $this->percent * $this->barWidth : (null === $this->redrawFreq ? (int) (min(5, $this->barWidth / 15) * $this->writeCount) : $this->step) % $this->barWidth);
}
public function getEstimated(): float
@@ -280,7 +280,7 @@ public function setFormat(string $format)
/**
* Sets the redraw frequency.
*
- * @param int|float $freq The frequency in steps
+ * @param int|null $freq The frequency in steps
*/
public function setRedrawFrequency(?int $freq)
{
diff --git a/Helper/Table.php b/Helper/Table.php
index 4bf3ed396..a592cdff5 100644
--- a/Helper/Table.php
+++ b/Helper/Table.php
@@ -442,7 +442,7 @@ private function renderRowSeparator(int $type = self::SEPARATOR_MID, string $tit
$formattedTitle = sprintf($titleFormat, Helper::substr($title, 0, $limit - $formatLength - 3).'...');
}
- $titleStart = ($markupLength - $titleLength) / 2;
+ $titleStart = intdiv($markupLength - $titleLength, 2);
if (false === mb_detect_encoding($markup, null, true)) {
$markup = substr_replace($markup, $formattedTitle, $titleStart, $titleLength);
} else {
diff --git a/Input/Input.php b/Input/Input.php
index dd7658094..3b054c4b6 100644
--- a/Input/Input.php
+++ b/Input/Input.php
@@ -146,6 +146,14 @@ public function getOptions()
*/
public function getOption(string $name)
{
+ if ($this->definition->hasNegation($name)) {
+ if (null === $value = $this->getOption($this->definition->negationToName($name))) {
+ return $value;
+ }
+
+ return !$value;
+ }
+
if (!$this->definition->hasOption($name)) {
throw new InvalidArgumentException(sprintf('The "%s" option does not exist.', $name));
}
@@ -158,7 +166,11 @@ public function getOption(string $name)
*/
public function setOption(string $name, $value)
{
- if (!$this->definition->hasOption($name)) {
+ if ($this->definition->hasNegation($name)) {
+ $this->options[$this->definition->negationToName($name)] = !$value;
+
+ return;
+ } elseif (!$this->definition->hasOption($name)) {
throw new InvalidArgumentException(sprintf('The "%s" option does not exist.', $name));
}
@@ -170,7 +182,7 @@ public function setOption(string $name, $value)
*/
public function hasOption(string $name)
{
- return $this->definition->hasOption($name);
+ return $this->definition->hasOption($name) || $this->definition->hasNegation($name);
}
/**
diff --git a/Tests/ApplicationTest.php b/Tests/ApplicationTest.php
index 7bf1e5700..361e174cd 100644
--- a/Tests/ApplicationTest.php
+++ b/Tests/ApplicationTest.php
@@ -15,6 +15,7 @@
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Command\HelpCommand;
+use Symfony\Component\Console\Command\LazyCommand;
use Symfony\Component\Console\Command\SignalableCommandInterface;
use Symfony\Component\Console\CommandLoader\FactoryCommandLoader;
use Symfony\Component\Console\DependencyInjection\AddConsoleCommandPass;
@@ -927,6 +928,23 @@ public function testRenderExceptionStackTraceContainsRootException()
$this->assertStringContainsString('Dummy type "class@anonymous" is invalid.', $tester->getDisplay(true));
}
+ public function testRenderExceptionEscapesLinesOfSynopsis()
+ {
+ $application = new Application();
+ $application->setAutoExit(false);
+ $application
+ ->register('foo')
+ ->setCode(function () {
+ throw new \Exception('some exception');
+ })
+ ->addArgument('info')
+ ;
+ $tester = new ApplicationTester($application);
+
+ $tester->run(['command' => 'foo'], ['decorated' => false]);
+ $this->assertStringMatchesFormatFile(self::$fixturesPath.'/application_rendersynopsis_escapesline.txt', $tester->getDisplay(true), '->renderException() escapes lines containing formatting of synopsis');
+ }
+
public function testRun()
{
$application = new Application();
@@ -1655,7 +1673,7 @@ public function testRunLazyCommandService()
$container = new ContainerBuilder();
$container->addCompilerPass(new AddConsoleCommandPass());
$container
- ->register('lazy-command', LazyCommand::class)
+ ->register('lazy-command', LazyTestCommand::class)
->addTag('console.command', ['command' => 'lazy:command'])
->addTag('console.command', ['command' => 'lazy:alias'])
->addTag('console.command', ['command' => 'lazy:alias2']);
@@ -1830,7 +1848,7 @@ public function testSignal()
$application->setAutoExit(false);
$application->setDispatcher($dispatcher);
$application->setSignalsToDispatchEvent(\SIGALRM);
- $application->add($command);
+ $application->add(new LazyCommand('signal', [], '', false, function () use ($command) { return $command; }, true));
$this->assertFalse($command->signaled);
$this->assertFalse($dispatcherCalled);
@@ -1885,7 +1903,7 @@ public function __construct()
}
}
-class LazyCommand extends Command
+class LazyTestCommand extends Command
{
public function execute(InputInterface $input, OutputInterface $output): int
{
diff --git a/Tests/Command/CommandTest.php b/Tests/Command/CommandTest.php
index fad1f3458..839beac3f 100644
--- a/Tests/Command/CommandTest.php
+++ b/Tests/Command/CommandTest.php
@@ -186,7 +186,8 @@ public function testGetSynopsis()
$command = new \TestCommand();
$command->addOption('foo');
$command->addArgument('bar');
- $this->assertEquals('namespace:name [--foo] [--] []', $command->getSynopsis(), '->getSynopsis() returns the synopsis');
+ $command->addArgument('info');
+ $this->assertEquals('namespace:name [--foo] [--] [ []]', $command->getSynopsis(), '->getSynopsis() returns the synopsis');
}
public function testAddGetUsages()
@@ -413,6 +414,13 @@ public function testCommandAttribute()
{
$this->assertSame('|foo|f', Php8Command::getDefaultName());
$this->assertSame('desc', Php8Command::getDefaultDescription());
+
+ $command = new Php8Command();
+
+ $this->assertSame('foo', $command->getName());
+ $this->assertSame('desc', $command->getDescription());
+ $this->assertTrue($command->isHidden());
+ $this->assertSame(['f'], $command->getAliases());
}
}
diff --git a/Tests/Fixtures/application_rendersynopsis_escapesline.txt b/Tests/Fixtures/application_rendersynopsis_escapesline.txt
new file mode 100644
index 000000000..a781326c6
--- /dev/null
+++ b/Tests/Fixtures/application_rendersynopsis_escapesline.txt
@@ -0,0 +1,7 @@
+
+In ApplicationTest.php line %d:
+
+ some exception
+
+
+foo []
diff --git a/Tests/Helper/ProgressBarTest.php b/Tests/Helper/ProgressBarTest.php
index 701b50960..7cf7ca770 100644
--- a/Tests/Helper/ProgressBarTest.php
+++ b/Tests/Helper/ProgressBarTest.php
@@ -535,7 +535,7 @@ public function testRedrawFrequencyIsAtLeastOneIfZeroGiven()
public function testRedrawFrequencyIsAtLeastOneIfSmallerOneGiven()
{
$bar = new ProgressBar($output = $this->getOutputStream(), 0, 0);
- $bar->setRedrawFrequency(0.9);
+ $bar->setRedrawFrequency(0);
$bar->start();
$bar->advance();
diff --git a/Tests/Input/InputTest.php b/Tests/Input/InputTest.php
index 48c287cd9..6547822fb 100644
--- a/Tests/Input/InputTest.php
+++ b/Tests/Input/InputTest.php
@@ -45,6 +45,20 @@ public function testOptions()
$input = new ArrayInput(['--name' => 'foo', '--bar' => null], new InputDefinition([new InputOption('name'), new InputOption('bar', '', InputOption::VALUE_OPTIONAL, '', 'default')]));
$this->assertNull($input->getOption('bar'), '->getOption() returns null for options explicitly passed without value (or an empty value)');
$this->assertEquals(['name' => 'foo', 'bar' => null], $input->getOptions(), '->getOptions() returns all option values');
+
+ $input = new ArrayInput(['--name' => null], new InputDefinition([new InputOption('name', null, InputOption::VALUE_NEGATABLE)]));
+ $this->assertTrue($input->hasOption('name'));
+ $this->assertTrue($input->hasOption('no-name'));
+ $this->assertTrue($input->getOption('name'));
+ $this->assertFalse($input->getOption('no-name'));
+
+ $input = new ArrayInput(['--no-name' => null], new InputDefinition([new InputOption('name', null, InputOption::VALUE_NEGATABLE)]));
+ $this->assertFalse($input->getOption('name'));
+ $this->assertTrue($input->getOption('no-name'));
+
+ $input = new ArrayInput([], new InputDefinition([new InputOption('name', null, InputOption::VALUE_NEGATABLE)]));
+ $this->assertNull($input->getOption('name'));
+ $this->assertNull($input->getOption('no-name'));
}
public function testSetInvalidOption()