Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions src/specify_cli/workflows/expressions.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,11 +230,14 @@ def _evaluate_simple_expression(expr: str, namespace: dict[str, Any]) -> Any:
if expr[:1] in ("'", '"') and expr.find(expr[0], 1) == len(expr) - 1:
return expr[1:-1]

# Handle pipe filters
if "|" in expr:
parts = expr.split("|", 1)
value = _evaluate_simple_expression(parts[0].strip(), namespace)
filter_expr = parts[1].strip()
# Handle pipe filters. Split at the first *top-level* ``|`` so a pipe inside
# a quoted operand (e.g. the ``|`` in ``mode == 'read|write'``) or nested
# brackets is not mistaken for a filter separator. A naive ``"|" in expr``
# check would mis-parse such operands and raise a spurious "unknown filter".
pipe_idx = _find_top_level(expr, "|")
if pipe_idx != -1:
value = _evaluate_simple_expression(expr[:pipe_idx].strip(), namespace)
filter_expr = expr[pipe_idx + 1:].strip()

# `from_json` is strict: it takes no arguments and tolerates no
# trailing tokens. Match on the leading filter name and require the
Expand Down
22 changes: 22 additions & 0 deletions tests/test_workflows.py
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,28 @@ def test_operator_splitting_is_quote_aware(self):
assert evaluate_expression("{{ inputs.a == 9 or inputs.b == 2 }}", plain) is True
assert evaluate_expression("{{ inputs.missing | default('a and b') }}", plain) == "a and b"

def test_pipe_splitting_is_quote_aware(self):
from specify_cli.workflows.expressions import evaluate_expression
from specify_cli.workflows.base import StepContext

# A '|' INSIDE a quoted operand must not be treated as a filter
# separator: the comparison applies to the whole string literal.
# Previously this raised ValueError("unknown filter 'write'") because
# the pipe split was not quote-aware (unlike and/or/comparison).
ctx = StepContext(inputs={"mode": "read|write"})
assert evaluate_expression("{{ inputs.mode == 'read|write' }}", ctx) is True
assert evaluate_expression("{{ inputs.mode != 'a|b' }}", ctx) is True

# A single quoted literal that itself contains a pipe is preserved.
assert evaluate_expression("{{ 'read|write' }}", StepContext()) == "read|write"

# A pipe used as a real filter argument still works (separator is '|').
joined = StepContext(inputs={"tags": ["a", "b", "c"]})
assert evaluate_expression("{{ inputs.tags | join('|') }}", joined) == "a|b|c"

# Regression: an ordinary (top-level) filter still applies.
assert evaluate_expression("{{ inputs.missing | default('x') }}", StepContext()) == "x"

def test_filter_default(self):
from specify_cli.workflows.expressions import evaluate_expression
from specify_cli.workflows.base import StepContext
Expand Down