diff --git a/esphome/arduino8266/component.py b/esphome/arduino8266/component.py index 94d008374c..6365a26dda 100644 --- a/esphome/arduino8266/component.py +++ b/esphome/arduino8266/component.py @@ -16,7 +16,6 @@ from __future__ import annotations from dataclasses import dataclass, field import logging from pathlib import Path -import shlex from esphome.core import CORE, Library from esphome.espidf.extra_script import apply_extra_script @@ -36,6 +35,7 @@ from esphome.platformio.library import ( lib_ignore_set, normalize_dependencies, parse_library_properties, + split_flag_entry, ) _LOGGER = logging.getLogger(__name__) @@ -79,7 +79,7 @@ def _library_info(name: str, read_path: Path, data: dict) -> ArduinoLibrary: ( token for entry in ensure_list(build.get("flags", [])) - for token in shlex.split(entry) + for token in split_flag_entry(entry, f"library {name}") ), f"library {name}", ) diff --git a/esphome/build_gen/arduino8266.py b/esphome/build_gen/arduino8266.py index cbb09057f2..e5af8cc1c4 100644 --- a/esphome/build_gen/arduino8266.py +++ b/esphome/build_gen/arduino8266.py @@ -19,7 +19,6 @@ import logging import os from pathlib import Path import re -import shlex import subprocess import sys @@ -40,7 +39,7 @@ from esphome.const import KEY_CORE, KEY_FRAMEWORK_VERSION from esphome.core import CORE, EsphomeError from esphome.framework_helpers import get_project_cxx_compile_flags from esphome.helpers import mkdir_p, write_file_if_changed -from esphome.platformio.library import join_flag_args +from esphome.platformio.library import join_flag_args, split_flag_entry _LOGGER = logging.getLogger(__name__) @@ -179,7 +178,7 @@ def _flag_defines() -> dict[str, str]: # Shell-lex multi-token entries the way PlatformIO does, so a knob # in "-DKNOB -DOTHER" is still detected; single tokens pass verbatim # to keep any quoting in their bodies intact. - for tok in shlex.split(flag) if " " in flag else (flag,): + for tok in split_flag_entry(flag, "esphome") if " " in flag else (flag,): if tok.startswith("-D"): body = tok[2:] defines[body.split("=", 1)[0]] = body @@ -319,7 +318,7 @@ def _project_flags() -> tuple[list[str], list[str], list[Path], list[str]]: # they do under PlatformIO. Other entries pass verbatim: lexing them # would strip the quotes in defines like -DBOARD="...". tokens = ( - join_flag_args(shlex.split(flag), "esphome") + join_flag_args(split_flag_entry(flag, "esphome"), "esphome") if flag.startswith(("-L", "-l")) else [flag] ) diff --git a/esphome/espidf/component.py b/esphome/espidf/component.py index 1c951ae274..68c9398e19 100644 --- a/esphome/espidf/component.py +++ b/esphome/espidf/component.py @@ -28,6 +28,7 @@ from esphome.platformio.library import ( convert_libraries, ensure_list, join_flag_args, + split_flag_entry, split_list_by_condition, ) @@ -62,10 +63,6 @@ def generate_cmakelists_txt(component: IDFComponent) -> str: Returns: str: The complete CMakeLists.txt content as a string """ - # Late import: this module loads with the esp32 platform on every - # validate/compile, but shlex is only needed when generating component - # CMakeLists. - import shlex def escape_entry(p: PathType) -> str: # In CMakeLists.txt, backslashes need to be escaped @@ -107,7 +104,11 @@ def generate_cmakelists_txt(component: IDFComponent) -> str: # -I/-L/-l tokens re-glue to their argument ("-I foo" -> "-Ifoo") so the # prefix classifiers below still route them. build_flags = join_flag_args( - (token for entry in build_flags for token in shlex.split(entry)), + ( + token + for entry in build_flags + for token in split_flag_entry(entry, f"library {component.name}") + ), f"library {component.name}", ) diff --git a/esphome/platformio/library.py b/esphome/platformio/library.py index 5c936c280f..c1e659e0f6 100644 --- a/esphome/platformio/library.py +++ b/esphome/platformio/library.py @@ -553,6 +553,16 @@ def _resolve_registry_version( return owner, name, best["name"], pkgfile["download_url"] +def split_flag_entry(entry: str, owner: str) -> list[str]: + """``shlex.split`` with a clean error naming the offending flags entry.""" + import shlex + + try: + return shlex.split(entry) + except ValueError as err: + raise EsphomeError(f"Malformed build flag {entry!r} in {owner}: {err}") from err + + def join_flag_args(tokens: Iterable[str], owner: str) -> list[str]: """Join a bare ``-I``/``-L``/``-l`` with its following token (PIO lexing).""" out: list[str] = [] diff --git a/tests/unit_tests/test_platformio_library.py b/tests/unit_tests/test_platformio_library.py index 0eede78656..6c7f6d6462 100644 --- a/tests/unit_tests/test_platformio_library.py +++ b/tests/unit_tests/test_platformio_library.py @@ -531,3 +531,12 @@ def test_convert_libraries_skips_incompatible_dependency(tmp_path, monkeypatch): top = convert_libraries([Library("esphome/A", "1.0.0", None)], _backend()) assert top[0].dependencies == [] + + +def test_split_flag_entry_unbalanced_quote_is_clean() -> None: + """A malformed flags entry raises EsphomeError, not a raw ValueError.""" + from esphome.platformio.library import split_flag_entry + + assert split_flag_entry('-DX="a b"', "library x") == ["-DX=a b"] + with pytest.raises(EsphomeError, match=r"Malformed build flag.*library x"): + split_flag_entry('-DX="unclosed', "library x")