Skip to content

Escape backslashes so set_key values round-trip - #673

Closed
eeshsaxena wants to merge 2 commits into
theskumar:mainfrom
eeshsaxena:fix-set-key-backslash
Closed

Escape backslashes so set_key values round-trip#673
eeshsaxena wants to merge 2 commits into
theskumar:mainfrom
eeshsaxena:fix-set-key-backslash

Conversation

@eeshsaxena

Copy link
Copy Markdown

Summary

Fixes #661.

set_key (and dotenv set) writes single-quoted values but only escapes single quotes, not backslashes. Because the single-quoted-value parser treats backslash as an escape character, values containing backslashes did not round-trip:

  • an embedded \ collapsed to \ on read;
  • a value ending in a backslash (e.g. a Windows path like C:\) wrote a trailing \', which the parser read as an escaped quote - swallowing the closing quote and corrupting the whole file (subsequent lines became unparseable).

Reproduction (before)

import dotenv
dotenv.set_key(".env", "A", r"a\b")   # embedded double backslash
dotenv.get_key(".env", "A")            # -> "a\b"   (one backslash lost)

dotenv.set_key(".env", "B", "C:\\")    # trailing backslash
dotenv.get_key(".env", "B")            # -> None    (line unparseable)

Fix

  1. In set_key, escape backslashes before single quotes (order matters) so the written value round-trips.
  2. Update the _single_quoted_value tokenizer from (?:\'|[^'])* to (?:\[\s\S]|[^'\])* so every \<char> (including \) is consumed as one escaped unit. Without this, even a correctly-escaped trailing backslash ('C:\') merges its second backslash with the closing quote. The [\s\S] keeps the previous acceptance of newlines after a backslash inside single quotes.

The change is scoped to single-quoted values (the form set_key emits). Values without backslashes are unaffected, so all existing set_key output expectations are unchanged.

Tests

  • test_set_key_backslash_round_trip - round-trips Windows paths, regexes, embedded/trailing backslashes, and backslash-before-quote.
  • Two test_parse_stream cases - an escaped backslash decodes to one backslash, and a trailing escaped backslash does not consume the closing quote.

Full test_parser.py + test_main.py pass (124 passed). (test_cli.py::test_run_with_invalid_cmd fails on main too - a pre-existing Windows-only error-message assertion, unrelated to this change.)

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 "\<char>" as one escaped
unit so an escaped trailing backslash no longer merges with the closing
quote.

Fixes #661
@eeshsaxena eeshsaxena closed this Jul 16, 2026
@eeshsaxena eeshsaxena reopened this Jul 16, 2026
@eeshsaxena

Copy link
Copy Markdown
Author

Hi! Gentle ping on this one; it's been open a couple of weeks and CI is green. Happy to rebase or make any changes if something's needed; whenever you get a chance to take a look, I'd really appreciate it. Thanks!

@theskumar theskumar added the inprogress Being actively reviewed/worked on label Aug 16, 2026
@theskumar

Copy link
Copy Markdown
Owner

Hi @eeshsaxena - thank you for this, and sorry for the slow turnaround. This was really close to what we're landing, and your writeup on the trailing-backslash parser bug was spot on. I'm consolidating the #661 work into #680, which takes the same write + parser approach and also covers double-quoted values, so I'm going to close this in its favor. Genuinely appreciate the detailed PR and the nudges - they helped. Thanks again!

@theskumar theskumar closed this Aug 16, 2026
@theskumar theskumar removed the inprogress Being actively reviewed/worked on label Aug 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

2 participants