Skip to content

Commit 7abc0cc

Browse files
fix(integrations): exit cleanly on unbalanced quote in --integration-options (#3457)
`_parse_integration_options` called `shlex.split(raw_options)` unguarded, so an unbalanced quote in the flag value (e.g. `--integration-options='--commands-dir "foo'`) made shlex raise `ValueError: No closing quotation` and a raw traceback escaped — unlike every other bad-input path in this function (unknown option, missing value, unexpected value), which print a message and exit 1. Reachable from `specify init --integration-options=...` and every `specify integration install/switch/upgrade/migrate --integration-options=...`. Wrap the split in a try/except ValueError that prints a one-line error and raises `typer.Exit(1)`, matching the existing loud-fail UX. Add a test asserting the unbalanced-quote input raises `typer.Exit` with exit code 1. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 1be4299 commit 7abc0cc

2 files changed

Lines changed: 29 additions & 1 deletion

File tree

src/specify_cli/integrations/_helpers.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,15 @@ def _parse_integration_options(integration: Any, raw_options: str) -> dict[str,
190190
"""
191191
import shlex
192192
parsed: dict[str, Any] = {}
193-
tokens = shlex.split(raw_options)
193+
try:
194+
tokens = shlex.split(raw_options)
195+
except ValueError as exc:
196+
# An unbalanced quote (e.g. --integration-options='--commands-dir "foo')
197+
# makes shlex raise "No closing quotation". Translate it into the same
198+
# clean exit-1 UX as every other bad-input path below rather than
199+
# letting a raw traceback escape.
200+
console.print(f"[red]Error:[/red] Could not parse integration options: {exc}.")
201+
raise typer.Exit(1)
194202
declared_options = list(integration.options())
195203
declared = {opt.name.lstrip("-"): opt for opt in declared_options}
196204
allowed = ", ".join(sorted(opt.name for opt in declared_options))

tests/integrations/test_integration_subcommand.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2675,6 +2675,26 @@ def test_equals_form_parsed(self):
26752675
assert result_space["commands_dir"] == "./mydir"
26762676
assert result_equals["commands_dir"] == "./mydir"
26772677

2678+
def test_unbalanced_quote_exits_cleanly(self):
2679+
"""An unbalanced quote must exit(1) with a message, not a raw ValueError.
2680+
2681+
shlex.split() raises ValueError("No closing quotation") on an unbalanced
2682+
quote; the parser must translate that into the same clean typer.Exit(1)
2683+
UX as unknown-option / missing-value, rather than letting the traceback
2684+
escape (issue #3457).
2685+
"""
2686+
import typer
2687+
2688+
from specify_cli.integrations._commands import _parse_integration_options
2689+
from specify_cli.integrations import get_integration
2690+
2691+
integration = get_integration("generic")
2692+
assert integration is not None
2693+
2694+
with pytest.raises(typer.Exit) as excinfo:
2695+
_parse_integration_options(integration, '--commands-dir "foo')
2696+
assert excinfo.value.exit_code == 1
2697+
26782698

26792699
class TestUninstallNoManifestClearsInitOptions:
26802700
def test_init_options_cleared_on_no_manifest_uninstall(self, tmp_path):

0 commit comments

Comments
 (0)