/* PIP - Platform Independent Primitives Config parser Ivan Pelipenko peri4ko@yandex.ru This program is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this program. If not, see . */ #include "piconfig.h" #include "pifile.h" #include "piiostring.h" #ifdef PIP_STD_IOSTREAM # include "pistring_std.h" # include #endif /*! \class PIConfig * \brief Configuration file * \details This class provide handle access to configuration file. * * \section PIConfig_sec0 Synopsis * PIConfig reads configuration file and create internal dendritic * representation of all entries of this file. You can easily read * some values and write new. * \image html piconfig.png * * %PIConfig supports also INI-style files with sections "[section]". * In this case line with section name interpret as prefix to the next * lines. For example, these configs are equal: * \code * ser.device = /dev/ttyS0 * ser.speed = 115200 * debug = true * \endcode * \code * [ser] * device = /dev/ttyS0 * speed = 115200 * [] * debug = true * \endcode * * You can use multiline values ends with " \" * \code * value = start \ #s comment * _mid \ * _end * \endcode * In this example value = "start_mid_end" * * \section PIConfig_sec1 Concepts * Each node of internal tree has type PIConfig::Entry. %PIConfig * has one root element \a rootEntry(). Any entry of configuration file is a * child of this element. * */ /*! \class PIConfig::Entry * \brief %Entry of configuration file * \details This class is node of internal PIConfig tree. * %Entry provide access to elements of PIConfig. Each entry has * children or next properties: * * name * * value * * type * * comment * * Each property is a PIString. These properties forms from text line with * format: \code{.cpp} = # \endcode * Type and comment are optional fields. Type is a single letter immediately * after comment symbol "#". \n \n * %Entry has many implicit convertions to common types: boolean, integers, * float, double, PIString, PIStringList. \n \n * Generally there is no need to create instance of %PIConfig::Entry manually, * it returns by functions \a getValue() of \a PIConfig, \a PIConfig::Entry or * \a PIConfig::Branch. If there is no suitable entry to return, reference to * internal instance of %PIConfig::Entry with "default" value will be returned. * \snippet piconfig.cpp PIConfig::Entry * */ /*! \class PIConfig::Branch * \brief %Branch is a list of entries of configuration file * \details %Branch provides some features to get entries lists. * \snippet piconfig.cpp PIConfig::Branch * */ PIConfig::Entry PIConfig::Branch::_empty; PIConfig::Entry PIConfig::Entry::_empty; PIConfig::Branch PIConfig::Branch::allLeaves() { Branch b; b.delim = delim; piForeach(Entry * i, *this) { if (i->isLeaf()) b << i; else allLeaves(b, i); } return b; } PIConfig::Entry & PIConfig::Branch::getValue(const PIString & vname, const PIString & def, bool * exist) { if (vname.isEmpty()) { _empty.clear(); _empty.delim = delim; if (exist != 0) *exist = false; return _empty; } PIStringList tree = vname.split(delim); PIString name = tree.front(); tree.pop_front(); Entry * ce = 0; piForeach(Entry * i, *this) if (i->_name == name) { ce = i; break; } if (ce == 0) { _empty._name = vname; _empty._value = def; _empty.delim = delim; if (exist != 0) *exist = false; return _empty; } piForeach(PIString & i, tree) { ce = ce->findChild(i); if (ce == 0) { _empty._name = vname; _empty._value = def; _empty.delim = delim; if (exist != 0) *exist = false; return _empty; } } if (exist != 0) *exist = true; return *ce; } PIConfig::Branch PIConfig::Branch::getValues(const PIString & name) { Branch b; b.delim = delim; piForeach(Entry * i, *this) { if (i->isLeaf()) { if (i->_name.find(name) >= 0) b << i; } else { piForeach(Entry * j, i->_children) if (j->_name.find(name) >= 0) b << j; } } return b; } PIConfig::Branch PIConfig::Branch::getLeaves() { Branch b; b.delim = delim; piForeach(Entry * i, *this) if (i->isLeaf()) b << i; return b; } PIConfig::Branch PIConfig::Branch::getBranches() { Branch b; b.delim = delim; piForeach(Entry * i, *this) if (!i->isLeaf()) b << i; return b; } PIConfig::Branch & PIConfig::Branch::filter(const PIString & f) { for (int i = 0; i < size_s(); ++i) { if (at(i)->_name.find(f) < 0) { remove(i); --i; } } return *this; } bool PIConfig::Branch::entryExists(const Entry * e, const PIString & name) const { if (e->_children.isEmpty()) { return (e->_name == name); } piForeachC(Entry * i, e->_children) if (entryExists(i, name)) return true; return false; } PIConfig::Entry & PIConfig::Entry::getValue(const PIString & vname, const PIString & def, bool * exist) { PIStringList tree = vname.split(delim); Entry * ce = this; piForeach(PIString & i, tree) { ce = ce->findChild(i); if (ce == 0) { _empty._name = vname; _empty._value = def; _empty.delim = delim; if (exist != 0) *exist = false; return _empty; } } if (exist != 0) *exist = true; return *ce; } PIConfig::Branch PIConfig::Entry::getValues(const PIString & vname) { Branch b; b.delim = delim; piForeach(Entry * i, _children) if (i->_name.find(vname) >= 0) b << i; return b; } bool PIConfig::Entry::entryExists(const Entry * e, const PIString & name) const { if (e->_children.isEmpty()) { return (e->_name == name); } piForeachC(Entry * i, e->_children) if (entryExists(i, name)) return true; return false; } #ifdef PIP_STD_IOSTREAM void PIConfig::Entry::coutt(std::ostream & s, const PIString & p) const { PIString nl = p + " "; if (!_value.isEmpty()) s << p << _name << " = " << _value << std::endl; else std::cout << p << _name << std::endl; piForeachC(Entry * i, _children) i->coutt(s, nl); } #endif void PIConfig::Entry::piCoutt(PICout s, const PIString & p) const { PIString nl = p + " "; if (!_value.isEmpty()) s << p << _name << " = " << _value << " (" << _type << " " << _comment << ")" << PICoutManipulators::NewLine; else s << p << _name << PICoutManipulators::NewLine; piForeachC(Entry * i, _children) i->piCoutt(s, nl); } PIConfig::PIConfig(const PIString & path, PIIODevice::DeviceMode mode) { _init(); open(path, mode); } PIConfig::PIConfig(PIString * string, PIIODevice::DeviceMode mode) { _init(); open(string, mode); } PIConfig::PIConfig(PIIODevice * device, PIIODevice::DeviceMode mode) { _init(); open(device, mode); } PIConfig::PIConfig(const PIString & path, PIStringList dirs) { _init(); internal = true; own_dev = true; dev = new PIFile(path, PIIODevice::ReadOnly); incdirs = dirs; incdirs << PIFile::fileInfo(path).dir(); while (!dev->isOpened()) { if (dirs.isEmpty()) break; PIString cp = dirs.back(); if (cp.endsWith("/") || cp.endsWith("\\")) cp.pop_back(); cp += "/" + path; dev->open(cp, PIIODevice::ReadOnly); dirs.pop_back(); } if (!dev->isOpened()) { delete dev; dev = 0; return; } _setupDev(); parse(); } PIConfig::~PIConfig() { root.deleteBranch(); _destroy(); } bool PIConfig::open(const PIString & path, PIIODevice::DeviceMode mode) { _destroy(); incdirs << PIFile::fileInfo(path).dir(); own_dev = true; dev = new PIFile(path, mode); if (!dev->isOpened()) dev->open(path, mode); _setupDev(); parse(); return dev->isOpened(); } bool PIConfig::open(PIString * string, PIIODevice::DeviceMode mode) { _destroy(); own_dev = true; dev = new PIIOString(string, mode); _setupDev(); parse(); return true; } bool PIConfig::open(PIIODevice * device, PIIODevice::DeviceMode mode) { _destroy(); own_dev = false; dev = device; if (dev) { dev->open(mode); if (dev->isTypeOf()) incdirs << PIFile::fileInfo(((PIFile *)dev)->path()).dir(); } _setupDev(); parse(); if (device) return device->isOpened(); return false; } void PIConfig::_init() { delim = PIStringAscii("."); root.delim = delim; empty.delim = delim; empty._parent = 0; } void PIConfig::_destroy() { if (stream) { delete stream; stream = nullptr; } if (own_dev && dev) delete dev; dev = nullptr; piForeach(PIConfig * c, inc_devs) delete c; inc_devs.clear(); } void PIConfig::_setupDev() { if (!dev) return; stream = new PIIOTextStream(dev); stream->setEncoding(PIIOTextStream::UTF8); } void PIConfig::_clearDev() { if (!dev) return; if (PIString(dev->className()) == "PIFile") { ((PIFile *)dev)->clear(); return; } if (PIString(dev->className()) == "PIIOString") { ((PIIOString *)dev)->clear(); return; } } void PIConfig::_flushDev() { if (!dev) return; if (PIString(dev->className()) == "PIFile") { ((PIFile *)dev)->flush(); } } bool PIConfig::_isEndDev() { if (!stream) return true; return stream->isEnd(); } void PIConfig::_seekToBeginDev() { if (!dev) return; if (PIString(dev->className()) == "PIFile") { ((PIFile *)dev)->seekToBegin(); return; } if (PIString(dev->className()) == "PIIOString") { ((PIIOString *)dev)->seekToBegin(); return; } } PIString PIConfig::_readLineDev() { if (!stream) return PIString(); return stream->readLine(); } void PIConfig::_writeDev(const PIString & l) { // piCout << "write \"" << l << "\""; if (!stream) return; stream->append(l); } bool PIConfig::isOpened() const { if (dev) return dev->isOpened(); return false; } PIConfig::Entry & PIConfig::getValue(const PIString & vname, const PIString & def, bool * exist) { PIStringList tree = vname.split(delim); Entry * ce = &root; piForeach(PIString & i, tree) { ce = ce->findChild(i); if (ce == 0) { if (exist != 0) *exist = false; empty._name = vname; empty._value = def; empty.delim = delim; return empty; } } if (exist != 0) *exist = true; return *ce; } PIConfig::Branch PIConfig::getValues(const PIString & vname) { Branch b; b.delim = delim; piForeach(Entry * i, root._children) if (i->_name.find(vname) >= 0) b << i; return b; }; void PIConfig::addEntry(const PIString & name, const PIString & value, const PIString & type, bool write) { if (getValue(name)._parent != 0) return; bool toRoot = false; PIStringList tree = name.split(delim); PIString ename = tree.back(); tree.pop_back(); Entry *te, *ce, *entry = &root; if (tree.isEmpty()) toRoot = true; piForeach(PIString & i, tree) { te = entry->findChild(i); if (te == 0) { ce = new Entry(); ce->delim = delim; ce->_tab = entry->_tab; ce->_line = entry->_line; ce->_name = i; ce->_parent = entry; entry->_children << ce; entry = ce; } else entry = te; } PIConfig::Branch ch = entry->_children; ch.sort(PIConfig::Entry::compare); te = (entry->isLeaf() ? 0 : ch.back()); ce = new Entry(); ce->delim = delim; ce->_name = ename; ce->_value = value; ce->_type = type; if (te == 0) { ce->_tab = entry->_tab; if (toRoot) ce->_line = other.size_s() - 1; else ce->_line = entry->_line; } else { ce->_tab = te->_tab; if (toRoot) ce->_line = other.size_s() - 1; else { ch = entry->_parent->_children; ch.sort(PIConfig::Entry::compare); ce->_line = ch.back()->_line + 1; } } ce->_parent = entry; entry->_children << ce; other.insert(ce->_line, ""); Branch b = allLeaves(); bool found = false; for (int i = 0; i < b.size_s(); ++i) { if (found) { b[i]->_line++; continue; } if (b[i] == ce) { found = true; if (i > 0) if (b[i - 1]->_line == b[i]->_line) b[i - 1]->_line++; } } if (write) writeAll(); } void PIConfig::setValue(const PIString & name, const PIString & value, const PIString & type, bool write) { Entry & e(getValue(name)); if (&e == &empty) { addEntry(name, value, type, write); return; } e._value = value; e._type = type; if (write) writeAll(); } int PIConfig::entryIndex(const PIString & name) { PIStringList tree = name.split(delim); Entry * ce = &root; piForeach(PIString & i, tree) { ce = ce->findChild(i); if (ce == 0) return -1; } return allLeaves().indexOf(ce); } void PIConfig::setValue(uint number, const PIString & value, bool write) { Entry & e(entryByIndex(number)); if (&e == &empty) return; e._value = value; if (write) writeAll(); } void PIConfig::setName(uint number, const PIString & name, bool write) { Entry & e(entryByIndex(number)); if (&e == &empty) return; e._name = name; if (write) writeAll(); } void PIConfig::setType(uint number, const PIString & type, bool write) { Entry & e(entryByIndex(number)); if (&e == &empty) return; e._type = type; if (write) writeAll(); } void PIConfig::setComment(uint number, const PIString & comment, bool write) { Entry & e(entryByIndex(number)); if (&e == &empty) return; e._comment = comment; if (write) writeAll(); } void PIConfig::removeEntry(const PIString & name, bool write) { Entry & e(getValue(name)); if (&e == &empty) return; Branch b = allLeaves(); removeEntry(b, &e); if (write) writeAll(); } void PIConfig::removeEntry(uint number, bool write) { Entry & e(entryByIndex(number)); if (&e == &empty) return; Branch b = allLeaves(); removeEntry(b, &e); if (write) writeAll(); } void PIConfig::removeEntry(Branch & b, PIConfig::Entry * e) { bool leaf = true; if (e->isLeaf()) other.remove(e->_line); if (!e->isLeaf() && !e->_value.isEmpty()) { e->_value.clear(); leaf = false; } else { int cc = e->_children.size_s(); piForTimes(cc) removeEntry(b, e->_children.back()); } bool found = false; for (int i = 0; i < b.size_s(); ++i) { if (found) { b[i]->_line--; continue; } if (b[i] == e) found = true; } if (!leaf) return; e->_parent->_children.removeOne(e); b.removeOne(e); delete e; } PIString PIConfig::getPrefixFromLine(PIString line, bool * exists) { line.trim(); if (line.left(1) == "#") { if (exists) *exists = false; return PIString(); } int ci = line.find("#"); if (ci >= 0) line.cutRight(line.size() - ci); if (line.find("=") >= 0) { if (exists) *exists = false; return PIString(); } if (line.find("[") >= 0 && line.find("]") >= 0) { if (exists) *exists = true; return line.takeRange('[', ']').trim(); } if (exists) *exists = false; return PIString(); } void PIConfig::writeAll() { // cout << this << " write < " << size() << endl; _clearDev(); buildFullNames(&root); Branch b = allLeaves(); PIString prefix, tprefix; bool isPrefix; // for (int i = 0; i < b.size_s(); ++i) // cout << b[i]->_name << " = " << b[i]->_value << endl; int j = 0; for (int i = 0; i < other.size_s(); ++i) { // cout << j << endl; if (j >= 0 && j < b.size_s()) { if (b[j]->_line == i) { b[j]->buildLine(); _writeDev((b[j]->_all).cutLeft(prefix.size()) + "\n"); // cout << this << " " << b[j]->_all << endl; ++j; } else { _writeDev(other[i]); tprefix = getPrefixFromLine(other[i], &isPrefix); if (isPrefix) { prefix = tprefix; if (!prefix.isEmpty()) prefix += delim; } if (i < other.size_s() - 1) _writeDev('\n'); // cout << this << " " << other[i] << endl; } } else { _writeDev(other[i]); tprefix = getPrefixFromLine(other[i], &isPrefix); if (isPrefix) { prefix = tprefix; if (!prefix.isEmpty()) prefix += delim; } if (i < other.size_s() - 1) _writeDev('\n'); // cout << this << " " << other[i] << endl; } } _flushDev(); readAll(); // cout << this << " write > " << size() << endl; } void PIConfig::clear() { _clearDev(); parse(); } void PIConfig::readAll() { root.deleteBranch(); root.clear(); parse(); } bool PIConfig::entryExists(const Entry * e, const PIString & name) const { if (e->_children.isEmpty()) { return (e->_name == name); } piForeachC(Entry * i, e->_children) if (entryExists(i, name)) return true; return false; } void PIConfig::updateIncludes() { if (internal) return; all_includes.clear(); piForeach(PIConfig * c, includes) all_includes << c->allLeaves(); } PIString PIConfig::parseLine(PIString v) { int i = -1, l = 0; while (1) { i = v.find("${"); if (i < 0) break; PIString w = v.mid(i + 1).takeRange('{', '}'), r; l = w.length() + 3; w = parseLine(w); w.trim(); bool ex = false; PIConfig::Entry & me = getValue(w, "", &ex); if (ex) { r = me._value; } else { piForeachC(PIConfig::Entry * e, all_includes) if (e->_full_name == w) { r = e->_value; break; } } v.replace(i, l, r); } return v; } void PIConfig::parse() { // piCout << "[PIConfig] charset" << PIFile::defaultCharset(); PIString src, str, tab, comm, all, name, type, prefix, tprefix; PIStringList tree; Entry *entry = 0, *te = 0, *ce = 0; int ind, sind; bool isNew = false, isPrefix = false, wasMultiline = false, isMultiline = false; piForeach(PIConfig * c, inc_devs) delete c; inc_devs.clear(); includes.clear(); if (!isOpened()) return; _seekToBeginDev(); other.clear(); lines = 0; while (!_isEndDev()) { other.push_back(PIString()); src = str = parseLine(_readLineDev()); tprefix = getPrefixFromLine(src, &isPrefix); if (isPrefix) { prefix = tprefix; if (!prefix.isEmpty()) prefix += delim; } // piCout << "line \"" << str << "\""; tab = str.left(str.find(str.trimmed().left(1))); str.trim(); all = str; sind = str.find('#'); if (sind > 0) { comm = str.mid(sind + 1).trimmed(); if (!comm.isEmpty()) { type = comm[0]; comm.cutLeft(1).trim(); } else type = "s"; str = str.left(sind).trim(); } else { type = "s"; comm = ""; } if (str.endsWith(" \\")) { isMultiline = true; str.cutRight(2).trim(); } else isMultiline = false; if (wasMultiline) { wasMultiline = false; if (ce) { ce->_value += str; ce->_all += " \\\n" + all; } str.clear(); } else ce = 0; wasMultiline = isMultiline; // piCout << "[PIConfig] str" << str.size() << str << str.toUTF8(); ind = str.find('='); if ((ind > 0) && (str[0] != '#')) { tree = (prefix + str.left(ind).trimmed()).split(delim); if (tree.front() == "include") { name = str.mid(ind + 1).trimmed(); PIConfig * iconf = new PIConfig(name, incdirs); // piCout << "include" << name << iconf->dev; if (!iconf->dev) { delete iconf; } else { inc_devs << iconf; includes << iconf << iconf->includes; updateIncludes(); } other.back() = src; } else { name = tree.back(); tree.pop_back(); entry = &root; piForeachC(PIString & i, tree) { te = entry->findChild(i); if (te == 0) { ce = new Entry(); ce->delim = delim; ce->_tab = tab; ce->_line = lines; ce->_name = i; ce->_parent = entry; entry->_children << ce; entry = ce; } else entry = te; } isNew = false; ce = entry->findChild(name); if (ce == 0) { ce = new Entry(); isNew = true; } ce->delim = delim; ce->_tab = tab; ce->_name = name; ce->_value = str.mid(ind + 1).trimmed(); ce->_type = type; ce->_comment = comm; // piCout << "[PIConfig] comm" << comm.size() << comm << comm.toUTF8(); // piCout << "[PIConfig] type" << type.size() << type << type.toUTF8(); ce->_line = lines; ce->_all = all; if (isNew) { ce->_parent = entry; entry->_children << ce; } } } else other.back() = src; lines++; } setEntryDelim(&root, delim); buildFullNames(&root); } #ifdef PIP_STD_IOSTREAM std::ostream & operator<<(std::ostream & s, const PIConfig::Entry & v) { s << v.value(); return s; } std::ostream & operator<<(std::ostream & s, const PIConfig::Branch & v) { v.coutt(s, ""); return s; } #endif