Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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

<!-- contributors -->
Expand All @@ -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
Expand Down
6 changes: 5 additions & 1 deletion src/dotenv/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
4 changes: 3 additions & 1 deletion src/dotenv/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 ``\<char>`` 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]*)?")
Expand Down
16 changes: 16 additions & 0 deletions tests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
22 changes: 22 additions & 0 deletions tests/test_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"',
[
Expand Down