diff --git a/esphome/ota_key_edit.py b/esphome/ota_key_edit.py
index 8465ee05b62..5dcdf07c0d3 100644
--- a/esphome/ota_key_edit.py
+++ b/esphome/ota_key_edit.py
@@ -67,7 +67,6 @@ _INDENT_RE = re.compile(r"\s*")
_KEY_FIELD_RE = re.compile(rf"^\s*{CONF_KEY}\s*:")
_BARE_BLOCK_RE = re.compile(rf"^\s*{CONF_ENCRYPTION}\s*:\s*(#.*)?$")
_YAML_SUFFIXES = (".yaml", ".yml")
-_COMMENT_RE = re.compile(r"(?:^|\s)#.*$")
_INCLUDE_DIR_RE = re.compile(r"!include_dir_\w+\s+(?P
[^\s#]+)")
# An include or package file whose path holds a substitution
_SUBSTITUTED_INCLUDE_RE = re.compile(
@@ -100,8 +99,18 @@ def _indent(text: str) -> str:
def _without_comment(text: str) -> str:
- """The line up to its comment, which starts at a `#` after whitespace."""
- return _COMMENT_RE.sub("", text)
+ """The line up to its comment: a `#` after whitespace that is not inside
+ a quoted scalar, so `!include "shared # ota.yaml"` keeps its name."""
+ quote = ""
+ for i, char in enumerate(text):
+ if quote:
+ if char == quote:
+ quote = ""
+ elif char in "\"'":
+ quote = char
+ elif char == "#" and (i == 0 or text[i - 1].isspace()):
+ return text[:i]
+ return text
def _file_use_re(file_name: str) -> re.Pattern[str]:
diff --git a/tests/unit_tests/test_ota_key_edit.py b/tests/unit_tests/test_ota_key_edit.py
index 74022a9de8f..3a4845b44f8 100644
--- a/tests/unit_tests/test_ota_key_edit.py
+++ b/tests/unit_tests/test_ota_key_edit.py
@@ -729,6 +729,28 @@ def test_shared_secret_scan_covers_yml_and_skips_unreadable_files(
assert [e.shared_with for e in edits] == [[tmp_path.resolve() / "other.yml"]]
+def test_shared_include_with_a_hash_in_its_quoted_name_is_reported(
+ tmp_path: Path,
+) -> None:
+ """A `#` inside a quoted include path is part of the name, not a comment."""
+ (tmp_path / "shared # ota.yaml").write_bytes(
+ f'encryption:\n key: "{OLD_KEY}"\n'.encode()
+ )
+ (tmp_path / "other.yaml").write_bytes(
+ b'api: !include "shared # ota.yaml" # the device next door\n'
+ )
+ _setup(
+ tmp_path,
+ "esphome:\n name: test\n\napi: !include 'shared # ota.yaml'\n\n"
+ "ota:\n - platform: esphome\n encryption:\n",
+ )
+ edits = locate_key_edits(OLD_KEY, NEW_KEY)
+ with_sharers(edits)
+ assert [(e.path.name, e.shared_with) for e in edits] == [
+ ("shared # ota.yaml", [tmp_path.resolve() / "other.yaml"])
+ ]
+
+
def test_literal_key_in_a_shared_include_is_reported(tmp_path: Path) -> None:
"""Another configuration including the file that holds the key is
listed, like a shared secret."""