From 4a2fb3b7106a472919cf5b6bc72a5719a658a656 Mon Sep 17 00:00:00 2001 From: Sanjays2402 <51058514+Sanjays2402@users.noreply.github.com> Date: Sun, 28 Jun 2026 02:27:13 -0700 Subject: [PATCH] fix: strip ANSI from confirm and prompt when output is not a tty The readline change in 8.4.0 (PR #2969) passes the full prompt text directly to the prompt function instead of writing it through echo. On non-Windows that bypassed echo's ANSI stripping, so styled prompts kept their color codes even when the output stream was not a terminal (for example under CliRunner with color=False). Strip ANSI in _readline_prompt using the same should_strip_ansi check echo uses, so styled prompts match echo again while real terminals keep their colors. fixes #3572 --- CHANGES.md | 4 ++++ src/click/termui.py | 18 ++++++++++++++++++ tests/test_termui.py | 27 +++++++++++++++++++++++++++ 3 files changed, 49 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 7b91bee1e..2d965af27 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -27,6 +27,10 @@ Unreleased stream when no external pager runs, completing the partial `I/O operation on closed file` fix from {pr}`3482`. {issue}`3449` {pr}`3533` +- `confirm` and `prompt` strip ANSI style codes from the prompt text when + the output stream is not a terminal, restoring the pre-`8.4.0` behavior + so styled prompts match `echo` under `CliRunner` and redirected output. + {issue}`3572` {pr}`3646` ## Version 8.4.1 diff --git a/src/click/termui.py b/src/click/termui.py index e5e9678c3..74648fa09 100644 --- a/src/click/termui.py +++ b/src/click/termui.py @@ -11,7 +11,10 @@ from contextlib import redirect_stdout from gettext import gettext as _ +from ._compat import _default_text_stderr +from ._compat import _default_text_stdout from ._compat import isatty +from ._compat import should_strip_ansi from ._compat import strip_ansi from .exceptions import Abort from .exceptions import UsageError @@ -83,7 +86,14 @@ def hidden_prompt_func(prompt: str) -> str: def _readline_prompt(func: t.Callable[[str], str], text: str, err: bool) -> str: """Call a prompt function, passing the full prompt on non-Windows so readline can handle line editing and cursor positioning correctly. + + The prompt is written by the prompt function rather than :func:`echo`, + so strip ANSI style codes here when the target stream is not a + terminal, matching :func:`echo`'s behavior. """ + stream = _default_text_stderr() if err else _default_text_stdout() + if should_strip_ansi(stream, resolve_color_default()): + text = strip_ansi(text) if err: with redirect_stdout(sys.stderr): return func(text) @@ -156,6 +166,10 @@ def prompt( show_choices is true and text is "Group by" then the prompt will be "Group by (day, week): ". + .. versionchanged:: 8.4.2 + ANSI style codes are stripped from the prompt when the output + stream is not a terminal, matching ``echo``. + .. versionchanged:: 8.3.3 ``show_default`` can be a string to show a custom value instead of the actual default, matching the help text behavior. @@ -251,6 +265,10 @@ def confirm( :param err: if set to true the file defaults to ``stderr`` instead of ``stdout``, the same as with echo. + .. versionchanged:: 8.4.2 + ANSI style codes are stripped from the prompt when the output + stream is not a terminal, matching ``echo``. + .. versionchanged:: 8.3.1 A space is no longer appended to the prompt. diff --git a/tests/test_termui.py b/tests/test_termui.py index ce974f0cb..62512e525 100644 --- a/tests/test_termui.py +++ b/tests/test_termui.py @@ -1560,3 +1560,30 @@ def cli(): result = runner.invoke(cli, input="leaky\n", mix_stderr=False) assert "leaky" not in result.stdout assert "leaky" not in result.stderr + + +def test_confirm_strips_ansi_when_color_disabled(runner): + """``click.confirm`` writes its prompt through a non-tty stream under + ``CliRunner``. ANSI style codes must be stripped from that prompt, the + same way :func:`click.echo` strips them, so output matches what users + saw before the readline change in 8.4.0 (see issue #3572). + """ + + @click.command() + def cli(): + click.confirm(click.style("Continue", fg="green"), default=True) + + result = runner.invoke(cli, input="y\n", color=False) + assert result.output == "Continue [Y/n]: y\n" + + +def test_prompt_strips_ansi_when_color_disabled(runner): + """``click.prompt`` must also strip ANSI codes from its prompt text + when the output stream is not a terminal (issue #3572).""" + + @click.command() + def cli(): + click.prompt(click.style("Name", fg="green")) + + result = runner.invoke(cli, input="click\n", color=False) + assert result.output == "Name: click\n"