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.
      */

Reply via email to