diff --git a/CHANGES.rst b/CHANGES.rst index 3b3ba08b2..f36e6a5a8 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Version 8.3.2 Released 2026-04-02 +- Quote the ``/select,`` argument passed to Explorer in ``launch(locate=True)`` + on Windows so that paths containing spaces are handled correctly. + :issue:`2994` - Fix handling of ``flag_value`` when ``is_flag=False`` to allow such options to be used without an explicit value. :issue:`3084` :pr:`3152` - Hide ``Sentinel.UNSET`` values as ``None`` when using ``lookup_default()``. diff --git a/src/click/_termui_impl.py b/src/click/_termui_impl.py index ee8225c4c..a7343347c 100644 --- a/src/click/_termui_impl.py +++ b/src/click/_termui_impl.py @@ -698,8 +698,8 @@ def _unquote_file(url: str) -> str: null.close() elif WIN: if locate: - url = _unquote_file(url) - args = ["explorer", f"/select,{url}"] + url = _unquote_file(url).replace('"', '""') + args = ["explorer", f'/select,"{url}"'] else: args = ["start"] if wait: diff --git a/tests/test_termui.py b/tests/test_termui.py index 8220431bb..e36036725 100644 --- a/tests/test_termui.py +++ b/tests/test_termui.py @@ -710,3 +710,34 @@ def cli(flag): assert result.output == expected_output assert not result.stderr assert result.exit_code == 0 if expected not in (REPEAT, INVALID) else 1 + + +@pytest.mark.skipif(not WIN, reason="Windows-only") +def test_open_url_locate_quotes_path_with_spaces(): + """launch(locate=True) must quote the path so Explorer handles spaces.""" + from unittest.mock import patch + + path = r"C:\path with spaces\file.txt" + + with patch("subprocess.call", return_value=0) as mock_call: + click._termui_impl.open_url(path, locate=True) + + args = mock_call.call_args[0][0] + # The /select, argument must wrap the path in double quotes + assert args[0] == "explorer" + assert args[1] == f'/select,"{path}"' + + +@pytest.mark.skipif(not WIN, reason="Windows-only") +def test_open_url_locate_escapes_quotes_in_path(): + """Embedded double-quotes in path are escaped as \"\".""" + from unittest.mock import patch + + path = 'C:\\path\\with "quotes"\\file.txt' + + with patch("subprocess.call", return_value=0) as mock_call: + click._termui_impl.open_url(path, locate=True) + + args = mock_call.call_args[0][0] + escaped = path.replace('"', '""') + assert args[1] == f'/select,"{escaped}"'