From 386598fd8261692c94926114beffc17b41fb2ee5 Mon Sep 17 00:00:00 2001 From: Roman Lutz Date: Wed, 15 Apr 2026 05:44:49 -0700 Subject: [PATCH] Preserve quoted shell arguments in run parsing Replace naive str.split() with shlex.split() in parse_run_arguments() and parse_list_targets_arguments() to correctly handle: - Quoted paths with spaces (e.g., "/tmp/my script.py") - Quoted JSON values (e.g., '{"experiment": "test 1"}') - Unterminated quote detection (raises ValueError) The -s flag swallowing bug from PR #1483 was already fixed in the _parse_shell_arguments refactor (flag_to_spec lookup catches short aliases). Update existing memory-labels test to use properly quoted JSON (required by shlex, matching real shell behavior). Add regression tests for quoted paths, quoted JSON with spaces, -s after initializers, and unterminated quotes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- pyrit/cli/_cli_args.py | 5 +++-- tests/unit/cli/test_frontend_core.py | 33 ++++++++++++++++++++++++++-- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/pyrit/cli/_cli_args.py b/pyrit/cli/_cli_args.py index 2131f72a4..b4bb58441 100644 --- a/pyrit/cli/_cli_args.py +++ b/pyrit/cli/_cli_args.py @@ -18,6 +18,7 @@ import inspect import json import logging +import shlex from pathlib import Path from typing import TYPE_CHECKING, Any, Optional @@ -533,7 +534,7 @@ def parse_run_arguments(*, args_string: str) -> dict[str, Any]: Raises: ValueError: If parsing or validation fails. """ - parts = args_string.split() + parts = shlex.split(args_string) if not parts: raise ValueError("No scenario name provided") @@ -558,7 +559,7 @@ def parse_list_targets_arguments(*, args_string: str) -> dict[str, Any]: Raises: ValueError: If parsing or validation fails. """ - parts = args_string.split() + parts = shlex.split(args_string) return _parse_shell_arguments(parts=parts, arg_specs=_LIST_TARGETS_ARG_SPECS) diff --git a/tests/unit/cli/test_frontend_core.py b/tests/unit/cli/test_frontend_core.py index 2507f4eb4..d2556a9ad 100644 --- a/tests/unit/cli/test_frontend_core.py +++ b/tests/unit/cli/test_frontend_core.py @@ -710,8 +710,8 @@ def test_parse_run_arguments_with_max_retries(self): assert result["max_retries"] == 3 def test_parse_run_arguments_with_memory_labels(self): - """Test parsing with memory-labels.""" - result = frontend_core.parse_run_arguments(args_string='test_scenario --memory-labels {"key":"value"}') + """Test parsing with memory-labels (JSON must be quoted in shell mode).""" + result = frontend_core.parse_run_arguments(args_string="""test_scenario --memory-labels '{"key":"value"}'""") assert result["memory_labels"] == {"key": "value"} @@ -729,6 +729,35 @@ def test_parse_run_arguments_with_initialization_scripts(self): assert result["initialization_scripts"] == ["script1.py", "script2.py"] + def test_parse_run_arguments_with_quoted_paths(self): + """Test parsing quoted paths with spaces for shell mode.""" + result = frontend_core.parse_run_arguments( + args_string='test_scenario --initialization-scripts "/tmp/my script.py" --strategies s1' + ) + + assert result["initialization_scripts"] == ["/tmp/my script.py"] + assert result["scenario_strategies"] == ["s1"] + + def test_parse_run_arguments_with_quoted_memory_labels(self): + """Test parsing quoted JSON for memory-labels in shell mode.""" + result = frontend_core.parse_run_arguments( + args_string="""test_scenario --memory-labels '{"experiment": "test 1"}'""" + ) + + assert result["memory_labels"] == {"experiment": "test 1"} + + def test_parse_run_arguments_with_short_strategies_after_initializers(self): + """Test that -s is treated as a flag after multi-value initializers.""" + result = frontend_core.parse_run_arguments(args_string="test_scenario --initializers init1 -s s1 s2") + + assert result["initializers"] == ["init1"] + assert result["scenario_strategies"] == ["s1", "s2"] + + def test_parse_run_arguments_unterminated_quote_raises(self): + """Test that unterminated quotes raise ValueError.""" + with pytest.raises(ValueError): + frontend_core.parse_run_arguments(args_string='test_scenario --initialization-scripts "/tmp/my script.py') + def test_parse_run_arguments_complex(self): """Test parsing complex argument combination.""" args = "test_scenario --initializers init1 --strategies s1 s2 --max-concurrency 10"