diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6a0dee2d..8fefd986 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,5 @@ default_language_version: - python: python3.15 + python: python3 repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v6.0.0 diff --git a/docs/changelog/+quoted-user-dir-backslashes.bugfix.rst b/docs/changelog/+quoted-user-dir-backslashes.bugfix.rst new file mode 100644 index 00000000..c9613f5c --- /dev/null +++ b/docs/changelog/+quoted-user-dir-backslashes.bugfix.rst @@ -0,0 +1,2 @@ +Preserve literal backslashes before ordinary characters in double-quoted ``user-dirs.dirs`` paths instead of removing +them. diff --git a/docs/platforms.rst b/docs/platforms.rst index f09e831a..eeccfb61 100644 --- a/docs/platforms.rst +++ b/docs/platforms.rst @@ -285,11 +285,12 @@ See also: :ref:`api:User binary directory` See also: :ref:`api:User documents directory` -On Unix, use percent signs as literal characters in ``user-dirs.dirs`` paths, for example ``$HOME/100% complete``. Lines -follow ``xdg-user-dir`` rules: the last valid line for a directory wins, ``\"`` and other shell escapes inside the -quotes resolve to the escaped character, and platformdirs skips values that are neither ``$HOME``-relative nor absolute. -``XDG_DOCUMENTS_DIR`` in the environment takes precedence over the file and is skipped in the same way unless it is an -absolute path. +On Unix, use percent signs as literal characters in ``user-dirs.dirs`` paths, for example ``$HOME/100% complete``. The +last valid line for a directory wins. Inside double quotes, a backslash is removed only before ``$``, a backtick, ``"``, +or another backslash. Other backslashes, such as ``\q`` in a folder name, remain literal. The ``xdg-user-dir`` utility +also removes a backslash before an ordinary character in a hand-written entry; use ``\\`` for the same result in both. +Platformdirs skips values that are neither ``$HOME``-relative nor absolute. ``XDG_DOCUMENTS_DIR`` in the environment +takes precedence over the file and is skipped in the same way unless it is an absolute path. .. tab-set:: diff --git a/src/platformdirs/unix.py b/src/platformdirs/unix.py index 7130527f..fe343768 100644 --- a/src/platformdirs/unix.py +++ b/src/platformdirs/unix.py @@ -354,7 +354,7 @@ def _resolve_user_dirs_value(entry: re.Match[str]) -> str | None: return None if entry["quoted"] is not None: # xdg-user-dirs-update backslash-escapes $, `, " and \ inside the quotes. - value = re.sub(r"\\(.)", r"\1", value) + value = re.sub(r'\\([$`"\\])', r"\1", value) return prefix + value diff --git a/tests/test_unix.py b/tests/test_unix.py index 9c2d014d..a314759a 100644 --- a/tests/test_unix.py +++ b/tests/test_unix.py @@ -1282,6 +1282,29 @@ def test_user_dirs_preserves_percent_signs(folder: str, base: str, tmp_path: Pat assert Unix().user_documents_path == Path(tmp_path if base == "$HOME" else base) / folder +@pytest.mark.parametrize( + ("folder", "expected"), + [ + pytest.param(r"a\q", r"a\q", id="ordinary-letter"), + pytest.param(r"a\ b", r"a\ b", id="space"), + pytest.param(r"a\-b", r"a\-b", id="hyphen"), + pytest.param(r"a\nb", r"a\nb", id="literal-backslash-n"), + pytest.param(r"a\'b", r"a\'b", id="single-quote"), + pytest.param(r"a\\b", r"a\b", id="escaped-backslash"), + pytest.param(r"a\$b", "a$b", id="escaped-dollar"), + pytest.param(r"a\`b", "a`b", id="escaped-backtick"), + pytest.param(r"a\"b", 'a"b', id="escaped-double-quote"), + pytest.param(r"a\q\\b\$c", r"a\q\b$c", id="mixed-escapes"), + ], +) +@pytest.mark.parametrize("base", ["$HOME", "/absolute"]) +def test_user_dirs_double_quoted_backslashes( + folder: str, expected: str, base: str, tmp_path: Path, user_dirs_file: Path +) -> None: + user_dirs_file.write_text(f'XDG_DOCUMENTS_DIR="{base}/{folder}"\n', encoding="utf-8") + assert Unix().user_documents_path == Path(tmp_path if base == "$HOME" else base) / expected + + @pytest.mark.parametrize( ("content", "expected"), [