diff --git a/CHANGELOG.md b/CHANGELOG.md index fa81cc6e..3660eaec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Fixed - Strip a leading UTF-8 BOM from `.env` file contents so the first variable is no longer silently lost when the file is saved with BOM (e.g. by some JetBrains IDEs on Windows) by [@h1whelan] in [#640] +- Treat an inline comment that follows an empty unquoted value as a comment rather than the value, so `KEY= # comment` parses to `""` instead of `"# comment"` by [@dchaudhari7177] in [#600] ## [1.2.2] - 2026-03-01 @@ -435,6 +436,7 @@ os.PathLike]` instead of just `os.PathLike` (#347 by [@bbc2]). [#497]: https://github.com/theskumar/python-dotenv/pull/497 [#161]: https://github.com/theskumar/python-dotenv/issues/161 [#640]: https://github.com/theskumar/python-dotenv/pull/640 +[#600]: https://github.com/theskumar/python-dotenv/issues/600 [790c5c0]: https://github.com/theskumar/python-dotenv/commit/790c5c02991100aa1bf41ee5330aca75edc51311 @@ -452,6 +454,7 @@ os.PathLike]` instead of just `os.PathLike` (#347 by [@bbc2]). [@bbc2]: https://github.com/bbc2 [@befeleme]: https://github.com/befeleme [@cjauvin]: https://github.com/cjauvin +[@dchaudhari7177]: https://github.com/dchaudhari7177 [@eaf]: https://github.com/eaf [@earlbread]: https://github.com/earlbread [@eekstunt]: https://github.com/eekstunt diff --git a/src/dotenv/parser.py b/src/dotenv/parser.py index 66773604..6625ad7c 100644 --- a/src/dotenv/parser.py +++ b/src/dotenv/parser.py @@ -120,12 +120,17 @@ def parse_key(reader: Reader) -> Optional[str]: return key -def parse_unquoted_value(reader: Reader) -> str: +def parse_unquoted_value(reader: Reader, preceded_by_whitespace: bool = False) -> str: (part,) = reader.read_regex(_unquoted_value) + # An unquoted value only ends at a `#` that is preceded by whitespace. The + # whitespace right after the `=` is consumed by `_equal_sign`, so a value + # starting with `#` is a comment only if that whitespace was there. + if preceded_by_whitespace and part.startswith("#"): + return "" return re.sub(r"\s+#.*", "", part).rstrip() -def parse_value(reader: Reader) -> str: +def parse_value(reader: Reader, preceded_by_whitespace: bool = False) -> str: char = reader.peek(1) if char == "'": (value,) = reader.read_regex(_single_quoted_value) @@ -136,7 +141,7 @@ def parse_value(reader: Reader) -> str: elif char in ("", "\n", "\r"): return "" else: - return parse_unquoted_value(reader) + return parse_unquoted_value(reader, preceded_by_whitespace) def parse_binding(reader: Reader) -> Binding: @@ -154,8 +159,10 @@ def parse_binding(reader: Reader) -> Binding: key = parse_key(reader) reader.read_regex(_whitespace) if reader.peek(1) == "=": - reader.read_regex(_equal_sign) - value: Optional[str] = parse_value(reader) + (equal_sign,) = reader.read_regex(_equal_sign) + value: Optional[str] = parse_value( + reader, preceded_by_whitespace=len(equal_sign) > 1 + ) else: value = None reader.read_regex(_comment) diff --git a/tests/test_parser.py b/tests/test_parser.py index 4ec5a5af..d2def9df 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -119,6 +119,56 @@ ) ], ), + ( + "a= #c", + [ + Binding( + key="a", + value="", + original=Original(string="a= #c", line=1), + error=False, + ) + ], + ), + ( + "a=\t#c", + [ + Binding( + key="a", + value="", + original=Original(string="a=\t#c", line=1), + error=False, + ) + ], + ), + ( + "a=#c", + [ + Binding( + key="a", + value="#c", + original=Original(string="a=#c", line=1), + error=False, + ) + ], + ), + ( + "a= #c\nd=e", + [ + Binding( + key="a", + value="", + original=Original(string="a= #c\n", line=1), + error=False, + ), + Binding( + key="d", + value="e", + original=Original(string="d=e", line=2), + error=False, + ), + ], + ), ( "a=b c", [