Skip to content

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

Open
dchaudhari7177 wants to merge 1 commit into
theskumar:mainfrom
dchaudhari7177:fix/set-key-escape-backslash
Open

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

Conversation

@dchaudhari7177

Copy link
Copy Markdown

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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

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

1 participant