diff --git a/src/main/java/org/apache/commons/text/StringSubstitutor.java b/src/main/java/org/apache/commons/text/StringSubstitutor.java index 9c24a6deba..4053488158 100644 --- a/src/main/java/org/apache/commons/text/StringSubstitutor.java +++ b/src/main/java/org/apache/commons/text/StringSubstitutor.java @@ -198,6 +198,12 @@ * "The variable $${${name}} must be used." * *
+ * An escape character at the end of a substituted value does not escape a following variable reference in the containing text. + * For example, if {@code price} is {@code 5$} and {@code item} is {@code X}, then {@code ${price}${item}} becomes {@code 5$X}. + * This also applies to default values and recursive substitutions. Escape characters before variable references within a value + * are processed when that value is recursively substituted. + *
+ ** In some complex scenarios you might even want to perform substitution in the names of variables, for instance *
* @@ -1436,13 +1442,15 @@ private Result substitute(final TextStringBuilder builder, final int offset, fin int bufEnd = offset + length; int pos = offset; int escPos = -1; + // an escape character must come from the text being scanned, not from the end of a substituted value + int escapeStart = offset; outer: while (pos < bufEnd) { final int startMatchLen = prefixMatcher.isMatch(builder, pos, offset, bufEnd); if (startMatchLen == 0) { pos++; } else { // found variable start marker - if (pos > offset && builder.charAt(pos - 1) == escapeCh) { + if (pos > escapeStart && builder.charAt(pos - 1) == escapeCh) { // escape detected if (preserveEscapes) { // keep escape @@ -1536,6 +1544,7 @@ private Result substitute(final TextStringBuilder builder, final int offset, fin } change = change + varLen - (endPos - startPos); pos += change; + escapeStart = pos; bufEnd += change; lengthChange += change; } else if (undefinedVariableException) { diff --git a/src/test/java/org/apache/commons/text/StringSubstitutorTest.java b/src/test/java/org/apache/commons/text/StringSubstitutorTest.java index a6fdf45297..de5235974f 100644 --- a/src/test/java/org/apache/commons/text/StringSubstitutorTest.java +++ b/src/test/java/org/apache/commons/text/StringSubstitutorTest.java @@ -834,6 +834,69 @@ void testReplaceUnknownShortestKeyOnlyExtraLast() throws IOException { assertEqualsCharSeq(expected, replace(new StringSubstitutor(values), expected)); } + /** + * Tests that an escape character at the end of a substituted value does not escape the next variable. + */ + @Test + void testReplaceValueEndingWithEscape() throws IOException { + values.put("price", "5$"); + doReplace("The price is 5$lazy dog.", "The price is ${price}${target}.", true); + values.put("animal", "$"); + doReplace("The $lazy dog jumps over the $${target}.", "The ${animal}${target} jumps over the ${animal}$${target}.", true); + } + + @Test + void testReplaceValueEndingWithEscapeCustomDelimiters() throws IOException { + values.put("price", "5!"); + final StringSubstitutor sub = new StringSubstitutor(values, "<", ">", '!'); + assertEquals("5!lazy dog", replace(sub, "