From aac14ff87d467722c8adc8fe9748e27b958eff85 Mon Sep 17 00:00:00 2001 From: Simone Gigante <92672883+simoncraf@users.noreply.github.com> Date: Mon, 16 Mar 2026 18:12:38 +0100 Subject: [PATCH] fix(parser): support bash-style single-quote splices in values --- src/dotenv/parser.py | 17 +++++++++++++++-- tests/test_main.py | 1 + tests/test_parser.py | 11 +++++++++++ 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/dotenv/parser.py b/src/dotenv/parser.py index eb100b47..a2283dd6 100644 --- a/src/dotenv/parser.py +++ b/src/dotenv/parser.py @@ -23,6 +23,7 @@ def make_regex(string: str, extra_flags: int = 0) -> Pattern[str]: _unquoted_key = make_regex(r"([^=\#\s]+)") _equal_sign = make_regex(r"(=[^\S\r\n]*)") _single_quoted_value = make_regex(r"'((?:\\'|[^'])*)'") +_single_quote_splice = make_regex('"\'"') _double_quoted_value = make_regex(r'"((?:\\"|[^"])*)"') _unquoted_value = make_regex(r"([^\r\n]*)") _comment = make_regex(r"(?:[^\S\r\n]*#[^\r\n]*)?") @@ -125,11 +126,23 @@ def parse_unquoted_value(reader: Reader) -> str: return re.sub(r"\s+#.*", "", part).rstrip() +def parse_single_quoted_value(reader: Reader) -> str: + splice_value = '"\'"' + value = "" + while True: + (part,) = reader.read_regex(_single_quoted_value) + value += decode_escapes(_single_quote_escapes, part) + if reader.peek(len(splice_value)) != splice_value: + break + reader.read_regex(_single_quote_splice) + value += "'" + return value + + def parse_value(reader: Reader) -> str: char = reader.peek(1) if char == "'": - (value,) = reader.read_regex(_single_quoted_value) - return decode_escapes(_single_quote_escapes, value) + return parse_single_quoted_value(reader) elif char == '"': (value,) = reader.read_regex(_double_quoted_value) return decode_escapes(_double_quote_escapes, value) diff --git a/tests/test_main.py b/tests/test_main.py index 50703af0..f55ec711 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -663,6 +663,7 @@ def test_dotenv_values_file(dotenv_path): # With quotes ({"b": "c"}, 'a="${b}"', True, {"a": "c"}), ({"b": "c"}, "a='${b}'", True, {"a": "c"}), + ({}, "a='b'\"'\"'c'", True, {"a": "b'c"}), # With surrounding text ({"b": "c"}, "a=x${b}y", True, {"a": "xcy"}), # Self-referential diff --git a/tests/test_parser.py b/tests/test_parser.py index 43386e5a..c224170b 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -295,6 +295,17 @@ ) ], ), + ( + "a='b'\"'\"'c'", + [ + Binding( + key="a", + value="b'c", + original=Original(string="a='b'\"'\"'c'", line=1), + error=False, + ) + ], + ), ( "a=à", [