forked from qt-creator/qt-creator
Utils: Support pattern substitution in macro expansion
Syntax is similar to bash substitution:
%{variable/pattern/replacement} takes the expansion of variable, and
replaces the regular expression pattern with replacement once.
%{variable//pattern/replacement} replaces all occurrences.
Capture groups are supported for both forms.
Change-Id: I67ff91e2dad4dd8be81573ea446cd1093a05ccf6
Reviewed-by: Oswald Buddenhagen <oswald.buddenhagen@theqtcompany.com>
Reviewed-by: hjk <hjk@qt.io>
This commit is contained in:
committed by
Orgad Shaneh
parent
7e1b4ccebb
commit
2ec82a2a93
@@ -30,6 +30,7 @@
|
|||||||
#include <utils/algorithm.h>
|
#include <utils/algorithm.h>
|
||||||
|
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
|
#include <QRegularExpression>
|
||||||
|
|
||||||
#include <limits.h>
|
#include <limits.h>
|
||||||
|
|
||||||
@@ -118,18 +119,34 @@ QTCREATOR_UTILS_EXPORT QString withTildeHomePath(const QString &path)
|
|||||||
return outPath;
|
return outPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool validateVarName(const QString &varName)
|
||||||
|
{
|
||||||
|
return !varName.startsWith("JS:");
|
||||||
|
}
|
||||||
|
|
||||||
bool AbstractMacroExpander::expandNestedMacros(const QString &str, int *pos, QString *ret)
|
bool AbstractMacroExpander::expandNestedMacros(const QString &str, int *pos, QString *ret)
|
||||||
{
|
{
|
||||||
QString varName;
|
QString varName;
|
||||||
|
QString pattern, replace;
|
||||||
|
QString *currArg = &varName;
|
||||||
QChar prev;
|
QChar prev;
|
||||||
QChar c;
|
QChar c;
|
||||||
|
bool replaceAll = false;
|
||||||
|
|
||||||
int i = *pos;
|
int i = *pos;
|
||||||
int strLen = str.length();
|
int strLen = str.length();
|
||||||
varName.reserve(strLen - i);
|
varName.reserve(strLen - i);
|
||||||
for (; i < strLen; prev = c) {
|
for (; i < strLen; prev = c) {
|
||||||
c = str.at(i++);
|
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 "%"
|
if (varName.isEmpty()) { // replace "%{}" with "%"
|
||||||
*ret = QString('%');
|
*ret = QString('%');
|
||||||
*pos = i;
|
*pos = i;
|
||||||
@@ -137,6 +154,22 @@ bool AbstractMacroExpander::expandNestedMacros(const QString &str, int *pos, QSt
|
|||||||
}
|
}
|
||||||
if (resolveMacro(varName, ret)) {
|
if (resolveMacro(varName, ret)) {
|
||||||
*pos = i;
|
*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 true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@@ -145,8 +178,16 @@ bool AbstractMacroExpander::expandNestedMacros(const QString &str, int *pos, QSt
|
|||||||
return false;
|
return false;
|
||||||
varName.chop(1);
|
varName.chop(1);
|
||||||
varName += ret;
|
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 {
|
} else {
|
||||||
varName += c;
|
*currArg += c;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -50,6 +50,18 @@ public:
|
|||||||
*ret = QLatin1String("bar");
|
*ret = QLatin1String("bar");
|
||||||
return true;
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -129,6 +141,18 @@ void tst_StringUtils::testMacroExpander_data()
|
|||||||
{ "%{%{a}}}post", "ho}post" },
|
{ "%{%{a}}}post", "ho}post" },
|
||||||
{ "%{hi%{a}}", "bar" },
|
{ "%{hi%{a}}", "bar" },
|
||||||
{ "%{hi%{%{foo}}}", "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++)
|
for (unsigned i = 0; i < sizeof(vals)/sizeof(vals[0]); i++)
|
||||||
|
|||||||
Reference in New Issue
Block a user