diff --git a/src/specify_cli/integrations/_helpers.py b/src/specify_cli/integrations/_helpers.py index d1bf051f77..ce273f9d72 100644 --- a/src/specify_cli/integrations/_helpers.py +++ b/src/specify_cli/integrations/_helpers.py @@ -190,7 +190,15 @@ def _parse_integration_options(integration: Any, raw_options: str) -> dict[str, """ import shlex parsed: dict[str, Any] = {} - tokens = shlex.split(raw_options) + try: + tokens = shlex.split(raw_options) + except ValueError as exc: + # An unbalanced quote (e.g. --integration-options='--commands-dir "foo') + # makes shlex raise "No closing quotation". Translate it into the same + # clean exit-1 UX as every other bad-input path below rather than + # letting a raw traceback escape. + console.print(f"[red]Error:[/red] Could not parse integration options: {exc}.") + raise typer.Exit(1) declared_options = list(integration.options()) declared = {opt.name.lstrip("-"): opt for opt in declared_options} allowed = ", ".join(sorted(opt.name for opt in declared_options)) diff --git a/tests/integrations/test_integration_subcommand.py b/tests/integrations/test_integration_subcommand.py index 56f338cc82..3ec48db747 100644 --- a/tests/integrations/test_integration_subcommand.py +++ b/tests/integrations/test_integration_subcommand.py @@ -2675,6 +2675,26 @@ def test_equals_form_parsed(self): assert result_space["commands_dir"] == "./mydir" assert result_equals["commands_dir"] == "./mydir" + def test_unbalanced_quote_exits_cleanly(self): + """An unbalanced quote must exit(1) with a message, not a raw ValueError. + + shlex.split() raises ValueError("No closing quotation") on an unbalanced + quote; the parser must translate that into the same clean typer.Exit(1) + UX as unknown-option / missing-value, rather than letting the traceback + escape (issue #3457). + """ + import typer + + from specify_cli.integrations._commands import _parse_integration_options + from specify_cli.integrations import get_integration + + integration = get_integration("generic") + assert integration is not None + + with pytest.raises(typer.Exit) as excinfo: + _parse_integration_options(integration, '--commands-dir "foo') + assert excinfo.value.exit_code == 1 + class TestUninstallNoManifestClearsInitOptions: def test_init_options_cleared_on_no_manifest_uninstall(self, tmp_path):