fix(events): return None for an unparseable script command (#3957)

_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>
This commit is contained in:
Marsel Safin
2026-08-04 07:34:02 -05:00
committed by GitHub
co-authored by Copilot
parent 99970560db
commit 316cd1235a
2 changed files with 33 additions and 1 deletions
+7 -1
View File
@@ -584,7 +584,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]
+26
View File
@@ -1186,6 +1186,32 @@ class TestCommandRunner:
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."""