Skip to content

fix: make values containing backslashes round-trip through set_key - #680

Merged
theskumar merged 1 commit into
theskumar:mainfrom
dchaudhari7177:fix/set-key-escape-backslash
Aug 16, 2026
Merged

fix: make values containing backslashes round-trip through set_key#680
theskumar merged 1 commit into
theskumar:mainfrom
dchaudhari7177:fix/set-key-escape-backslash

Conversation

@dchaudhari7177

Copy link
Copy Markdown
Contributor

Fixes #661.

Problem

set_key writes single-quoted values and escapes single quotes, but not backslashes. The single-quoted-value parser decodes \ and \', so a value containing a backslash does not survive a write/read round-trip:

>>> dotenv.set_key(".env", "PATH", r"C:\Users")   # file gets PATH='C:\Users'
>>> dotenv.get_key(".env", "PATH")
'C:Users'
>>> dotenv.set_key(".env", "RE", r"\d+")
>>> dotenv.get_key(".env", "RE")
'd+'

Windows paths and regular expressions are the common victims.

Fix, part 1 — escape on write

value_to_set.replace("\\", "\\\\").replace("'", "\'"). Backslashes first, so the backslash introduced by the quote escaping is not escaped again.

Fix, part 2 — the write side alone is not enough

With only part 1, a value ending in a backslash is still broken, and breaks its neighbours too:

A='back\'
B='sentinel'

_single_quoted_value was '((?:\'|[^'])*)' — it knows \' is an escape but not \. So in back\' the second backslash pairs with the closing quote as \', the match keeps going, and since [^'] also matches newlines it runs into the next line. The binding then fails _end_of_line, error recovery kicks in, and B is lost as well.

Both quoted-value patterns now say "a backslash escapes the next character":

_single_quoted_value = make_regex(r"'((?:\.|[^'\])*)'", extra_flags=re.DOTALL)
_double_quoted_value = make_regex(r'"((?:\.|[^"\])*)"', extra_flags=re.DOTALL)

re.DOTALL keeps multi-line quoted values working, which the old [^'] allowed.

This is not a behaviour change for anything that parsed correctly before — the captured group is identical for every input the old pattern handled, and decode_escapes is untouched. It only changes inputs that previously failed to parse. Hand-written A='C:\Users\name' still yields C:\Users\name, and A='b\'c' still yields b'c.

Tests

  • test_set_key gains three cases pinning the written form for b\c, b\ and b\'c.
  • New test_set_key_round_trips parametrizes ten values (Windows paths with and without a trailing separator, regexes, quotes, empty) and asserts both that the value comes back intact and that a following key is still readable — that second assertion is what catches the line-swallowing.
  • test_parse_stream gains five cases for escaped backslashes in both quote styles, including the two-binding case.

pytest tests/ passes. The two test_cli.py::test_run_* failures on my machine are pre-existing and Windows-specific (they reproduce on an unmodified checkout).

🤖 Generated with Claude Code

`set_key` writes single-quoted values but only escaped single quotes,
not backslashes. The single-quoted-value parser decodes `\` and `\'`,
so any value with a backslash came back wrong:

    set_key(".env", "P", r"C:\Users")   # writes P='C:\Users'
    get_key(".env", "P")                # -> "C:Users"

Escape backslashes before single quotes when writing.

That alone is not enough for a value that ends in a backslash. The
quoted-value patterns treated `\'` as an escape but not `\`, so in
`P='back\'` the second backslash paired with the closing quote. `[^']`
matches newlines, so the match ran on into the following lines and the
binding failed to parse, taking the next entries with it. Both the
single- and double-quoted patterns now treat a backslash as escaping
whatever follows it.

Fixes theskumar#661

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@theskumar

Copy link
Copy Markdown
Owner

Hey @dchaudhari7177 - merging this now, thank you! This was the most complete take on #661 by a good margin: escaping on write plus fixing the parser so a trailing backslash can't swallow the next line, and covering both single- and double-quoted values. The test that writes a sentinel key right after the value was a nice touch for catching the desync. Really appreciate the clear writeup too - thanks for the fix!

@theskumar
theskumar merged commit f7b18d9 into theskumar:main Aug 16, 2026
9 checks passed
theskumar added a commit that referenced this pull request Aug 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

inprogress Being actively reviewed/worked on

Projects

None yet

Development

Successfully merging this pull request may close these issues.

set_key corrupts values containing backslashes (Windows paths, regexes) on round-trip

2 participants