Preserve literal backslash before variable - #550
Open
MsfPablo wants to merge 1 commit into
Open
Conversation
unescape_characters now leaves an unescaped double backslash (backslash not followed by $ or another backslash) intact so that Variable substitution can detect the literal-backslash + variable pattern introduced by the parser. Variable substitution recognizes this pattern and emits a literal backslash followed by the substituted value. AI-assisted: This patch was prepared with assistance from Claude (Anthropic) and reviewed by the human author before submission.
There was a problem hiding this comment.
Pull request overview
This PR adjusts Dotenv parsing/substitution to preserve a literal backslash immediately before variable references (e.g. RESULT="a\\$FOO" with FOO=bar should become RESULT="a\bar"), addressing Issue #549.
Changes:
- Tweaks
Parser#unescape_charactersso\\is not collapsed before substitution. - Extends variable substitution matching to treat a doubled backslash before
$VARas “emit one backslash + expanded value”. - Adds an RSpec regression test covering the reported scenario.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
lib/dotenv/parser.rb |
Changes unescape behavior for backslashes prior to running substitutions. |
lib/dotenv/substitutions/variable.rb |
Adds special handling for a doubled backslash immediately before a variable reference. |
spec/dotenv/parser_spec.rb |
Adds a regression test for preserving a literal backslash before $VAR. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+26
to
+30
| if match[1] == "\\\\" | ||
| "\\" + (env[match[4]] || ENV[match[4]] || "") | ||
| elsif match[2] == "\\" | ||
| variable[1..] | ||
| elsif match[3] | ||
| env[match[3]] || ENV[match[3]] || "" | ||
| elsif match[4] |
|
|
||
| def unescape_characters(value) | ||
| value.gsub(/\\([^$])/, '\1') | ||
| value.gsub(/\\([^$\\])/, '\1') |
Comment on lines
+20
to
+22
| it "preserves a literal backslash before a variable reference (#549)" do | ||
| expect(env("FOO=bar\nRESULT=\"a\\\\$FOO\"")).to eql("FOO" => "bar", "RESULT" => "a\\bar") | ||
| end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Preserves a literal
\before$VARreferences soRESULT="a\\$FOO"withFOO=baryieldsRESULT="a\bar"(one backslash + the substituted value) instead of either dropping the backslash silently or substituting nothing.Why
Issue #549 reported that
\$VARwas treated as escape + nothing, but\\$VARwas being collapsed by the parser's existingunescape_charactersstep (regex/\\([^$])/) before variable substitution ever ran, so substitution got$VARand the\was already gone. The reporter's expectation: a doubled\\is a literal backslash, which the substitution layer should then preserve when expanding$FOO.Changes
lib/dotenv/parser.rb(1 line) —unescape_charactersregex tightened from/\\([^$])/to/\\([^$\\])/. A\followed by another\is no longer collapsed here, so the double-backslash reaches variable substitution intact.lib/dotenv/substitutions/variable.rb(~6 lines) — VARIABLE regex gains a leading(\\\\)?group (matched against a double backslash). New branch: when match[1] is the double backslash, emit"\\" + value— one literal backslash, then the substituted value. Existing single-backslash escape and bare-variable branches behave the same as before; their match indices shifted by one ($2,$3,$4instead of$1,$2,$3).spec/dotenv/parser_spec.rb(4 lines) — new testpreserves a literal backslash before a variable reference (#549). Asserts the input above yieldsRESULT=a\bar.Test plan
rspec spec/dotenv/parser_spec.rb— 59 examples, 0 failures (includes the new test)rspec --exclude-pattern "spec/dotenv/log_subscriber_spec.rb,spec/dotenv/rails_spec.rb"— 165 examples, 0 failures (the two excluded specs require unavailableactive_support/railsgems and are unrelated to this change)bundle exec rspecblocked by a sandbox-tooling parsing issue on this machine; rawrspecfrom the user gem dir reproduces the same 165/0 resultNotes
\\$FOOmeans "literal backslash, then expand $FOO";\$FOOstill means "escape, leave$FOOliteral".$VARor\$VARpaths — only\\$VARchanges.Disclosed: this PR was prepared with LLM assistance under the MsfPablo persona and reviewed by the human author before submission.