diff --git a/src/libs/utils/stringutils.cpp b/src/libs/utils/stringutils.cpp
index aa7a45bb73ec5513e8e6b7eb8c53bbdb5ca64a07..274a8bfb7c7e38e7c9575b74373b5669552ccd8e 100644
--- a/src/libs/utils/stringutils.cpp
+++ b/src/libs/utils/stringutils.cpp
@@ -30,6 +30,7 @@
 #include <utils/algorithm.h>
 
 #include <QDir>
+#include <QRegularExpression>
 
 #include <limits.h>
 
@@ -118,18 +119,34 @@ QTCREATOR_UTILS_EXPORT QString withTildeHomePath(const QString &path)
     return outPath;
 }
 
+static bool validateVarName(const QString &varName)
+{
+    return !varName.startsWith("JS:");
+}
+
 bool AbstractMacroExpander::expandNestedMacros(const QString &str, int *pos, QString *ret)
 {
     QString varName;
+    QString pattern, replace;
+    QString *currArg = &varName;
     QChar prev;
     QChar c;
+    bool replaceAll = false;
 
     int i = *pos;
     int strLen = str.length();
     varName.reserve(strLen - i);
     for (; i < strLen; prev = c) {
         c = str.at(i++);
-        if (c == '}') {
+        if (c == '\\' && i < strLen && validateVarName(varName)) {
+            c = str.at(i++);
+            // For the replacement, do not skip the escape sequence when followed by a digit.
+            // This is needed for enabling convenient capture group replacement,
+            // like %{var/(.)(.)/\2\1}, without escaping the placeholders.
+            if (currArg == &replace && c.isDigit())
+                *currArg += '\\';
+            *currArg += c;
+        } else if (c == '}') {
             if (varName.isEmpty()) { // replace "%{}" with "%"
                 *ret = QString('%');
                 *pos = i;
@@ -137,6 +154,22 @@ bool AbstractMacroExpander::expandNestedMacros(const QString &str, int *pos, QSt
             }
             if (resolveMacro(varName, ret)) {
                 *pos = i;
+                if (!pattern.isEmpty() && currArg == &replace) {
+                    const QRegularExpression regexp(pattern);
+                    if (regexp.isValid()) {
+                        if (replaceAll) {
+                            ret->replace(regexp, replace);
+                        } else {
+                            // There isn't an API for replacing once...
+                            const QRegularExpressionMatch match = regexp.match(*ret);
+                            if (match.hasMatch()) {
+                                *ret = ret->left(match.capturedStart(0))
+                                        + match.captured(0).replace(regexp, replace)
+                                        + ret->mid(match.capturedEnd(0));
+                            }
+                        }
+                    }
+                }
                 return true;
             }
             return false;
@@ -145,8 +178,16 @@ bool AbstractMacroExpander::expandNestedMacros(const QString &str, int *pos, QSt
                 return false;
             varName.chop(1);
             varName += ret;
+        } else if (currArg == &varName && c == '/' && validateVarName(varName)) {
+            currArg = &pattern;
+            if (i < strLen && str.at(i) == '/') {
+                ++i;
+                replaceAll = true;
+            }
+        } else if (currArg == &pattern && c == '/') {
+            currArg = &replace;
         } else {
-            varName += c;
+            *currArg += c;
         }
     }
     return false;
diff --git a/tests/auto/utils/stringutils/tst_stringutils.cpp b/tests/auto/utils/stringutils/tst_stringutils.cpp
index 314bf1391dbe02c0e68770da1ee6a7b69c8c39d4..972d0b35d5b0e4a83399688781b3cfd3b623bc89 100644
--- a/tests/auto/utils/stringutils/tst_stringutils.cpp
+++ b/tests/auto/utils/stringutils/tst_stringutils.cpp
@@ -50,6 +50,18 @@ public:
             *ret = QLatin1String("bar");
             return true;
         }
+        if (name == "slash") {
+            *ret = "foo/bar";
+            return true;
+        }
+        if (name == "sl/sh") {
+            *ret = "slash";
+            return true;
+        }
+        if (name == "JS:foo") {
+            *ret = "bar";
+            return true;
+        }
         return false;
     }
 };
@@ -129,6 +141,18 @@ void tst_StringUtils::testMacroExpander_data()
         { "%{%{a}}}post", "ho}post" },
         { "%{hi%{a}}", "bar" },
         { "%{hi%{%{foo}}}", "bar" },
+        { "%{hihi/b/c}", "car" },
+        { "%{hihi/a/}", "br" }, // empty replacement
+        { "%{hihi/b}", "bar" }, // incomplete substitution
+        { "%{hihi/./c}", "car" },
+        { "%{hihi//./c}", "ccc" },
+        { "%{hihi/(.)(.)r/\\2\\1c}", "abc" }, // no escape for capture groups
+        { "%{hihi/b/c/d}", "c/dar" },
+        { "%{hihi/a/e{\\}e}", "be{}er" }, // escape closing brace
+        { "%{slash/o\\/b/ol's c}", "fool's car" },
+        { "%{sl\\/sh/(.)(a)(.)/\\2\\1\\3as}", "salsash" }, // escape in variable name
+        { "%{JS:foo/b/c}", "%{JS:foo/b/c}" }, // No replacement for JS (all considered varName)
+        { "%{%{a}%{a}/b/c}", "car" },
     };
 
     for (unsigned i = 0; i < sizeof(vals)/sizeof(vals[0]); i++)