Skip to content

Commit fdaaf18

Browse files
jawwad-aliclaude
andauthored
fix(workflows): preserve commas inside quoted list-literal elements (#3134)
* fix(workflows): preserve commas inside quoted list-literal elements The simple-expression evaluator parsed a list literal with a naive `inner.split(",")`, which splits on commas inside quoted strings (and nested brackets). So `{{ ["a, b", "c"] }}` evaluated to three items (`["a", "b", "c"]`) instead of two, silently corrupting `fan-out` `items:` and any list expression that contains a comma inside a quoted element. Split list-literal elements on top-level commas only, ignoring commas inside quotes or nested brackets, via a small `_split_top_level_commas` helper. Plain and empty lists are unchanged. Add tests covering quoted commas, nested lists, and the existing plain/empty cases. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(workflows): cover single-quoted and nested list literals Address review: extend the list-literal regression test to assert single-quoted elements with commas and nested lists parse correctly, alongside the existing double-quoted cases. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent e5df517 commit fdaaf18

2 files changed

Lines changed: 56 additions & 1 deletion

File tree

src/specify_cli/workflows/expressions.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,40 @@ def _build_namespace(context: Any) -> dict[str, Any]:
146146
return ns
147147

148148

149+
def _split_top_level_commas(text: str) -> list[str]:
150+
"""Split *text* on commas that are not inside quotes or nested brackets.
151+
152+
Used for list-literal elements so a quoted element containing a comma
153+
(e.g. ``["a, b", "c"]``) is not split mid-string, and nested lists/calls
154+
(e.g. ``[[1, 2], 3]``) are kept intact.
155+
"""
156+
parts: list[str] = []
157+
buf: list[str] = []
158+
quote: str | None = None
159+
depth = 0
160+
for ch in text:
161+
if quote is not None:
162+
buf.append(ch)
163+
if ch == quote:
164+
quote = None
165+
elif ch in ("'", '"'):
166+
quote = ch
167+
buf.append(ch)
168+
elif ch in "([{":
169+
depth += 1
170+
buf.append(ch)
171+
elif ch in ")]}":
172+
depth = max(0, depth - 1)
173+
buf.append(ch)
174+
elif ch == "," and depth == 0:
175+
parts.append("".join(buf))
176+
buf = []
177+
else:
178+
buf.append(ch)
179+
parts.append("".join(buf))
180+
return parts
181+
182+
149183
def _evaluate_simple_expression(expr: str, namespace: dict[str, Any]) -> Any:
150184
"""Evaluate a simple expression against the namespace.
151185
@@ -291,7 +325,10 @@ def _evaluate_simple_expression(expr: str, namespace: dict[str, Any]) -> Any:
291325
inner = expr[1:-1].strip()
292326
if not inner:
293327
return []
294-
items = [_evaluate_simple_expression(i.strip(), namespace) for i in inner.split(",")]
328+
items = [
329+
_evaluate_simple_expression(i.strip(), namespace)
330+
for i in _split_top_level_commas(inner)
331+
]
295332
return items
296333

297334
# Variable reference (dot-path)

tests/test_workflows.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,24 @@ def test_boolean_or(self):
268268
ctx = StepContext(inputs={"a": False, "b": True})
269269
assert evaluate_expression("{{ inputs.a or inputs.b }}", ctx) is True
270270

271+
def test_list_literal_preserves_quoted_commas(self):
272+
from specify_cli.workflows.expressions import evaluate_expression
273+
from specify_cli.workflows.base import StepContext
274+
275+
ctx = StepContext()
276+
# commas inside a double-quoted element must not split it
277+
assert evaluate_expression('{{ ["a, b", "c"] }}', ctx) == ["a, b", "c"]
278+
assert evaluate_expression('{{ ["x, y, z"] }}', ctx) == ["x, y, z"]
279+
# single-quoted elements are handled the same way
280+
assert evaluate_expression("{{ ['a, b', 'c'] }}", ctx) == ["a, b", "c"]
281+
assert evaluate_expression("{{ ['p, q, r'] }}", ctx) == ["p, q, r"]
282+
# plain and empty lists still parse correctly
283+
assert evaluate_expression("{{ [1, 2, 3] }}", ctx) == [1, 2, 3]
284+
assert evaluate_expression("{{ [] }}", ctx) == []
285+
# nested lists (commas inside the inner brackets) stay intact
286+
assert evaluate_expression('{{ [["a", "b"], "c"] }}', ctx) == [["a", "b"], "c"]
287+
assert evaluate_expression("{{ [[1, 2], [3, 4]] }}", ctx) == [[1, 2], [3, 4]]
288+
271289
def test_filter_default(self):
272290
from specify_cli.workflows.expressions import evaluate_expression
273291
from specify_cli.workflows.base import StepContext

0 commit comments

Comments
 (0)