Files
openfoam/src/OpenFOAM/primitives/strings/stringOps/stringOps.C
Mark Olesen 6b5492e3bd ENH: code simplification, improvements for reading dictionary variables
- Now accept '/' when reading variables without requiring
  a surrounding '{}'

- fix some degenerate parsing cases when the first character is
  already bad.

  Eg, $"abc" would have previously parsed as a <$"> variable, even
  although a double quote is not a valid variable character.

  Now emits a warning and parses as a '$' token and a string token.
2019-10-08 18:43:38 +02:00

1283 lines
29 KiB
C

/*---------------------------------------------------------------------------*\
========= |
\\ / F ield | OpenFOAM: The Open Source CFD Toolbox
\\ / O peration |
\\ / A nd | Copyright (C) 2017-2019 OpenCFD Ltd.
\\/ M anipulation |
-------------------------------------------------------------------------------
| Copyright (C) 2011-2016 OpenFOAM Foundation
-------------------------------------------------------------------------------
License
This file is part of OpenFOAM.
OpenFOAM is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
OpenFOAM is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
for more details.
You should have received a copy of the GNU General Public License
along with OpenFOAM. If not, see <http://www.gnu.org/licenses/>.
\*---------------------------------------------------------------------------*/
#include "stringOps.H"
#include "typeInfo.H"
#include "etcFiles.H"
#include "Pstream.H"
#include "StringStream.H"
#include "OSstream.H"
#include "OSspecific.H"
#include <cctype>
// * * * * * * * * * * * * * * * Local Functions * * * * * * * * * * * * * * //
namespace Foam
{
// Return the file location mode (string) as a numerical value.
//
// - u : location mask 0700
// - g : location mask 0070
// - o : location mask 0007
// - a : location mask 0777
//
static inline unsigned short modeToLocation
(
const std::string& mode,
std::size_t pos = 0
)
{
unsigned short where(0);
if (std::string::npos != mode.find('u', pos)) { where |= 0700; } // User
if (std::string::npos != mode.find('g', pos)) { where |= 0070; } // Group
if (std::string::npos != mode.find('o', pos)) { where |= 0007; } // Other
if (std::string::npos != mode.find('a', pos)) { where |= 0777; } // All
return where;
}
// Expand a leading <tag>/
// Convenient for frequently used directories
//
// <etc>/ => user/group/other etc - findEtcEntry()
// <etc(:[ugoa]+)?>/ => user/group/other etc - findEtcEntry()
// <case>/ => FOAM_CASE directory
// <constant>/ => FOAM_CASE/constant directory
// <system>/ => FOAM_CASE/system directory
static void expandLeadingTag(std::string& s, const char b, const char e)
{
if (s[0] != b)
{
return;
}
auto delim = s.find(e);
if (std::string::npos == delim)
{
return; // Error: no closing delim - ignore expansion
}
fileName file;
const char nextC = s[++delim];
// Require the following character to be '/' or the end of string.
if (nextC)
{
if (nextC != '/')
{
return;
}
file.assign(s.substr(delim + 1));
}
const std::string tag(s, 1, delim-2);
const auto tagLen = tag.length();
// Note that file is also allowed to be an empty string.
if (tag == "etc")
{
s = findEtcEntry(file);
}
else if (tag == "case")
{
s = fileName(Foam::getEnv("FOAM_CASE"))/file;
}
else if (tag == "constant" || tag == "system")
{
s = fileName(Foam::getEnv("FOAM_CASE"))/tag/file;
}
else if (tagLen >= 4 && tag.compare(0, 4, "etc:") == 0)
{
// <etc:[ugoa]+> type of tag - convert "ugo" to numeric
s = findEtcEntry(file, modeToLocation(tag,4));
}
}
// Expand a leading tilde
// ~/ => home directory
// ~user => home directory for specified user
// Deprecated ~OpenFOAM => <etc> instead
static void expandLeadingTilde(std::string& s)
{
if (s[0] != '~')
{
return;
}
std::string user;
fileName file;
const auto slash = s.find('/');
if (slash == std::string::npos)
{
user = s.substr(1);
}
else
{
user = s.substr(1, slash - 1);
file = s.substr(slash + 1);
}
// NB: be a bit lazy and expand ~unknownUser as an
// empty string rather than leaving it untouched.
// otherwise add extra test
if (user == "OpenFOAM")
{
// Compat Warning
const int version(1806);
// Single warning (on master) with guard to avoid Pstream::master()
// when Pstream has not yet been initialized
if (Pstream::parRun() ? Pstream::master() : true)
{
std::cerr
<< nl
<< "--> FOAM Warning :" << nl
<< " Found [v" << version << "] '"
<< "~OpenFOAM" << "' string expansion instead of '"
<< "<etc>" << "' in string\n\"" << s << "\"\n" << nl
<< std::endl;
error::warnAboutAge("expansion", version);
}
s = findEtcFile(file);
}
else
{
s = home(user)/file;
}
}
// Expand leading contents: "./", "~..", "<tag>/"
static void expandLeading(std::string& s)
{
if (s.empty())
{
return;
}
switch (s[0])
{
case '.':
{
// Expand a lone '.' and an initial './' into cwd
if (s.size() == 1)
{
s = cwd();
}
else if (s[1] == '/')
{
s.std::string::replace(0, 1, cwd());
}
break;
}
case '<':
{
expandLeadingTag(s, '<', '>');
break;
}
case '~':
{
expandLeadingTilde(s);
break;
}
}
}
// Serialize an entry (primitive or dictionary) with special treatment
// for primitive entries that are already a string-type.
static inline std::string entryToString
(
const entry* eptr,
const bool allowSubDict
)
{
std::string str;
if (eptr)
{
OStringStream buf;
// Force floating point numbers to be printed with at least
// some decimal digits.
buf << fixed;
buf.precision(IOstream::defaultPrecision());
if (allowSubDict && eptr->isDict())
{
eptr->dict().write(buf, false);
str = buf.str();
}
else
{
// Fail for non-primitiveEntry
const primitiveEntry& pe =
dynamicCast<const primitiveEntry>(*eptr);
if (pe.size() == 1 && pe[0].isStringType())
{
// Already a string-type. Just copy.
str = pe[0].stringToken();
}
else
{
pe.write(buf, true);
str = buf.str();
}
}
}
return str;
}
} // End namespace Foam
// Details for handling dictionary expansion
namespace
{
// Acceptable values for $variable names.
//
// Similar to word::valid(), except we don't have the benefit of a parser
// to filter out other unacceptable entries for us.
//
// Does not currently accept '/' in a variable name.
// We would like "$file/$name" to expand as two variables.
static inline bool validVariableChar(char c)
{
return
(
std::isalnum(c)
|| c == '.'
|| c == ':'
|| c == '_'
);
}
// Find the type/position of the ":-" or ":+" alternative values
// Returns 0, '-', '+' corresponding to not-found or ':-' or ':+'
static inline int findParameterAlternative
(
const std::string& s,
std::string::size_type& pos,
std::string::size_type endPos
)
{
while (pos != std::string::npos)
{
pos = s.find(':', pos);
if (pos != std::string::npos)
{
if (pos < endPos)
{
// in-range: check for '+' or '-' following the ':'
const int altType = s[pos+1];
if (altType == '+' || altType == '-')
{
return altType;
}
++pos; // unknown/unsupported - continue at next position
}
else
{
// out-of-range: abort
pos = std::string::npos;
}
}
}
return 0;
}
// For input string of "$variable with other" return the length of
// the variable.
//
// Intentionally will not capture ':+', ':-' alterations. Use ${ .. } for that
static inline std::string::size_type findVariableLen
(
const std::string& s,
std::string::size_type pos,
const char sigil = '$'
)
{
std::string::size_type len = 0;
if (pos < s.length())
{
if (s[pos] == sigil)
{
// Skip leading '$' in the count!
++pos;
}
for
(
auto iter = s.cbegin() + pos;
iter != s.cend() && validVariableChar(*iter);
++iter
)
{
++len;
}
}
return len;
}
} // End namespace anonymous
namespace Foam
{
// Get dictionary or (optionally) environment variable
//
// Handles default and alternative values as per the POSIX shell.
// \code
// ${parameter:-defValue}
// ${parameter:+altValue}
// \endcode
static Foam::string getVariable
(
const word& name,
const dictionary* dictptr,
const bool allowEnv,
const bool allowEmpty,
const bool allowSubDict
)
{
// The type/position of the ":-" or ":+" alternative values
std::string::size_type altPos = 0;
// Check for parameter:-word or parameter:+word
const int altType =
findParameterAlternative(name, altPos, name.size()-1);
const word lookupName =
(altType ? word(name.substr(0,altPos), false) : name);
const entry* eptr =
(
(dictptr != nullptr)
? dictptr->findScoped(lookupName, keyType::LITERAL_RECURSIVE)
: nullptr
);
string value;
if (eptr)
{
value = entryToString(eptr, allowSubDict);
}
else if (allowEnv || dictptr == nullptr)
{
value = Foam::getEnv(lookupName);
}
if (value.empty() ? (altType == '-') : (altType == '+'))
{
// Not found or empty: use ":-" alternative value
// Found and not empty: use ":+" alternative value
value = name.substr(altPos + 2);
}
if (!allowEmpty && value.empty())
{
if (dictptr != nullptr)
{
auto& err =
FatalIOErrorInFunction(*dictptr)
<< "Cannot find dictionary entry ";
if (allowEnv)
{
err << "or environment ";
}
err << "variable '" << lookupName << "'" << nl
<< exit(FatalIOError);
}
else
{
FatalErrorInFunction
<< "Unknown variable '" << lookupName << "'" << nl
<< exit(FatalError);
}
}
return value;
}
// Recursively expands (dictionary or environment) variable
// starting at index in string. Updates index.
//
// String: "abc ${var} def",
// Receive: "var} def"
//
// String: "abc ${{expr}} def"
// Receive: "{expr}} def"
//
// On return, the index will be adjust to be AFTER the closing '}'
static Foam::string recursiveExpand
(
const std::string& s,
std::string::size_type& index,
const dictionary* dictptr,
const bool allowEnv,
const bool allowEmpty,
const bool allowSubDict
)
{
///Info<< "process:" << index << "=" << s.substr(index) << endl;
// Track ${{ expr }} expressions
const bool isExpr = (index < s.size() && s[index] == '{');
if (isExpr)
{
++index;
}
// Initially called for a ${variable}, not ${{expr}}
bool isVar = !isExpr;
string out;
for (/*nil*/; index < s.size(); ++index)
{
///Info<< "remaining:" << index << "=" << s.substr(index) << endl;
if (s[index] == '$')
{
if (s[index+1] == '{')
{
// Recurse to parse variable name
index += 2;
string val =
recursiveExpand
(
s,
index,
dictptr,
allowEnv,
allowEmpty,
allowSubDict
);
out.append(val); // Append content
///Info<< "got:" << val << nl << "now:" << out << endl;
// Already skipped past '}' terminator?
if (s[index-1] == '}')
{
--index;
}
}
else if (validVariableChar(s[index+1]))
{
// A regular $var expansion without a surrounding {}.
const auto varLen = findVariableLen(s, index);
const word varName(s.substr(index+1, varLen), false);
index += varLen;
string val =
getVariable
(
varName,
dictptr,
allowEnv,
allowEmpty,
allowSubDict
);
out.append(val); // Append content
}
else
{
// Perhaps received something like '$[ ]' ? - pass through
out += s[index]; // Append char
}
}
else if (s[index] == '}')
{
// Closing an expression or variable
if (isExpr)
{
// Closes with '}}'
++index; // Index past closing '}'
if (s[index] == '}')
{
++index; // Index past closing '}'
}
else if (dictptr != nullptr)
{
// Missing '}'? - Warn/error/ignore
FatalIOErrorInFunction(*dictptr)
<< "Expansion ${{ is missing a closing '}}'\n"
<< exit(FatalIOError);
}
else
{
FatalErrorInFunction
<< "Expansion ${{ is missing a closing '}}'\n"
<< exit(FatalIOError);
}
///Info<< "eval <" << out << ">" << endl;
// Even with allow empty, expressions need content
const scalar sval = stringOps::toScalar(out);
const word val(Foam::name(sval));
return val;
}
else if (isVar)
{
// Variable - closes with '}'
++index; // Index past closing '}'
return
getVariable
(
out,
dictptr,
allowEnv,
allowEmpty,
allowSubDict
);
}
else
{
// Stray '}'? - Leave on output
out += s[index]; // append char
}
}
else
{
out += s[index]; // append char
}
}
return out;
}
static void expandString
(
std::string& s,
const dictionary* dictptr,
const bool allowEnv,
const bool allowEmpty,
const bool allowSubDict,
const char sigil
)
{
std::string::size_type varBeg = 0;
// Expand $VAR, ${VAR} or ${{EXPR}}
// Repeat until nothing more is found
while
(
(varBeg = s.find(sigil, varBeg)) != std::string::npos
&& varBeg < s.size()-1
)
{
if (varBeg == 0 || s[varBeg-1] != '\\')
{
if (s[varBeg+1] == '{')
{
// Recursive variable expansion mode: '${' or '${{'
const auto replaceBeg = varBeg;
varBeg += 2;
string varValue
(
recursiveExpand
(
s,
varBeg,
dictptr,
allowEnv,
allowEmpty,
allowSubDict
)
);
s.std::string::replace
(
replaceBeg,
varBeg - replaceBeg,
varValue
);
varBeg = replaceBeg+varValue.size();
}
else
{
const auto varLen(findVariableLen(s, varBeg, sigil));
const word varName(s.substr(varBeg+1, varLen), false);
string varValue
(
getVariable
(
varName,
dictptr,
allowEnv,
allowEmpty,
allowSubDict
)
);
s.std::string::replace
(
varBeg,
varName.size()+1,
varValue
);
varBeg += varValue.size();
}
}
else
{
++varBeg;
}
}
expandLeading(s);
}
} // End namespace Foam
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
std::string::size_type Foam::stringOps::count
(
const std::string& str,
const char c
)
{
std::string::size_type n = 0;
for (auto iter = str.cbegin(); iter != str.cend(); ++iter)
{
if (*iter == c)
{
++n;
}
}
return n;
}
std::string::size_type Foam::stringOps::count(const char* str, const char c)
{
if (!str)
{
return 0;
}
std::string::size_type n = 0;
for (const char *iter = str; *iter; ++iter)
{
if (*iter == c)
{
++n;
}
}
return n;
}
Foam::string Foam::stringOps::expand
(
const std::string& original,
const HashTable<string, word, string::hash>& mapping,
const char sigil
)
{
string s(original);
inplaceExpand(s, mapping);
return s;
}
void Foam::stringOps::inplaceExpand
(
std::string& s,
const HashTable<string, word, string::hash>& mapping,
const char sigil
)
{
std::string::size_type varBeg = 0;
// Expand $VAR or ${VAR}
// Repeat until nothing more is found
while
(
(varBeg = s.find(sigil, varBeg)) != std::string::npos
&& varBeg < s.size()-1
)
{
if (varBeg == 0 || s[varBeg-1] != '\\')
{
// Find end of first occurrence
std::string::size_type varEnd = varBeg;
std::string::size_type delim = 0;
// The type/position of the ":-" or ":+" alternative values
int altType = 0;
auto altPos = std::string::npos;
if (s[varBeg+1] == '{')
{
varEnd = s.find('}', varBeg);
delim = 1;
// Check for ${parameter:-word} or ${parameter:+word}
if (varEnd != std::string::npos)
{
altPos = varBeg;
altType = findParameterAlternative(s, altPos, varEnd);
}
}
else
{
varEnd += findVariableLen(s, varBeg, sigil);
}
if (varEnd == std::string::npos)
{
// Likely parsed '${...' without closing '}' - abort
break;
}
else if (varEnd == varBeg)
{
// Parsed '${}' or $badChar - skip over
varBeg = varEnd + 1;
}
else
{
const word varName
(
s.substr
(
varBeg + 1 + delim,
(
(altPos == std::string::npos ? varEnd : altPos)
- varBeg - 2*delim
)
),
false
);
std::string altValue;
if (altPos != std::string::npos)
{
// Had ":-" or ":+" alternative value
altValue = s.substr
(
altPos + 2,
varEnd - altPos - 2*delim
);
}
const auto fnd = mapping.cfind(varName);
if (fnd.found() ? (altType == '+') : (altType == '-'))
{
// Found and ":+" alternative
// Not-found and ":-" alternative
s.std::string::replace
(
varBeg,
varEnd - varBeg + 1,
altValue
);
varBeg += altValue.size();
}
else if (fnd.found())
{
// Found: use value
s.std::string::replace
(
varBeg,
varEnd - varBeg + 1,
*fnd
);
varBeg += (*fnd).size();
}
else
{
// Not-found: empty value
s.std::string::erase(varBeg, varEnd - varBeg + 1);
}
}
}
else
{
++varBeg;
}
}
}
Foam::string Foam::stringOps::expand
(
const std::string& original,
const dictionary& dict,
const char sigil
)
{
string s(original);
inplaceExpand(s, dict, sigil);
return s;
}
void Foam::stringOps::inplaceExpand
(
std::string& s,
const dictionary& dict,
const bool allowEnv,
const bool allowEmpty,
const bool allowSubDict,
const char sigil
)
{
expandString(s, &dict, allowEnv, allowEmpty, allowSubDict, sigil);
}
void Foam::stringOps::inplaceExpand
(
std::string& s,
const dictionary& dict,
const char sigil
)
{
// Allow everything, including subDict expansions
// env=true, empty=true, subDict=true
expandString(s, &dict, true, true, true, sigil);
}
Foam::string Foam::stringOps::expand
(
const std::string& original,
const bool allowEmpty
)
{
string s(original);
inplaceExpand(s, allowEmpty);
return s;
}
void Foam::stringOps::inplaceExpand
(
std::string& s,
const bool allowEmpty
)
{
// Expand without a dictionary context
// allowEnv=true, allowSubDict=N/A
expandString(s, nullptr, true, allowEmpty, false, '$');
}
bool Foam::stringOps::inplaceReplaceVar(std::string& s, const word& varName)
{
if (s.empty() || varName.empty())
{
return false;
}
const string content(Foam::getEnv(varName));
if (content.empty())
{
return false;
}
const auto i = s.find(content);
if (i == std::string::npos)
{
return false;
}
s.replace(i, content.size(), string("${" + varName + "}"));
return true;
}
Foam::string Foam::stringOps::trimLeft(const std::string& s)
{
if (!s.empty())
{
std::string::size_type beg = 0;
while (beg < s.size() && std::isspace(s[beg]))
{
++beg;
}
if (beg)
{
return s.substr(beg);
}
}
return s;
}
void Foam::stringOps::inplaceTrimLeft(std::string& s)
{
if (!s.empty())
{
std::string::size_type beg = 0;
while (beg < s.size() && std::isspace(s[beg]))
{
++beg;
}
if (beg)
{
s.erase(0, beg);
}
}
}
Foam::string Foam::stringOps::trimRight(const std::string& s)
{
if (!s.empty())
{
auto n = s.size();
while (n && std::isspace(s[n-1]))
{
--n;
}
if (n < s.size())
{
return s.substr(0, n);
}
}
return s;
}
void Foam::stringOps::inplaceTrimRight(std::string& s)
{
if (!s.empty())
{
auto n = s.size();
while (n && std::isspace(s[n-1]))
{
--n;
}
s.resize(n);
}
}
Foam::string Foam::stringOps::trim(const std::string& original)
{
string s(original);
inplaceTrim(s);
return s;
}
void Foam::stringOps::inplaceTrim(std::string& s)
{
inplaceTrimRight(s);
inplaceTrimLeft(s);
}
Foam::string Foam::stringOps::removeComments(const std::string& original)
{
string s(original);
inplaceRemoveComments(s);
return s;
}
void Foam::stringOps::inplaceRemoveComments(std::string& s)
{
const auto len = s.length();
if (len < 2)
{
return;
}
std::string::size_type n = 0;
for (std::string::size_type i = 0; i < len; ++i)
{
char c = s[i];
if (n != i)
{
s[n] = c;
}
++n;
// The start of a C/C++ comment?
if (c == '/')
{
++i;
if (i == len)
{
// No further characters
break;
}
c = s[i];
if (c == '/')
{
// C++ comment - search for end-of-line
--n;
i = s.find('\n', ++i);
if (i == std::string::npos)
{
// Trucated - done
break;
}
}
else if (c == '*')
{
// C comment - search for '*/'
--n;
i = s.find("*/", ++i, 2);
if (i == std::string::npos)
{
// Trucated - done
break;
}
++i;
}
else
{
// Not a C/C++ comment
if (n != i)
{
s[n] = c;
}
++n;
}
}
}
s.resize(n);
}
Foam::string Foam::stringOps::lower(const std::string& original)
{
string s(original);
inplaceLower(s);
return s;
}
void Foam::stringOps::inplaceLower(std::string& s)
{
for (auto iter = s.begin(); iter != s.end(); ++iter)
{
*iter = static_cast<std::string::value_type>
(
std::tolower(static_cast<unsigned char>(*iter))
);
}
}
Foam::string Foam::stringOps::upper(const std::string& original)
{
string s(original);
inplaceUpper(s);
return s;
}
void Foam::stringOps::inplaceUpper(std::string& s)
{
for (auto iter = s.begin(); iter != s.end(); ++iter)
{
*iter = static_cast<std::string::value_type>
(
std::toupper(static_cast<unsigned char>(*iter))
);
}
}
void Foam::stringOps::writeWrapped
(
OSstream& os,
const std::string& str,
const std::string::size_type width,
const std::string::size_type indent,
const bool escape
)
{
const auto len = str.length();
std::string::size_type pos = 0;
// Handle leading newlines
while (str[pos] == '\n' && pos < len)
{
os << '\n';
++pos;
}
while (pos < len)
{
// Potential end point and next point
std::string::size_type end = pos + width - 1;
std::string::size_type eol = str.find('\n', pos);
std::string::size_type next = string::npos;
if (end >= len)
{
// No more wrapping needed
end = len;
if (std::string::npos != eol && eol <= end)
{
// Manual '\n' break, next follows it (default behaviour)
end = eol;
}
}
else if (std::string::npos != eol && eol <= end)
{
// Manual '\n' break, next follows it (default behaviour)
end = eol;
}
else if (isspace(str[end]))
{
// Ended on a space - can use this directly
next = str.find_first_not_of(" \t\n", end); // Next non-space
}
else if (isspace(str[end+1]))
{
// The next one is a space - so we are okay
++end; // Otherwise the length is wrong
next = str.find_first_not_of(" \t\n", end); // Next non-space
}
else
{
// Line break will be mid-word
auto prev = str.find_last_of(" \t\n", end); // Prev word break
if (std::string::npos != prev && prev > pos)
{
end = prev;
next = prev + 1; // Continue from here
}
}
// The next position to continue from
if (std::string::npos == next)
{
next = end + 1;
}
// Has a length
if (end > pos)
{
// Indent following lines.
// The first one was already done prior to calling this routine.
if (pos)
{
for (std::string::size_type i = 0; i < indent; ++i)
{
os <<' ';
}
}
while (pos < end)
{
const char c = str[pos];
if (escape && c == '\\')
{
os << '\\';
}
os << c;
++pos;
}
os << nl;
}
pos = next;
}
}
// ************************************************************************* //