Skip to content

8.4.x: prompt/confirm(err=True) leaks prompt tail and CliRunner reply echo to stdout on Windows #3662

Description

@memtomem

On the stable 8.4.x line, prompt() / confirm() with err=True still write part of the prompt interaction to stdout on Windows, because _readline_prompt's WIN branch bypasses the stderr redirect that the POSIX branch applies:

# src/click/termui.py (8.4.2)
def _readline_prompt(func, text, err):
    if WIN:
        echo(text[:-1], nl=False, err=err)   # prompt body honors err ...
        return func(text[-1:])               # ... but this call is NOT redirected
    if err:
        with redirect_stdout(sys.stderr):    # POSIX: fully redirected
            return func(text)
    return func(text)

Consequences on Windows only:

  1. Real console: input(text[-1:]) writes the prompt's last character (the trailing space of "... [y/N]: ") to stdout. For CLIs that promise "stdout is a single machine-readable document" under a --json flag (prompt chrome on stderr via err=True), that's a stray byte in the payload stream — benign for JSON parsers since it's whitespace, but the contract is broken.
  2. CliRunner (the bigger practical hit): test isolation replaces termui.visible_prompt_func with a shim that echoes the prompt tail and the typed reply to sys.stdout. Because the WIN branch doesn't wrap the call in redirect_stdout, stdout becomes " n\n" + <payload>. Any test doing json.loads(result.stdout) on a prompt-then-JSON flow passes on POSIX and fails on Windows with Expecting value: line 1 column 2 (char 1).

Reproduction

Deterministic on a Windows runner; on POSIX you can force the branch to see the exact same behavior:

import json
import click, click.termui
from click.testing import CliRunner

@click.command()
def cmd():
    if not click.confirm("Continue?", default=False, err=True):
        click.echo(json.dumps({"ok": False}))

# click.termui.WIN = True   # <- uncomment on POSIX to simulate
result = CliRunner().invoke(cmd, [], input="n\n")
print(repr(result.stdout))
# POSIX / main:  '{"ok": false}\n'
# Windows 8.4.2: ' n\n{"ok": false}\n'

Environment: click 8.4.2, Python 3.12, observed on GitHub Actions windows-latest (and reproduced via the WIN = True simulation above on macOS/Linux).

Note on main

This is already fixed on main as a side effect of the colorama removal (74546476376b39d05186ec1085fd2faf9d86fa20) — _readline_prompt no longer has a WIN branch, so the err=True redirect applies on all platforms. So this report is effectively a request to consider backporting the redirect part to the 8.4.x stable line (the if WIN: branch could keep the separate echo for coloring but wrap the func call in redirect_stdout(sys.stderr) when err=True).

Downstream context, if useful: this surfaced as two Windows-only CI failures in memtomem (bisect + analysis in memtomem/memtomem#1640); we've since worked around it by bypassing the prompt machinery for our --json paths.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions