Skip to content

Bug: multi-expression templates raise ValueError on a literal }} inside a filter argument #3306

Description

@Quratulain-bilal

summary

evaluate_expression raises ValueError on a multi-expression template when any block has a literal }} inside a quoted filter argument, e.g. default('}}'). it should interpolate.

root cause

there are two paths in evaluate_expression:

_EXPR_PATTERN = re.compile(r"\{\{(.+?)\}\}")
...
return _EXPR_PATTERN.sub(_replacer, template)

the non-greedy (.+?)}} body stops at the first }} regardless of quoting. so in a multi-expression template, a block whose argument contains a literal }} gets captured truncated (e.g. inputs.missing | default(' instead of inputs.missing | default('}}')). that truncated body reaches the filter parser malformed and raises ValueError.

so #3208/#3228 fixed this class for a lone expression but left the interpolation path exposed.

reproduction

from specify_cli.workflows.expressions import evaluate_expression
from specify_cli.workflows.base import StepContext

ctx = StepContext(inputs={"name": "Bob", "missing": None})

# raises ValueError, should return "Bob: }}"
evaluate_expression("{{ inputs.name }}: {{ inputs.missing | default('}}') }}", ctx)

# raises ValueError, should return "}} / Bob"
evaluate_expression("{{ inputs.missing | default('}}') }} / {{ inputs.name }}", ctx)

the single-expression equivalent already works, because it takes the quote-aware path:

# returns "}}" fine
evaluate_expression("{{ inputs.missing | default('}}') }}", ctx)

impact

default('}}'), default('{{'), contains('}}') etc. inside a template that also has literal text or another expression around it will hard-error at evaluation. it's a narrow trigger (you need the brace literal inside a filter arg and more than one block), but it's a crash rather than a wrong value, and the fix for the sibling path is already in the tree.

proposed fix

replace the _EXPR_PATTERN.sub interpolation with a quote-aware scan that finds each block's closing }} outside string literals, mirroring the exact logic _is_single_expression already uses. a literal }} in a plain resolved value (not an expression) stays untouched.

happy to open a PR (have one ready with regression tests).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions