diff --git a/Attribute/Argument.php b/Attribute/Argument.php
index bca9a2e13..33b7a86b2 100644
--- a/Attribute/Argument.php
+++ b/Attribute/Argument.php
@@ -87,6 +87,8 @@ public static function tryFrom(\ReflectionParameter|\ReflectionProperty $member)
}
if (\is_array($self->suggestedValues) && !\is_callable($self->suggestedValues) && 2 === \count($self->suggestedValues) && ($instance = $reflection->getSourceThis()) && $instance::class === $self->suggestedValues[0] && \is_callable([$instance, $self->suggestedValues[1]])) {
+ // In case that the callback is declared as a static method `[Foo::class, 'methodName']` - yet it is not callable,
+ // while non-static method `[Foo $instance, 'methodName']` would be callable, we transform the callback on the fly into a non-static version.
$self->suggestedValues = [$instance, $self->suggestedValues[1]];
}
diff --git a/DependencyInjection/AddConsoleCommandPass.php b/DependencyInjection/AddConsoleCommandPass.php
index ffbb10e8c..3198f26eb 100644
--- a/DependencyInjection/AddConsoleCommandPass.php
+++ b/DependencyInjection/AddConsoleCommandPass.php
@@ -143,10 +143,11 @@ public function process(ContainerBuilder $container): void
}
if ($description) {
- $definition->addMethodCall('setDescription', [str_replace('%', '%%', $description)]);
+ $escapedDescription = str_replace('%', '%%', $description);
+ $definition->addMethodCall('setDescription', [$escapedDescription]);
$container->register('.'.$id.'.lazy', LazyCommand::class)
- ->setArguments([$commandName, $aliases, $description, $isHidden, new ServiceClosureArgument($lazyCommandRefs[$id])]);
+ ->setArguments([$commandName, $aliases, $escapedDescription, $isHidden, new ServiceClosureArgument($lazyCommandRefs[$id])]);
$lazyCommandRefs[$id] = new Reference('.'.$id.'.lazy');
}
diff --git a/Helper/QuestionHelper.php b/Helper/QuestionHelper.php
index 1f0cc1ca3..80e478f16 100644
--- a/Helper/QuestionHelper.php
+++ b/Helper/QuestionHelper.php
@@ -306,7 +306,7 @@ private function autocomplete(OutputInterface $output, Question $question, $inpu
if ($numMatches > 0 && -1 !== $ofs) {
$ret = (string) $matches[$ofs];
// Echo out remaining chars for current match
- $remainingCharacters = substr($ret, \strlen(trim($this->mostRecentlyEnteredValue($fullChoice))));
+ $remainingCharacters = substr($ret, \strlen($this->mostRecentlyEnteredValue($fullChoice)));
$output->write($remainingCharacters);
$fullChoice .= $remainingCharacters;
$i = (false === $encoding = mb_detect_encoding($fullChoice, null, true)) ? \strlen($fullChoice) : mb_strlen($fullChoice, $encoding);
@@ -360,7 +360,7 @@ private function autocomplete(OutputInterface $output, Question $question, $inpu
if ($numMatches > 0 && -1 !== $ofs) {
$cursor->savePosition();
// Write highlighted text, complete the partially entered response
- $charactersEntered = \strlen(trim($this->mostRecentlyEnteredValue($fullChoice)));
+ $charactersEntered = \strlen($this->mostRecentlyEnteredValue($fullChoice));
$output->write(''.OutputFormatter::escapeTrailingBackslash(substr($matches[$ofs], $charactersEntered)).'');
$cursor->restorePosition();
}
diff --git a/Helper/SymfonyQuestionHelper.php b/Helper/SymfonyQuestionHelper.php
index b452bf047..fb03fe615 100644
--- a/Helper/SymfonyQuestionHelper.php
+++ b/Helper/SymfonyQuestionHelper.php
@@ -31,7 +31,7 @@ protected function writePrompt(OutputInterface $output, Question $question): voi
$default = $question->getDefault();
if ($question->isMultiline()) {
- $text .= \sprintf(' (press %s to continue)', $this->getEofShortcut());
+ $text .= \sprintf(' (press %s to continue)', $this->getEofShortcut($output));
}
switch (true) {
@@ -92,9 +92,9 @@ protected function writeError(OutputInterface $output, \Exception $error): void
parent::writeError($output, $error);
}
- private function getEofShortcut(): string
+ private function getEofShortcut(OutputInterface $output): string
{
- if ('Windows' === \PHP_OS_FAMILY) {
+ if ('\\' === \DIRECTORY_SEPARATOR && !$output->isDecorated()) {
return 'Ctrl+Z then Enter';
}
diff --git a/Helper/TableStyle.php b/Helper/TableStyle.php
index 74ac58925..bc941efc2 100644
--- a/Helper/TableStyle.php
+++ b/Helper/TableStyle.php
@@ -78,10 +78,11 @@ public function getPaddingChar(): string
*
*
* ╔═══════════════╤══════════════════════════╤══════════════════╗
- * 1 ISBN 2 Title │ Author ║
- * ╠═══════════════╪══════════════════════════╪══════════════════╣
+ * ║ ISBN │ Title │ Author ║
+ * ╠═══════1═══════╪══════════════════════════╪══════════════════╣
* ║ 99921-58-10-7 │ Divine Comedy │ Dante Alighieri ║
* ║ 9971-5-0210-0 │ A Tale of Two Cities │ Charles Dickens ║
+ * ╟───────2───────┼──────────────────────────┼──────────────────╢
* ║ 960-425-059-0 │ The Lord of the Rings │ J. R. R. Tolkien ║
* ║ 80-902734-1-6 │ And Then There Were None │ Agatha Christie ║
* ╚═══════════════╧══════════════════════════╧══════════════════╝
@@ -102,11 +103,10 @@ public function setHorizontalBorderChars(string $outside, ?string $inside = null
*
*
* ╔═══════════════╤══════════════════════════╤══════════════════╗
- * ║ ISBN │ Title │ Author ║
- * ╠═══════1═══════╪══════════════════════════╪══════════════════╣
+ * 1 ISBN 2 Title │ Author ║
+ * ╠═══════════════╪══════════════════════════╪══════════════════╣
* ║ 99921-58-10-7 │ Divine Comedy │ Dante Alighieri ║
* ║ 9971-5-0210-0 │ A Tale of Two Cities │ Charles Dickens ║
- * ╟───────2───────┼──────────────────────────┼──────────────────╢
* ║ 960-425-059-0 │ The Lord of the Rings │ J. R. R. Tolkien ║
* ║ 80-902734-1-6 │ And Then There Were None │ Agatha Christie ║
* ╚═══════════════╧══════════════════════════╧══════════════════╝
diff --git a/Tests/Command/InvokableCommandTest.php b/Tests/Command/InvokableCommandTest.php
index e9a2961af..e63c588f3 100644
--- a/Tests/Command/InvokableCommandTest.php
+++ b/Tests/Command/InvokableCommandTest.php
@@ -42,8 +42,15 @@ public function testCommandInputArgumentDefinition()
#[Argument] ?string $firstName,
#[Argument] string $lastName = '',
#[Argument(description: 'Short argument description')] string $bio = '',
+ // In this test case, we declare the callback in static context, even when the method is NOT static.
+ // PHP doesn't allow using `$this` here, and the callback is later modified on-the-fly
+ // to be called on the instance instead, and this test case validates if this mechanism works.
+ //
+ // @see \Symfony\Component\Console\Attribute\Argument
#[Argument(suggestedValues: [self::class, 'getSuggestedRoles'])] array $roles = ['ROLE_USER'],
): int {
+ \assert(null !== $this); // so PHP CS Fixer knows this callback is actually coupled with `$this` and `static_lambda` rule shall not be applied
+
return 0;
});
@@ -88,6 +95,8 @@ public function testCommandInputOptionDefinition()
#[Option(suggestedValues: [self::class, 'getSuggestedRoles'])] array $roles = ['ROLE_USER'],
#[Option] string|bool $opt = false,
): int {
+ \assert(null !== $this); // so PHP CS Fixer knows this callback is actually coupled with `$this` and `static_lambda` rule shall not be applied
+
return 0;
});
diff --git a/Tests/DependencyInjection/AddConsoleCommandPassTest.php b/Tests/DependencyInjection/AddConsoleCommandPassTest.php
index 953e5843c..790e31ab0 100644
--- a/Tests/DependencyInjection/AddConsoleCommandPassTest.php
+++ b/Tests/DependencyInjection/AddConsoleCommandPassTest.php
@@ -329,6 +329,25 @@ public function testProcessInvokableCommand()
$this->assertStringContainsString('usage1', $command->getUsages()[0]);
}
+ public function testProcessCommandWithDescriptionWithpercentageSigns()
+ {
+ $container = new ContainerBuilder();
+ $container
+ ->register(
+ 'description_with_percentage_signs_command',
+ DescriptionWithPercentageSignsCommand::class,
+ )
+ ->addTag('console.command')
+ ;
+ $pass = new AddConsoleCommandPass();
+ $pass->process($container);
+
+ $command = $container->get('console.command_loader')->get('description-percentage-signs');
+
+ self::assertTrue($container->has('description_with_percentage_signs_command.command'));
+ self::assertSame('Just testing %percentage-signs%', $command->getDescription());
+ }
+
public function testProcessInvokableSignalableCommand()
{
$container = new ContainerBuilder();
@@ -386,6 +405,14 @@ public function __invoke(): void
}
}
+#[AsCommand(name: 'description-percentage-signs', description: 'Just testing %percentage-signs%')]
+class DescriptionWithPercentageSignsCommand
+{
+ public function __invoke(): void
+ {
+ }
+}
+
#[AsCommand(name: 'invokable-signalable', description: 'Just testing', help: 'The %command.name% help content.')]
class InvokableSignalableCommand implements SignalableCommandInterface
{
diff --git a/Tests/Helper/QuestionHelperTest.php b/Tests/Helper/QuestionHelperTest.php
index 8d492c564..a78305bca 100644
--- a/Tests/Helper/QuestionHelperTest.php
+++ b/Tests/Helper/QuestionHelperTest.php
@@ -415,6 +415,24 @@ public function testAskWithAutocompleteWithMultiByteCharacter($character)
$this->assertSame($character, $dialog->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question));
}
+ public function testAutocompleteWithSpaceAfterPartialMatch()
+ {
+ if (!Terminal::hasSttyAvailable()) {
+ $this->markTestSkipped('`stty` is required to test autocomplete functionality');
+ }
+
+ // a
+ $inputStream = $this->getInputStream("a \t\n");
+
+ $dialog = new QuestionHelper();
+ $dialog->setHelperSet(new HelperSet([new FormatterHelper()]));
+
+ $question = new ChoiceQuestion('Please select a choice', ['a test', 'another choice']);
+ $question->setMaxAttempts(1);
+
+ $this->assertSame('a test', $dialog->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question));
+ }
+
public function testAutocompleteWithTrailingBackslash()
{
if (!Terminal::hasSttyAvailable()) {
@@ -970,7 +988,7 @@ public function testExitCommandOnInputSIGINT(string $mode)
}
$p = new Process(
- ['php', dirname(__DIR__).'/Fixtures/application_test_sigint.php', $mode],
+ ['php', \dirname(__DIR__).'/Fixtures/application_test_sigint.php', $mode],
timeout: 2, // the process will auto shutdown if not killed by SIGINT, to prevent blocking
);
$p->setPty(true);