From b3f2cc3fc6786d84a625ab4b468f8bfa9a0abeca Mon Sep 17 00:00:00 2001 From: Virgil Date: Thu, 2 Apr 2026 01:51:39 +0000 Subject: [PATCH] feat(ansible): add regex_replace filter support Co-Authored-By: Virgil --- executor.go | 73 +++++++++++++++++++++++++++++++++++++++++++ executor_test.go | 7 +++++ modules_infra_test.go | 6 ++++ 3 files changed, 86 insertions(+) diff --git a/executor.go b/executor.go index 2591b57..d82f83b 100644 --- a/executor.go +++ b/executor.go @@ -2654,6 +2654,20 @@ func (e *Executor) applyFilter(value, filter string) string { return corexTrimSpace(value) } + // Handle regex_replace + if corexHasPrefix(filter, "regex_replace(") { + pattern, replacement, ok := parseRegexReplaceFilter(filter) + if !ok { + return value + } + + compiled, err := regexp.Compile(pattern) + if err != nil { + return value + } + return compiled.ReplaceAllString(value, replacement) + } + // Handle b64decode if filter == "b64decode" { decoded, err := base64.StdEncoding.DecodeString(value) @@ -2669,6 +2683,65 @@ func (e *Executor) applyFilter(value, filter string) string { return value } +func parseRegexReplaceFilter(filter string) (string, string, bool) { + if !corexHasPrefix(filter, "regex_replace(") || !corexHasSuffix(filter, ")") { + return "", "", false + } + + args := strings.TrimSpace(filter[len("regex_replace(") : len(filter)-1]) + parts := splitFilterArgs(args) + if len(parts) < 2 { + return "", "", false + } + + return trimCutset(parts[0], "'\""), trimCutset(parts[1], "'\""), true +} + +func splitFilterArgs(args string) []string { + if args == "" { + return nil + } + + var ( + parts []string + current strings.Builder + inSingle bool + inDouble bool + escaped bool + ) + + for _, r := range args { + switch { + case escaped: + current.WriteRune(r) + escaped = false + case r == '\\' && (inSingle || inDouble): + current.WriteRune(r) + escaped = true + case r == '\'' && !inDouble: + current.WriteRune(r) + inSingle = !inSingle + case r == '"' && !inSingle: + current.WriteRune(r) + inDouble = !inDouble + case r == ',' && !inSingle && !inDouble: + part := strings.TrimSpace(current.String()) + if part != "" { + parts = append(parts, part) + } + current.Reset() + default: + current.WriteRune(r) + } + } + + if tail := strings.TrimSpace(current.String()); tail != "" { + parts = append(parts, tail) + } + + return parts +} + func isUnresolvedTemplateValue(value string) bool { return corexHasPrefix(value, "{{ ") && corexHasSuffix(value, " }}") } diff --git a/executor_test.go b/executor_test.go index 124fe03..1b97e27 100644 --- a/executor_test.go +++ b/executor_test.go @@ -1707,6 +1707,13 @@ func TestExecutor_ApplyFilter_Good_Trim(t *testing.T) { assert.Equal(t, "hello", e.applyFilter(" hello ", "trim")) } +func TestExecutor_ApplyFilter_Good_RegexReplace(t *testing.T) { + e := NewExecutor("/tmp") + + assert.Equal(t, "web-01", e.applyFilter("web_01", "regex_replace('_', '-')")) + assert.Equal(t, "123", e.applyFilter("abc123", `regex_replace("\D+", "")`)) +} + // --- resolveLoop --- func TestExecutor_ResolveLoop_Good_SliceAny(t *testing.T) { diff --git a/modules_infra_test.go b/modules_infra_test.go index 6b40e5d..f574f91 100644 --- a/modules_infra_test.go +++ b/modules_infra_test.go @@ -499,6 +499,12 @@ func TestModulesInfra_ApplyFilter_Good_TrimFilter(t *testing.T) { assert.Equal(t, "", e.applyFilter(" ", "trim")) } +func TestModulesInfra_ApplyFilter_Good_RegexReplaceFilter(t *testing.T) { + e := NewExecutor("/tmp") + assert.Equal(t, "app-01", e.applyFilter("app_01", "regex_replace('_', '-')")) + assert.Equal(t, "42", e.applyFilter("v42", `regex_replace("^v", "")`)) +} + func TestModulesInfra_ApplyFilter_Good_B64Decode(t *testing.T) { e := NewExecutor("/tmp") assert.Equal(t, "test", e.applyFilter("dGVzdA==", "b64decode"))