Skip to content

Commit

Permalink
[conf] Add limited shell-style variable expansion
Browse files Browse the repository at this point in the history
Ported from the DNF 5 implementation here:
rpm-software-management/dnf5#800.

Adds support for ${variable:-word} and ${variable:+word}
POSIX-shell-like expansions of vars.

${variable:-word} means if `variable` is unset or empty, the expansion
of `word` is substituted. Otherwise, the value of `variable` is
substituted.

${variable:+word} means if `variable` is unset or empty, nothing is
substituted. Otherwise, the expansion of `word` is substituted.

Zypper supports these expansions, see here:
https://doc.opensuse.org/projects/libzypp/HEAD/structzypp_1_1repo_1_1RepoVarExpand.html

For: https://bugzilla.redhat.com/show_bug.cgi?id=1789346
  • Loading branch information
evan-goode committed Sep 20, 2023
1 parent b004051 commit e0f72cd
Show file tree
Hide file tree
Showing 2 changed files with 175 additions and 26 deletions.
189 changes: 163 additions & 26 deletions libdnf/conf/ConfigParser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,40 +29,177 @@ namespace libdnf {
void ConfigParser::substitute(std::string & text,
const std::map<std::string, std::string> & substitutions)
{
auto start = text.find_first_of("$");
while (start != text.npos)
{
auto variable = start + 1;
if (variable >= text.length())
break;
bool bracket;
if (text[variable] == '{') {
bracket = true;
if (++variable >= text.length())
text = ConfigParser::substitute_expression(text, substitutions, 0).first;
}

const unsigned int MAXIMUM_EXPRESSION_DEPTH = 32;

std::pair<std::string, size_t> ConfigParser::substitute_expression(const std::string & text,
const std::map<std::string, std::string> & substitutions,
unsigned int depth) {
if (depth > MAXIMUM_EXPRESSION_DEPTH) {
return std::make_pair(std::string(text), text.length());
}
std::string res{text};

// The total number of characters read in the replacee
size_t total_scanned = 0;

size_t pos = 0;
while (pos < res.length()) {
if (res[pos] == '}' && depth > 0) {
return std::make_pair(res.substr(0, pos), total_scanned);
}

if (res[pos] == '\\') {
// Escape the next character (if there is one)
if (pos + 1 >= res.length()) {
break;
} else
bracket = false;
auto it = std::find_if_not(text.begin()+variable, text.end(),
[](char c){return std::isalnum(c) || c=='_';});
if (bracket && it == text.end())
break;
auto pastVariable = std::distance(text.begin(), it);
if (bracket && *it != '}') {
start = text.find_first_of("$", pastVariable);
}
res.erase(pos, 1);
total_scanned += 2;
pos += 1;
continue;
}
auto subst = substitutions.find(text.substr(variable, pastVariable - variable));
if (subst != substitutions.end()) {
if (bracket)
++pastVariable;
text.replace(start, pastVariable - start, subst->second);
start = text.find_first_of("$", start + subst->second.length());
if (res[pos] == '$') {
// variable expression starts after the $ and includes the braces
// ${variable:-word}
// ^-- pos_variable_expression
size_t pos_variable_expression = pos + 1;
if (pos_variable_expression >= res.length()) {
break;
}

// Does the variable expression use braces? If so, the variable name
// starts one character after the start of the variable_expression
bool has_braces;
size_t pos_variable;
if (res[pos_variable_expression] == '{') {
has_braces = true;
pos_variable = pos_variable_expression + 1;
if (pos_variable >= res.length()) {
break;
}
} else {
has_braces = false;
pos_variable = pos_variable_expression;
}

// Find the end of the variable name
auto it = std::find_if_not(res.begin() + static_cast<long>(pos_variable), res.end(), [](char c) {
return std::isalnum(c) != 0 || c == '_';
});
auto pos_after_variable = static_cast<size_t>(std::distance(res.begin(), it));

// Find the substituting string and the end of the variable expression
auto variable_mapping = substitutions.find(res.substr(pos_variable, pos_after_variable - pos_variable));
const std::string * subst_str = nullptr;

size_t pos_after_variable_expression;

if (has_braces) {
if (pos_after_variable >= res.length()) {
break;
}
if (res[pos_after_variable] == ':') {
if (pos_after_variable + 1 >= res.length()) {
break;
}
char expansion_mode = res[pos_after_variable + 1];
size_t pos_word = pos_after_variable + 2;
if (pos_word >= res.length()) {
break;
}

// Expand the default/alternate expression
auto word_str = res.substr(pos_word);
auto word_substitution = substitute_expression(word_str, substitutions, depth + 1);
auto expanded_word = word_substitution.first;
auto scanned = word_substitution.second;
auto pos_after_word = pos_word + scanned;
if (pos_after_word >= res.length()) {
break;
}
if (res[pos_after_word] != '}') {
// The variable expression doesn't end in a '}',
// continue after the word and don't expand it
total_scanned += pos_after_word - pos;
pos = pos_after_word;
continue;
}

if (expansion_mode == '-') {
// ${variable:-word} (default value)
// If variable is unset or empty, the expansion of word is
// substituted. Otherwise, the value of variable is
// substituted.
if (variable_mapping == substitutions.end() || variable_mapping->second.empty()) {
subst_str = &expanded_word;
} else {
subst_str = &variable_mapping->second;
}
} else if (expansion_mode == '+') {
// ${variable:+word} (alternate value)
// If variable is unset or empty nothing is substituted.
// Otherwise, the expansion of word is substituted.
if (variable_mapping == substitutions.end() || variable_mapping->second.empty()) {
const std::string empty{};
subst_str = &empty;
} else {
subst_str = &expanded_word;
}
} else {
// Unknown expansion mode, continue after the ':'
pos = pos_after_variable + 1;
continue;
}
pos_after_variable_expression = pos_after_word + 1;
} else if (res[pos_after_variable] == '}') {
// ${variable}
if (variable_mapping != substitutions.end()) {
subst_str = &variable_mapping->second;
}
// Move past the closing '}'
pos_after_variable_expression = pos_after_variable + 1;
} else {
// Variable expression doesn't end in a '}', continue after the variable
pos = pos_after_variable;
continue;
}
} else {
// No braces, we have a $variable
if (variable_mapping != substitutions.end()) {
subst_str = &variable_mapping->second;
}
pos_after_variable_expression = pos_after_variable;
}

// If there is no substitution to make, move past the variable expression and continue.
if (subst_str == nullptr) {
total_scanned += pos_after_variable_expression - pos;
pos = pos_after_variable_expression;
continue;
}

res.replace(pos, pos_after_variable_expression - pos, *subst_str);
total_scanned += pos_after_variable_expression - pos;
pos += subst_str->length();
} else {
start = text.find_first_of("$", pastVariable);
total_scanned += 1;
pos += 1;
}
}

// We have reached the end of the text
if (depth > 0) {
// If we are in a subexpression and we didn't find a closing '}', make no substitutions.
return std::make_pair(std::string{text}, text.length());
}

return std::make_pair(res, text.length());
}


static void read(ConfigParser & cfgParser, IniParser & parser)
{
IniParser::ItemType readedType;
Expand Down
12 changes: 12 additions & 0 deletions libdnf/conf/ConfigParser.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,18 @@ struct ConfigParser {
int itemNumber{0};
std::string header;
std::map<std::string, std::string> rawItems;

/**
* @brief Expand variables in a subexpression
*
* @param text String with variable expressions
* @param substitutions Substitution map
* @param depth The recursive depth
* @return Pair of the resulting string and the number of characters scanned in `text`
*/
static std::pair<std::string, size_t> substitute_expression(const std::string & text,
const std::map<std::string, std::string> & substitutions,
unsigned int depth);
};

inline void ConfigParser::setSubstitutions(const std::map<std::string, std::string> & substitutions)
Expand Down

0 comments on commit e0f72cd

Please sign in to comment.