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 @@ -14,6 +14,7 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- `set_key` and `unset_key` no longer leave a `.tmp_*` file behind on Windows when writing a read-only `.env` fails, and the error raised is the one from the failed write rather than from cleaning up the temporary file by [@MohammedAlkindi] in [#686]
- `load_dotenv`, `dotenv_values`, `get_key`, `set_key`, `unset_key` and the CLI `--file` option now expand a leading `~` to the user's home directory by [@veeceey] in [#615]
- `find_dotenv` and the IPython `%dotenv` magic now expand a leading `~` in the file name by [@theskumar] in [#714]
- `set_key` now writes values containing `'` double-quoted, so the resulting line is valid shell and can be `source`d by [@SAY-5] in [#647]

## [1.2.4] - 2026-10-01

Expand Down Expand Up @@ -460,6 +461,7 @@ os.PathLike]` instead of just `os.PathLike` (#347 by [@bbc2]).
[#640]: https://github.com/theskumar/python-dotenv/pull/640
[#615]: https://github.com/theskumar/python-dotenv/pull/615
[#648]: https://github.com/theskumar/python-dotenv/pull/648
[#647]: https://github.com/theskumar/python-dotenv/pull/647
[#663]: https://github.com/theskumar/python-dotenv/pull/663
[#680]: https://github.com/theskumar/python-dotenv/pull/680
[#686]: https://github.com/theskumar/python-dotenv/pull/686
Expand Down Expand Up @@ -512,6 +514,7 @@ os.PathLike]` instead of just `os.PathLike` (#347 by [@bbc2]).
[@mgorny]: https://github.com/mgorny
[@naorlivne]: https://github.com/naorlivne
[@Noethix55555]: https://github.com/Noethix55555
[@SAY-5]: https://github.com/SAY-5
[@qnighy]: https://github.com/qnighy
[@rabinadk1]: https://github.com/rabinadk1
[@randomseed42]: https://github.com/randomseed42
Expand Down
18 changes: 12 additions & 6 deletions src/dotenv/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,12 +251,18 @@ def set_key(
)

if quote:
# The single-quoted-value parser decodes `\\` and `\'`, so both have to
# be escaped here for the value to survive a write/read round-trip.
# Backslashes first, otherwise the backslash added by the quote
# escaping would be escaped in turn.
escaped = value_to_set.replace("\\", "\\\\").replace("'", "\\'")
value_out = f"'{escaped}'"
if "'" in value_to_set:
# A single quote cannot be escaped inside a single-quoted shell
# value, so values containing one are written double-quoted to
# keep the file source-able.
escaped = value_to_set.replace("\\", "\\\\").replace('"', '\\"')
value_out = f'"{escaped}"'
else:
# The single-quoted-value parser decodes `\\`, so backslashes have
# to be escaped here for the value to survive a write/read
# round-trip.
escaped = value_to_set.replace("\\", "\\\\")
value_out = f"'{escaped}'"
else:
value_out = value_to_set
if export:
Expand Down
7 changes: 4 additions & 3 deletions tests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,11 @@ def test_set_key_no_file(tmp_path):
[
("", "a", "", (True, "a", ""), "a=''\n"),
("", "a", "b", (True, "a", "b"), "a='b'\n"),
("", "a", "'b'", (True, "a", "'b'"), "a='\\'b\\''\n"),
("", "a", "'b'", (True, "a", "'b'"), "a=\"'b'\"\n"),
("", "a", '"b"', (True, "a", '"b"'), "a='\"b\"'\n"),
("", "a", "b'c", (True, "a", "b'c"), "a='b\\'c'\n"),
("", "a", "b'c", (True, "a", "b'c"), 'a="b\'c"\n'),
("", "a", 'b"c', (True, "a", 'b"c'), "a='b\"c'\n"),
("", "a", 'I\'m "in"', (True, "a", 'I\'m "in"'), 'a="I\'m \\"in\\""\n'),
("a=b", "a", "c", (True, "a", "c"), "a='c'\n"),
("a=b\n", "a", "c", (True, "a", "c"), "a='c'\n"),
("a=b\n\n", "a", "c", (True, "a", "c"), "a='c'\n\n"),
Expand All @@ -43,7 +44,7 @@ def test_set_key_no_file(tmp_path):
("a=b", "c", "d", (True, "c", "d"), "a=b\nc='d'\n"),
("", "a", "b\\c", (True, "a", "b\\c"), "a='b\\\\c'\n"),
("", "a", "b\\", (True, "a", "b\\"), "a='b\\\\'\n"),
("", "a", "b\\'c", (True, "a", "b\\'c"), "a='b\\\\\\'c'\n"),
("", "a", "b\\'c", (True, "a", "b\\'c"), 'a="b\\\\\'c"\n'),
],
)
def test_set_key(dotenv_path, before, key, value, expected, after):
Expand Down
Loading