Skip to content

Commit d9e4565

Browse files
fix(integrations): escape Rich markup in --integration-options error messages (#3458)
* fix(integrations): exit cleanly on malformed --integration-options quoting _parse_integration_options called shlex.split(raw_options) unguarded. an unbalanced quote (e.g. --integration-options='--commands-dir "foo') makes shlex raise ValueError('No closing quotation'), so a raw traceback escaped instead of the typer.Exit(1) error every other bad-input path in this function produces. reachable from specify init and every integration install/switch/ upgrade/migrate that accepts --integration-options. wrap the split and convert ValueError into the same clean CLI error. added a regression test; confirmed it fails on the pre-fix code (raw ValueError). * escape user-controlled values in integration-options error messages the malformed-quoting handler (and the unexpected/unknown option branches) interpolate raw_options/token into console.print. a value carrying an unbalanced rich tag like '--commands-dir "[/red]foo' first trips the intended shlex ValueError, but the error print then raises rich.errors.MarkupError and leaks a traceback anyway. escape all three before printing so the clean typer.Exit survives. added a regression covering both the shlex path and a bare markup token. * address review: drop redundant, mis-described markup test The removed test's docstring claimed the shlex failure branch interpolates raw_options into console.print; it only prints {exc}, which never contains the caller's markup. Its two assertions also duplicated test_bad_option_token_with_rich_markup_exits_cleanly (the '[/red]foo' case) and the shlex-path case already covered by test_unbalanced_quote_exits_cleanly. The real change here remains the escape() of the two user-controlled token prints.
1 parent 840fb8d commit d9e4565

2 files changed

Lines changed: 27 additions & 2 deletions

File tree

src/specify_cli/integrations/_helpers.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from typing import Any, Callable
77

88
import typer
9+
from rich.markup import escape
910

1011
from .._agent_config import SCRIPT_TYPE_CHOICES
1112
from .._console import console
@@ -206,7 +207,7 @@ def _parse_integration_options(integration: Any, raw_options: str) -> dict[str,
206207
while i < len(tokens):
207208
token = tokens[i]
208209
if not token.startswith("-"):
209-
console.print(f"[red]Error:[/red] Unexpected integration option value '{token}'.")
210+
console.print(f"[red]Error:[/red] Unexpected integration option value '{escape(token)}'.")
210211
if allowed:
211212
console.print(f"Allowed options: {allowed}")
212213
raise typer.Exit(1)
@@ -217,7 +218,7 @@ def _parse_integration_options(integration: Any, raw_options: str) -> dict[str,
217218
name, value = name.split("=", 1)
218219
opt = declared.get(name)
219220
if not opt:
220-
console.print(f"[red]Error:[/red] Unknown integration option '{token}'.")
221+
console.print(f"[red]Error:[/red] Unknown integration option '{escape(token)}'.")
221222
if allowed:
222223
console.print(f"Allowed options: {allowed}")
223224
raise typer.Exit(1)

tests/integrations/test_integration_subcommand.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3119,6 +3119,30 @@ def test_unbalanced_quote_exits_cleanly(self, capsys):
31193119
assert excinfo.value.exit_code == 1
31203120
assert "Error: Could not parse integration options: No closing quotation." in capsys.readouterr().out
31213121

3122+
def test_bad_option_token_with_rich_markup_exits_cleanly(self):
3123+
"""A bad option token carrying Rich markup must exit cleanly, not crash.
3124+
3125+
The token is user-controlled and gets interpolated into console.print.
3126+
A value like '[/red]foo' parses fine through shlex but is an unexpected
3127+
value / unknown option — and an unbalanced Rich tag would raise
3128+
rich.errors.MarkupError inside console.print, leaking a traceback
3129+
instead of the intended typer.Exit(1). The token must be escaped."""
3130+
import typer
3131+
3132+
from specify_cli.integrations._commands import _parse_integration_options
3133+
from specify_cli.integrations import get_integration
3134+
3135+
integration = get_integration("generic")
3136+
assert integration is not None
3137+
3138+
# Unexpected value token carrying markup.
3139+
with pytest.raises(typer.Exit):
3140+
_parse_integration_options(integration, "[/red]foo")
3141+
3142+
# Unknown option token carrying markup.
3143+
with pytest.raises(typer.Exit):
3144+
_parse_integration_options(integration, "--[/red]bad")
3145+
31223146

31233147
class TestUninstallNoManifestClearsInitOptions:
31243148
def test_init_options_cleared_on_no_manifest_uninstall(self, tmp_path):

0 commit comments

Comments
 (0)