Skip to content

Compare Bash version components numerically in the completion gate - #1983

Open
HyperionBurn wants to merge 1 commit into
fastapi:masterfrom
HyperionBurn:fix-bash-completion-version-check
Open

HyperionBurn wants to merge 1 commit into
fastapi:masterfrom
HyperionBurn:fix-bash-completion-version-check

Conversation

@HyperionBurn

@HyperionBurn HyperionBurn commented Oct 18, 2026 •

Copy link
Copy Markdown

What is broken

BashComplete._check_version() in typer/_completion_classes.py decides whether to tell the user that Bash shell completion is unavailable. Bash is supported from 4.4 onwards, so the gate should report "too old" when the detected version is below 4.4.

The check parses the version and then compares the two components as strings:

major, minor = match.groups()

if major < "4" or major == "4" and minor < "4":

match.groups() returns text, so both comparisons are lexicographic. In Python "10" < "4" is True, because the character 1 sorts before 4. Any Bash whose major or minor component reaches 10 is therefore reported as older than 4.4, and completion is switched off for a Bash that supports it perfectly well.

I found this by reading the completion code; there is no tracking issue for it, and I checked the open and recently closed issue/PR lists for BASH_VERSION / _check_version / bash version gating to make sure it is not already covered.

Reproduction

Script that fakes the detected BASH_VERSION and reports whether typer printed the "not supported" warning:

from unittest import mock
from typer import _click
from typer._completion_classes import BashComplete

def detected_gate(bash_version: str) -> bool:
    output = mock.Mock()
    output.stdout = f"{bash_version}\n".encode()
    with (
        mock.patch("shutil.which", return_value="/usr/bin/bash"),
        mock.patch("subprocess.run", return_value=output),
        mock.patch.object(_click.utils, "echo") as echo,
    ):
        BashComplete._check_version()
        return echo.called

Output on current master:

Bash version        -> typer refuses completion?
----------------------------------------------------
  3.2.57             -> True
  4.3.0              -> True
  4.4.0              -> False
  4.9.0              -> False
  4.10.0             -> True
  5.3.0              -> False
  10.1.0             -> True

Strings compare lexicographically: "10" < "4" is True
So bash 4.10 and bash 10.1 are both called too old, while
bash 4.9 is correctly accepted.

4.9.0 is accepted but 4.10.0 is rejected. The version is going backwards.

Test

tests/test_completion/test_completion_bash_version.py covers the boundary in both directions: versions genuinely below 4.4 still warn, the 4.4/4.5/4.9/5.x cases still pass, and two-digit components are accepted.

Failing before the fix:

$ python -m pytest tests/test_completion/test_completion_bash_version.py -q
________ test_bash_two_digit_version_components_are_supported[4.10.0] _________

bash_version = '4.10.0'

    @pytest.mark.parametrize(
        "bash_version",
        ["4.10.0", "4.11.2", "5.10.0", "10.0.0", "12.3.4"],
    )
    def test_bash_two_digit_version_components_are_supported(bash_version: str):
        """Version components must be compared numerically.
        ...
        """
>       assert _check_version_with(bash_version) is False
E       AssertionError: assert True is False
E        +  where True = _check_version_with('4.10.0')

tests\test_completion\test_completion_bash_version.py:55: AssertionError
...
FAILED tests/test_completion/test_completion_bash_version.py::test_bash_two_digit_version_components_are_supported[4.10.0]
FAILED tests/test_completion/test_completion_bash_version.py::test_bash_two_digit_version_components_are_supported[4.11.2]
FAILED tests/test_completion/test_completion_bash_version.py::test_bash_two_digit_version_components_are_supported[10.0.0]
FAILED tests/test_completion/test_completion_bash_version.py::test_bash_two_digit_version_components_are_supported[12.3.4]
4 failed, 10 passed in 0.12s

(5.10.0 already passed, because only the major == 4 branch does a minor comparison, so only the four cases that reach a two-digit major or a two-digit minor on the 4.x line failed.)

After the fix:

$ python -m pytest tests/test_completion/test_completion_bash_version.py -q
..............                                                           [100%]
14 passed in 0.07s

and the reproduction now reads:

  3.2.57             -> True
  4.3.0              -> True
  4.4.0              -> False
  4.9.0              -> False
  4.10.0             -> False
  5.3.0              -> False
  10.1.0             -> False

The change

 if match is not None:
-    major, minor = match.groups()
+    major, minor = (int(part) for part in match.groups())

-    if major < "4" or major == "4" and minor < "4":
+    if major < 4 or (major == 4 and minor < 4):

I also added the parentheses around major == 4 and minor < 4, which was previously relying on and binding tighter than or. That is not a behaviour change, it just makes the precedence explicit now that the operands are integers.

Full suite

Run locally on Windows with Python 3.14.6:

$ python -m pytest tests -q
1492 passed, 21 skipped, 2 xfailed in 261.02s (0:04:21)

Baseline on unmodified master was 1478 passed, 21 skipped, 2 xfailed; the 14 extra tests are the new ones.

Project CI on this branch is green as well, including the full platform matrix and the pre-commit job (which runs ruff check, ruff format, mypy typer and ty check typer):

check	completed	success
coverage-combine	completed	success
pre-commit	completed	success
test (ubuntu-latest, 3.10, lowest-direct)	completed	success
test (ubuntu-latest, 3.14, highest)	completed	success
test (macos-latest, 3.11, highest)	completed	success
test (macos-latest, 3.12, lowest-direct)	completed	success
test (macos-latest, 3.14, highest)	completed	success
test (windows-latest, 3.13, highest)	completed	success
test (windows-latest, 3.14, highest)	completed	success
test-redistribute	completed	success
docs-all-green	completed	success
changes	completed	success

What I did not verify

Being explicit about the limits of the evidence:

  • No released Bash currently triggers this. The newest Bash is in the 5.x line, so every component is a single digit today and this check happens to give the right answer for all shipped versions. This is a latent correctness bug rather than something a user is hitting right now; it misfires as soon as Bash reaches 4.10 or 10.0. If the team would rather not carry a fix for an unreachable state, I am happy to close it.
  • The test is not end-to-end. It mocks shutil.which and subprocess.run, so it exercises the version gate only. I did not install a real Bash and did not verify that completion output is correct under an actual shell. Running the suite on a Linux/macOS CI runner does not exercise this either, since every job has its own Bash 5.x.
  • mypy typer in my own virtualenv reported 6 errors in vendored click files I did not touch. Those did not reproduce in the project's pre-commit job, which is the authoritative result; I mention it only so the discrepancy is on the record.
  • I did not run the test-cpython-nightly workflow, which this branch does not trigger.

BashComplete._check_version() gates shell completion on Bash being at least
4.4, but it compared the major and minor components as strings. Lexicographic
ordering places "10" before "4", so any Bash whose major or minor component
reaches 10 is reported as older than 4.4 and completion is incorrectly
disabled. Parse the components as integers instead.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants