From 60e6f7334144e36ada12473ed6ccfcb4ed035f04 Mon Sep 17 00:00:00 2001 From: behlole Date: Thu, 14 May 2026 20:53:38 +0500 Subject: [PATCH] Write bash completion output as raw bytes On Windows, sys.stdout is opened in text mode, which translates \n to \r\n. Typer's bash completion writes option names separated by \n; after text-mode translation those separators become \r\n, but bash on MSYS2 / Git Bash splits only on \n. The stray \r ends up attached to the next option name and shows up as ^M between completions. Encode the completion output to UTF-8 bytes before handing it to click.echo. click.echo writes bytes through the raw buffer, bypassing text-mode translation. On POSIX the output is unchanged. Closes #202 --- .../test_completion_complete.py | 26 +++++++++++++++++++ typer/completion.py | 7 ++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/tests/test_completion/test_completion_complete.py b/tests/test_completion/test_completion_complete.py index de353f8a0b..29ae1873d4 100644 --- a/tests/test_completion/test_completion_complete.py +++ b/tests/test_completion/test_completion_complete.py @@ -185,3 +185,29 @@ def test_completion_complete_subcommand_noshell(mod: ModuleType): }, ) assert ("") in result.stdout + + +def test_completion_complete_output_has_no_carriage_returns(mod: ModuleType): + """Bash on MSYS2 / Git Bash splits completion output on ``\\n`` only, so + any stray ``\\r`` (introduced by Windows text-mode I/O translating + in-string ``\\n`` to ``\\r\\n``) leaks into the option names and shows + up as ``^M`` between completions. + + Regression test for https://github.com/fastapi/typer/issues/202. + """ + file_name = Path(mod.__file__).name + result = subprocess.run( + [sys.executable, "-m", "coverage", "run", mod.__file__, " "], + capture_output=True, + env={ + **os.environ, + f"_{file_name.upper()}_COMPLETE": "complete_bash", + "COMP_WORDS": f"{file_name} del", + "COMP_CWORD": "1", + }, + ) + # Read raw bytes — encoding="utf-8" on subprocess.run would hide a + # newline-translation regression by silently decoding it away. The + # in-string separator must remain a bare ``\\n``. + assert b"delete\ndelete-all" in result.stdout + assert b"delete\r\ndelete-all" not in result.stdout diff --git a/typer/completion.py b/typer/completion.py index 0d621e411d..1742995bf6 100644 --- a/typer/completion.py +++ b/typer/completion.py @@ -138,7 +138,12 @@ def shell_complete( # Typer override to print the completion help msg with Rich if instruction == "complete": - click.echo(comp.complete()) + # Write the completion output as raw bytes so Python's text-mode I/O + # on Windows doesn't translate the in-string ``\n`` separators into + # ``\r\n``. Bash splits on ``\n`` only, and any stray ``\r`` leaks + # into the option names (showing up as ``^M`` between completions in + # MSYS2 / Git Bash). See https://github.com/fastapi/typer/issues/202. + click.echo(comp.complete().encode("utf-8")) return 0 # Typer override end