This is an automated email from the ASF dual-hosted git repository. garydgregory pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/commons-lang.git
commit 4b1d8cb83c263ce7446fe573e7150042722a6044 Author: Gary Gregory <[email protected]> AuthorDate: Sat Oct 24 13:29:36 2026 -0400 Keep StrSubstitutor from treating a value's trailing $ as an escape (#1834) Tests and Javadoc --- .../apache/commons/lang3/text/StrSubstitutor.java | 6 ++ .../commons/lang3/text/StrSubstitutorTest.java | 74 ++++++++++++++++++---- 2 files changed, 69 insertions(+), 11 deletions(-) diff --git a/src/main/java/org/apache/commons/lang3/text/StrSubstitutor.java b/src/main/java/org/apache/commons/lang3/text/StrSubstitutor.java index 0d87394ab..0b1ed285f 100644 --- a/src/main/java/org/apache/commons/lang3/text/StrSubstitutor.java +++ b/src/main/java/org/apache/commons/lang3/text/StrSubstitutor.java @@ -124,6 +124,12 @@ * The variable $${${name}} must be used. * </pre> * <p> + * 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. + * </p> + * <p> * In some complex scenarios you might even want to perform substitution in the * names of variables, for instance: * </p> diff --git a/src/test/java/org/apache/commons/lang3/text/StrSubstitutorTest.java b/src/test/java/org/apache/commons/lang3/text/StrSubstitutorTest.java index 2b9c70de0..147e6db9c 100644 --- a/src/test/java/org/apache/commons/lang3/text/StrSubstitutorTest.java +++ b/src/test/java/org/apache/commons/lang3/text/StrSubstitutorTest.java @@ -440,17 +440,6 @@ void testReplaceEscaping() { doTestReplace("The ${animal} jumps over the lazy dog.", "The $${animal} jumps over the ${target}.", true); } - /** - * Tests that an escape character at the end of a substituted value does not escape the next variable. - */ - @Test - void testReplaceValueEndingWithEscape() { - values.put("price", "5$"); - doTestReplace("The price is 5$lazy dog.", "The price is ${price}${target}.", true); - values.put("animal", "$"); - doTestReplace("The $lazy dog jumps over the $${target}.", "The ${animal}${target} jumps over the ${animal}$${target}.", true); - } - /** * Tests when no incomplete prefix. */ @@ -636,6 +625,69 @@ void testReplaceUnknownKey() { doTestReplace("The ${person} jumps over the lazy dog. 1234567890.", "The ${person} jumps over the ${target}. ${undefined.number:-1234567890}.", true); } + /** + * Tests that an escape character at the end of a substituted value does not escape the next variable. + */ + @Test + void testReplaceValueEndingWithEscape() { + values.put("price", "5$"); + doTestReplace("The price is 5$lazy dog.", "The price is ${price}${target}.", true); + values.put("animal", "$"); + doTestReplace("The $lazy dog jumps over the $${target}.", "The ${animal}${target} jumps over the ${animal}$${target}.", true); + } + + @Test + void testReplaceValueEndingWithEscapeCustomDelimiters() { + values.put("price", "5!"); + final StrSubstitutor sub = new StrSubstitutor(values, "<", ">", '!'); + assertEquals("5!lazy dog", sub.replace("<price><target>")); + assertEquals("5!<target>", sub.replace("<price>!<target>")); + } + + @Test + void testReplaceValueEndingWithEscapeDefaultValue() { + doTestReplace("The price is 5$lazy dog.", "The price is ${price:-5$}${target}.", true); + } + + @Test + void testReplaceValueEndingWithEscapeEmptyValue() { + values.put("price", "5$"); + values.put("empty", ""); + doTestReplace("The price is 5$lazy dog.", "The price is ${price}${empty}${target}.", true); + } + + @Test + void testReplaceValueEndingWithEscapeInVariableName() { + values.put("prefix", "animal$"); + values.put("suffix", "name"); + values.put("animal$name", "fox"); + final StrSubstitutor sub = new StrSubstitutor(values); + sub.setEnableSubstitutionInVariables(true); + assertEquals("fox", sub.replace("${${prefix}${suffix}}")); + } + + @Test + void testReplaceValueEndingWithEscapePreserved() { + values.put("price", "5$"); + values.put("escaped", "$${target}"); + final StrSubstitutor sub = new StrSubstitutor(values); + sub.setPreserveEscapes(true); + assertEquals("5$lazy dog", sub.replace("${price}${target}")); + assertEquals("5$$${target}", sub.replace("${price}$${target}")); + assertEquals("$${target}lazy dog", sub.replace("${escaped}${target}")); + } + + @Test + void testReplaceValueEndingWithEscapeRecursive() { + values.put("price", "5$"); + values.put("recursive", "${price}"); + values.put("inside", "${price}${target}"); + values.put("escaped", "$${target}"); + doTestReplace("The price is 5$lazy dog.", "The price is ${recursive}${target}.", true); + doTestReplace("The price is 5$lazy dog.", "The price is ${inside}.", true); + doTestReplace("The ${target}lazy dog.", "The ${escaped}${target}.", true); + } + /** * Tests interpolation with weird boundary patterns. */
