diff --git a/pkg/substitution/substitution.go b/pkg/substitution/substitution.go index 30d027074a1..1676bd2f075 100644 --- a/pkg/substitution/substitution.go +++ b/pkg/substitution/substitution.go @@ -118,10 +118,14 @@ func matchGroups(matches []string, pattern *regexp.Regexp) map[string]string { } func ApplyReplacements(in string, replacements map[string]string) string { + replacementsList := []string{} for k, v := range replacements { - in = strings.Replace(in, fmt.Sprintf("$(%s)", k), v, -1) + replacementsList = append(replacementsList, fmt.Sprintf("$(%s)", k), v) } - return in + // strings.Replacer does all replacements in one pass, preventing multiple replacements + // See #2093 for an explanation on why we need to do this. + replacer := strings.NewReplacer(replacementsList...) + return replacer.Replace(in) } // Take an input string, and output an array of strings related to possible arrayReplacements. If there aren't any diff --git a/pkg/substitution/substitution_test.go b/pkg/substitution/substitution_test.go index c218fda0612..9e5f11d53b5 100644 --- a/pkg/substitution/substitution_test.go +++ b/pkg/substitution/substitution_test.go @@ -167,6 +167,24 @@ func TestApplyReplacements(t *testing.T) { } } +func TestNestedReplacements(t *testing.T) { + replacements := map[string]string{ + // Foo should turn into barbar, which could then expand into bazbaz depending on how this is expanded + "foo": "$(bar)$(bar)", + "bar": "baz", + } + input := "$(foo) is cool" + expected := "$(bar)$(bar) is cool" + + // Run this test a lot of times to ensure the behavior is deterministic + for i := 0; i <= 1000; i++ { + got := substitution.ApplyReplacements(input, replacements) + if d := cmp.Diff(expected, got); d != "" { + t.Errorf("ApplyReplacements() output did not match expected value %s", diff.PrintWantGot(d)) + } + } +} + func TestApplyArrayReplacements(t *testing.T) { type args struct { input string