From ead5e5b85e612596d45d06114bcb5110e4573799 Mon Sep 17 00:00:00 2001 From: "Franklin \"Snaipe\" Mathieu" Date: Fri, 19 Sep 2025 13:49:43 +0200 Subject: [PATCH] subst: allow implementor to pick which variables it substitutes By implementing `CanSubstitute(key string) bool`, a VariableMap can safely choose to only substitute some variables and leave the others alone. --- subst.go | 20 ++++++++++++++++++++ subst_test.go | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+) diff --git a/subst.go b/subst.go index 1a186a9..7c2253a 100644 --- a/subst.go +++ b/subst.go @@ -47,7 +47,22 @@ var reGroup = regexp.MustCompile(`\\([0-9]+)`) // - ${variable/re/subst/} expands to the variable, with a regexp replacement. // for instance, ${variable/^([^:]*):/\1/}, where variable=foo:bar, expands // to foo. +// +// If the passed VariableMap implements CanSubstitute(key string) bool, then +// the method is called to determine whether the variable is to be substituted. +// If the method returns false, the variable is left untouched and is output +// as-is into the result. func Substitute(s string, vars VariableMap) (string, error) { + + type CanSubstitute interface { + CanSubstitute(key string) bool + } + + cansubst := func(key string) bool { return true } + if cs, ok := vars.(CanSubstitute); ok { + cansubst = cs.CanSubstitute + } + var out strings.Builder start := 0 outer: @@ -104,6 +119,11 @@ outer: break outer } + if !cansubst(name) { + i += delim + 1 + continue + } + out.WriteString(s[start:subsStart]) value, present := vars.Get(name) diff --git a/subst_test.go b/subst_test.go index f10e964..ec78bc4 100644 --- a/subst_test.go +++ b/subst_test.go @@ -6,9 +6,27 @@ package shutil import ( + "strings" "testing" ) +type PrefixedVarMap struct { + Prefix string + Values map[string]string +} + +func (m *PrefixedVarMap) Get(key string) (value string, present bool) { + if !strings.HasPrefix(key, m.Prefix) { + return "", false + } + value, present = m.Values[key[len(m.Prefix):]] + return +} + +func (m *PrefixedVarMap) CanSubstitute(key string) bool { + return strings.HasPrefix(key, m.Prefix) +} + func TestSubstitute(t *testing.T) { t.Run("Simple", func(t *testing.T) { @@ -69,4 +87,33 @@ func TestSubstitute(t *testing.T) { } }) + t.Run("Partial", func(t *testing.T) { + + tcases := []struct { + In, Expected string + Error bool + }{ + {`${substitute.variable}`, "value", false}, + {`${substitute.undefined}`, "", true}, + {`${nosubstitute.variable}`, `${nosubstitute.variable}`, false}, + } + + vals := &PrefixedVarMap{ + Prefix: "substitute.", + Values: map[string]string{"variable": "value"}, + } + + for _, tc := range tcases { + t.Run(tc.In, func(t *testing.T) { + actual, err := Substitute(tc.In, vals) + if tc.Error != (err != nil) { + t.Fatalf("unexpected error or success: %v", err) + } + if actual != tc.Expected { + t.Fatalf("expected %q, got %q", tc.Expected, actual) + } + }) + } + }) + }