From 4d3f5659be7a8382c710c0029def10493405293e Mon Sep 17 00:00:00 2001 From: eeshsaxena Date: Sat, 4 Jul 2026 21:34:36 +0530 Subject: [PATCH 1/2] Escape backslashes so set_key values round-trip set_key writes single-quoted values but only escaped single quotes, not backslashes. Since the single-quoted-value parser treats backslash as an escape character, values containing backslashes did not round-trip: an embedded "\\" collapsed to "\", and a value ending in a backslash (e.g. a Windows path like "C:\") wrote a trailing "\'" that swallowed the closing quote and corrupted the whole file. Escape backslashes before single quotes in set_key, and update the single-quoted-value tokenizer to treat every "\" as one escaped unit so an escaped trailing backslash no longer merges with the closing quote. Fixes #661 --- src/dotenv/main.py | 6 +++++- src/dotenv/parser.py | 4 +++- tests/test_main.py | 16 ++++++++++++++++ tests/test_parser.py | 22 ++++++++++++++++++++++ 4 files changed, 46 insertions(+), 2 deletions(-) diff --git a/src/dotenv/main.py b/src/dotenv/main.py index 3c4608d5..9c205f5a 100644 --- a/src/dotenv/main.py +++ b/src/dotenv/main.py @@ -216,7 +216,11 @@ def set_key( ) if quote: - value_out = "'{}'".format(value_to_set.replace("'", "\\'")) + # The single-quoted-value parser treats backslash as an escape + # character, so backslashes must be escaped before single quotes + # (order matters) for the written value to round-trip unchanged. + escaped = value_to_set.replace("\\", "\\\\").replace("'", "\\'") + value_out = "'{}'".format(escaped) else: value_out = value_to_set if export: diff --git a/src/dotenv/parser.py b/src/dotenv/parser.py index 66773604..3e781cb5 100644 --- a/src/dotenv/parser.py +++ b/src/dotenv/parser.py @@ -22,7 +22,9 @@ def make_regex(string: str, extra_flags: int = 0) -> Pattern[str]: _single_quoted_key = make_regex(r"'([^']+)'") _unquoted_key = make_regex(r"([^=\#\s]+)") _equal_sign = make_regex(r"(=[^\S\r\n]*)") -_single_quoted_value = make_regex(r"'((?:\\'|[^'])*)'") +# Treat every ``\`` as a single escaped unit (including ``\\``), so a +# value ending in an escaped backslash does not merge with the closing quote. +_single_quoted_value = make_regex(r"'((?:\\[\s\S]|[^'\\])*)'") _double_quoted_value = make_regex(r'"((?:\\"|[^"])*)"') _unquoted_value = make_regex(r"([^\r\n]*)") _comment = make_regex(r"(?:[^\S\r\n]*#[^\r\n]*)?") diff --git a/tests/test_main.py b/tests/test_main.py index 1c33c808..7e13fe1b 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -54,6 +54,22 @@ def test_set_key(dotenv_path, before, key, value, expected, after): mock_warning.assert_not_called() +@pytest.mark.parametrize( + "value", + [ + r"C:\Users", # Windows path + r"\d+", # regex + "a\\\\b", # embedded double backslash + "C:\\", # trailing backslash (previously corrupted the file) + "x\\'y", # backslash before a quote + ], +) +def test_set_key_backslash_round_trip(dotenv_path, value): + dotenv.set_key(dotenv_path, "a", value) + + assert dotenv.get_key(dotenv_path, "a") == value + + def test_set_key_encoding(dotenv_path): encoding = "latin-1" diff --git a/tests/test_parser.py b/tests/test_parser.py index 4ec5a5af..bade44be 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -273,6 +273,28 @@ ) ], ), + ( + "a='b\\\\c'", # escaped backslash decodes to a single backslash + [ + Binding( + key="a", + value="b\\c", + original=Original(string="a='b\\\\c'", line=1), + error=False, + ) + ], + ), + ( + "a='b\\\\'", # trailing escaped backslash must not consume the quote + [ + Binding( + key="a", + value="b\\", + original=Original(string="a='b\\\\'", line=1), + error=False, + ) + ], + ), ( 'a="b\\"c"', [ From 3263334852e023c4e08d7cdf3b6fed04930801f8 Mon Sep 17 00:00:00 2001 From: eeshsaxena Date: Sat, 4 Jul 2026 21:36:00 +0530 Subject: [PATCH 2/2] Add changelog entry for backslash round-trip fix --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fa81cc6e..3220fe92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Fixed - Strip a leading UTF-8 BOM from `.env` file contents so the first variable is no longer silently lost when the file is saved with BOM (e.g. by some JetBrains IDEs on Windows) by [@h1whelan] in [#640] +- Escape backslashes in `set_key` so values containing backslashes (e.g. Windows paths, regexes) round-trip instead of being corrupted, and fix the single-quoted-value parser so a trailing backslash no longer breaks the file by [@eeshsaxena] in [#673] ## [1.2.2] - 2026-03-01 @@ -435,6 +436,7 @@ os.PathLike]` instead of just `os.PathLike` (#347 by [@bbc2]). [#497]: https://github.com/theskumar/python-dotenv/pull/497 [#161]: https://github.com/theskumar/python-dotenv/issues/161 [#640]: https://github.com/theskumar/python-dotenv/pull/640 +[#673]: https://github.com/theskumar/python-dotenv/pull/673 [790c5c0]: https://github.com/theskumar/python-dotenv/commit/790c5c02991100aa1bf41ee5330aca75edc51311 @@ -455,6 +457,7 @@ os.PathLike]` instead of just `os.PathLike` (#347 by [@bbc2]). [@eaf]: https://github.com/eaf [@earlbread]: https://github.com/earlbread [@eekstunt]: https://github.com/eekstunt +[@eeshsaxena]: https://github.com/eeshsaxena [@eggplants]: https://github.com/eggplants [@ekohl]: https://github.com/ekohl [@elbehery95]: https://github.com/elbehery95