From 418921868decae8fadee867d32536238f0dd0c40 Mon Sep 17 00:00:00 2001 From: marcelsafin <179933638+marcelsafin@users.noreply.github.com> Date: Mon, 3 Aug 2026 21:36:14 +0200 Subject: [PATCH] fix(events): return None for an unparseable script command _script_command() split the configured command with a bare shlex.split(), so a command string with unbalanced quotes crashed event dispatch with a raw ValueError. The dispatcher-template twin a few lines up already wraps the same call in try/except ValueError and returns None so dispatch falls back cleanly. Wrap the split the same way and return None, restoring parity between the two paths. Assisted-by: GitHub Copilot (model: claude-fable-5, autonomous) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/specify_cli/events.py | 8 +++++++- tests/integrations/test_events.py | 26 ++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/src/specify_cli/events.py b/src/specify_cli/events.py index d3002fe805..bd4f09c29a 100644 --- a/src/specify_cli/events.py +++ b/src/specify_cli/events.py @@ -524,7 +524,13 @@ def _resolve_event_command_argv( else: base = project_root / ".specify" - tokens = shlex.split(script_cmd, posix=(os.name != "nt")) + try: + tokens = shlex.split(script_cmd, posix=(os.name != "nt")) + except ValueError: + # Mirror the generated dispatcher's _resolve_argv: a scripts: value + # shlex cannot tokenize (e.g. an unclosed quote) declares no runnable + # script, so degrade to "no argv" instead of raising. + return None if not tokens: return None script_abs = base / tokens[0] diff --git a/tests/integrations/test_events.py b/tests/integrations/test_events.py index 084156a523..1927f8ed61 100644 --- a/tests/integrations/test_events.py +++ b/tests/integrations/test_events.py @@ -1031,6 +1031,32 @@ def test_py_variant_anchored_under_specify(self, tmp_path): assert PurePath(argv[1]).as_posix().endswith(".specify/scripts/python/boot.py") assert ".specify" in argv[1] + def test_unparseable_script_command_returns_none(self, tmp_path): + """A ``scripts:`` value shlex cannot tokenize must resolve to no argv. + + The generated dispatcher's ``_resolve_argv`` twin wraps its + ``shlex.split`` in ``except ValueError: return None``, but the + CLI-side resolver did not: an unclosed quote in a ``scripts:`` + frontmatter value raised a raw ``ValueError: No closing quotation`` + through ``resolve_and_run_event_command`` instead of degrading to + "no runnable script" like every other malformed-input case here. + """ + from specify_cli.events import _resolve_event_command_argv + + cmd_dir = tmp_path / ".specify" / "templates" / "commands" + cmd_dir.mkdir(parents=True) + (cmd_dir / "boot.md").write_text( + "---\n" + "description: \"Boot\"\n" + "scripts:\n" + " sh: scripts/bash/boot.sh \"unclosed\n" + "---\nBody\n", + encoding="utf-8", + ) + + argv = _resolve_event_command_argv(cmd_dir / "boot.md", tmp_path, None) + assert argv is None + def test_ps_variant_prefixed_with_powershell_launcher(self, tmp_path): """S6: the ps variant prefixes argv with pwsh/powershell -File so subprocess.run(shell=False) can execute the .ps1 script."""