Skip to content

Commit 94036f1

Browse files
authored
fix: write the prompt to STDOUT before readline() on Windows (#10534)
1 parent 501706c commit 94036f1

3 files changed

Lines changed: 71 additions & 4 deletions

File tree

‎system/CLI/InputOutput.php‎

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,16 @@ public function input(?string $prefix = null): string
4444
// readline() can't be tested.
4545
if ($this->readlineSupport && ENVIRONMENT !== 'testing') {
4646
// @codeCoverageIgnoreStart
47-
// Libedit reports "EditLine wrapper" and mangles the markers, so only GNU readline gets them.
48-
if ($prefix !== null && ! str_contains(readline_info('library_version'), 'EditLine')) {
49-
$prefix = $this->markAnsiNonPrinting($prefix);
47+
$prompt = $this->readlinePrompt($prefix, readline_info('library_version'));
48+
49+
if ($prompt !== null) {
50+
return readline($prompt);
5051
}
5152

52-
return readline($prefix);
53+
// The library cannot render the prompt, so write it ourselves and let readline() only read the line.
54+
self::fwrite(STDOUT, $prefix ?? '');
55+
56+
return readline();
5357
// @codeCoverageIgnoreEnd
5458
}
5559

@@ -86,6 +90,31 @@ public function fwrite($handle, string $string): void
8690
fwrite($handle, $string);
8791
}
8892

93+
/**
94+
* Builds the prompt handed to readline(), or returns null when the caller has to write
95+
* the prompt to STDOUT itself because the line-editing library cannot render it.
96+
*
97+
* GNU readline gets ANSI sequences wrapped in its non-printing markers so line-redraw
98+
* column accounting skips them. Libedit reports "EditLine wrapper" and mangles the
99+
* markers, so it gets the raw prompt. Windows builds use WinEditLine, which prints ANSI
100+
* sequences literally, and readline_info() omits the version there (php-src guards it
101+
* with #ifndef PHP_WIN32), so they get no prompt.
102+
*
103+
* @param mixed $libraryVersion The value of readline_info('library_version')
104+
*/
105+
private function readlinePrompt(?string $prefix, mixed $libraryVersion): ?string
106+
{
107+
if ($prefix === null || ! is_string($libraryVersion)) {
108+
return null;
109+
}
110+
111+
if (str_contains($libraryVersion, 'EditLine')) {
112+
return $prefix;
113+
}
114+
115+
return $this->markAnsiNonPrinting($prefix);
116+
}
117+
89118
/**
90119
* Wraps ANSI escape sequences in readline's non-printing markers so line-redraw column accounting skips them.
91120
*/

‎tests/system/CLI/CLITest.php‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,43 @@ public function testMarkAnsiNonPrintingWrapsEscapeSequences(): void
267267
$this->assertSame('Name: ', $wrap('Name: '));
268268
}
269269

270+
#[DataProvider('provideReadlinePromptDependsOnLibrary')]
271+
public function testReadlinePromptDependsOnLibrary(mixed $libraryVersion, ?string $expected): void
272+
{
273+
$build = $this->getPrivateMethodInvoker(new InputOutput(), 'readlinePrompt');
274+
275+
$this->assertSame(
276+
$expected,
277+
$build(sprintf('What is your favorite color? [%s]: ', CLI::color('red', 'green')), $libraryVersion),
278+
);
279+
}
280+
281+
/**
282+
* @return iterable<string, array{0: mixed, 1: string|null}>
283+
*/
284+
public static function provideReadlinePromptDependsOnLibrary(): iterable
285+
{
286+
yield 'GNU readline gets the prompt with non-printing markers' => [
287+
'8.2',
288+
"What is your favorite color? [\x01\e[0;32m\x02red\x01\e[0m\x02]: ",
289+
];
290+
291+
yield 'libedit gets the raw prompt' => [
292+
'EditLine wrapper',
293+
"What is your favorite color? [\e[0;32mred\e[0m]: ",
294+
];
295+
296+
// Official Windows builds use WinEditLine, which exposes no library version.
297+
yield 'WinEditLine gets no prompt so the caller writes it' => [null, null];
298+
}
299+
300+
public function testReadlinePromptWithoutPrefixReturnsNull(): void
301+
{
302+
$build = $this->getPrivateMethodInvoker(new InputOutput(), 'readlinePrompt');
303+
304+
$this->assertNull($build(null, '8.2'));
305+
}
306+
270307
public function testPromptByKey(): void
271308
{
272309
PhpStreamWrapper::register();

‎user_guide_src/source/changelogs/v4.7.5.rst‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ Bugs Fixed
3838

3939
- **CLI:** Fixed a bug where pressing backspace in a ``CLI::prompt()`` erased the prompt text when the ``readline`` extension is enabled. The prompt is now passed to ``readline()`` so line redraws repaint it.
4040
ANSI color codes in the prompt (e.g., option defaults) are wrapped in readline's non-printing markers under GNU readline so cursor positioning stays accurate.
41+
On Windows, where the ``readline`` extension is built on WinEditLine, the prompt is written to STDOUT first because WinEditLine reports no library version and prints ANSI sequences literally.
4142
- **CLIRequest:** Fixed a bug where ``parseCommand()`` could throw a TypeError when ``argv`` is missing.
4243
- **CodeIgniter:** Fixed a bug where ``gatherOutput()`` could be called twice when ``startController()`` returned a ``ResponseInterface`` (e.g., from filter attributes or closure routes).
4344
- **Content Security Policy:** Fixed a bug where empty ``Content-Security-Policy``, ``Content-Security-Policy-Report-Only``, and ``Reporting-Endpoints`` response headers were generated when no corresponding values existed.

0 commit comments

Comments
 (0)