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

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions