diff --git a/src/specify_cli/integrations/_helpers.py b/src/specify_cli/integrations/_helpers.py index d1bf051f77..60d718a104 100644 --- a/src/specify_cli/integrations/_helpers.py +++ b/src/specify_cli/integrations/_helpers.py @@ -190,7 +190,18 @@ 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 error: + # shlex raises on malformed input (e.g. an unbalanced quote in + # --integration-options='--commands-dir "foo'). Surface the clean CLI + # error every other bad-input path here produces instead of leaking a + # raw ValueError traceback. + console.print( + f"[red]Error:[/red] Could not parse integration options " + f"'{raw_options}': {error}." + ) + raise typer.Exit(1) from error 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..a3bd75c31c 100644 --- a/tests/integrations/test_integration_subcommand.py +++ b/tests/integrations/test_integration_subcommand.py @@ -2675,6 +2675,24 @@ def test_equals_form_parsed(self): assert result_space["commands_dir"] == "./mydir" assert result_equals["commands_dir"] == "./mydir" + def test_malformed_quoting_exits_cleanly(self): + """An unbalanced quote must raise a clean typer.Exit, not a ValueError. + + shlex.split raises ValueError('No closing quotation') on malformed + input. Every other bad-input path in this function exits via + typer.Exit(1); a raw ValueError traceback would be inconsistent and + user-hostile for a plain CLI-flag typo.""" + 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): + _parse_integration_options(integration, '--commands-dir "foo') + class TestUninstallNoManifestClearsInitOptions: def test_init_options_cleared_on_no_manifest_uninstall(self, tmp_path):