Improve templating filter chaining
Some checks are pending
CI / auto-merge (push) Waiting to run
CI / test (push) Waiting to run
CI / auto-fix (push) Waiting to run

This commit is contained in:
Virgil 2026-04-03 11:03:04 +00:00
parent c52d539d3c
commit c65ca1cfd9
2 changed files with 90 additions and 5 deletions

View file

@ -2959,11 +2959,23 @@ func (e *Executor) templateString(s string, host string, task *Task) string {
// resolveExpr resolves a template expression.
func (e *Executor) resolveExpr(expr string, host string, task *Task) string {
// Handle filters
if contains(expr, " | ") {
parts := splitN(expr, " | ", 2)
value := e.resolveExpr(parts[0], host, task)
return e.applyFilter(value, parts[1])
parts := splitTemplatePipeline(expr)
if len(parts) == 0 {
return ""
}
value := e.resolveExprBase(parts[0], host, task)
for _, filter := range parts[1:] {
value = e.applyFilter(value, filter)
}
return value
}
// resolveExprBase resolves a single templating expression without applying filters.
func (e *Executor) resolveExprBase(expr string, host string, task *Task) string {
expr = corexTrimSpace(expr)
if expr == "" {
return ""
}
// Handle lookups
@ -3062,6 +3074,70 @@ func (e *Executor) resolveExpr(expr string, host string, task *Task) string {
return "{{ " + expr + " }}" // Return as-is if unresolved
}
// splitTemplatePipeline splits a template expression into a base expression
// and any chained filters, preserving quoted or parenthesised filter arguments.
func splitTemplatePipeline(expr string) []string {
expr = corexTrimSpace(expr)
if expr == "" {
return nil
}
parts := make([]string, 0, 4)
var (
current strings.Builder
depth int
inSingle bool
inDouble bool
escaped bool
)
flush := func() {
part := corexTrimSpace(current.String())
if part != "" {
parts = append(parts, part)
}
current.Reset()
}
for i := 0; i < len(expr); i++ {
ch := expr[i]
switch {
case escaped:
current.WriteByte(ch)
escaped = false
case ch == '\\' && (inSingle || inDouble):
current.WriteByte(ch)
escaped = true
case ch == '\'' && !inDouble:
current.WriteByte(ch)
inSingle = !inSingle
case ch == '"' && !inSingle:
current.WriteByte(ch)
inDouble = !inDouble
case inSingle || inDouble:
current.WriteByte(ch)
case ch == '(':
depth++
current.WriteByte(ch)
case ch == ')':
if depth > 0 {
depth--
}
current.WriteByte(ch)
case ch == '|' && depth == 0:
flush()
default:
current.WriteByte(ch)
}
}
flush()
if len(parts) == 0 {
return nil
}
return parts
}
// buildEnvironmentPrefix renders merged play/task environment variables as
// shell export statements.
func (e *Executor) buildEnvironmentPrefix(host string, task *Task, play *Play) string {

View file

@ -2478,6 +2478,15 @@ func TestExecutor_ApplyFilter_Good_RegexReplace(t *testing.T) {
assert.Equal(t, "123", e.applyFilter("abc123", `regex_replace("\D+", "")`))
}
func TestExecutor_TemplateString_Good_ChainedFilters(t *testing.T) {
e := NewExecutor("/tmp")
e.vars["padded"] = " web01 "
result := e.templateString("{{ missing_var | default('fallback') | trim }} {{ padded | trim }}", "", nil)
assert.Equal(t, "fallback web01", result)
}
// --- resolveLoop ---
func TestExecutor_ResolveLoop_Good_SliceAny(t *testing.T) {