/* * inicpp.h * * Created on: 26 Dec 2015 * Author: Fabian Meyer * License: MIT * https://github.com/Rookfighter/inifile-cpp */ #ifndef INICPP_H_ #define INICPP_H_ #include #include #include #include #include #include #include #include #include #ifdef __cpp_lib_string_view // This one is defined in if we have std::string_view # include #endif namespace ini { /************************************************ * Helper Functions ************************************************/ /** Returns a string of whitespace characters. */ constexpr const char *whitespaces() { return " \t\n\r\f\v"; } /** Returns a string of indentation characters. */ constexpr const char *indents() { return " \t"; } /** Trims a string in place. * @param str string to be trimmed in place */ inline void trim(std::string &str) { // first erasing from end should be slighty more efficient // because erasing from start potentially moves all chars // multiple indices towards the front. auto lastpos = str.find_last_not_of(whitespaces()); if(lastpos == std::string::npos) { str.clear(); return; } str.erase(lastpos + 1); str.erase(0, str.find_first_not_of(whitespaces())); } /************************************************ * Conversion Functors ************************************************/ inline bool strToLong(const std::string &value, long &result) { char *endptr; // check if decimal result = std::strtol(value.c_str(), &endptr, 10); if(*endptr == '\0') return true; // check if octal result = std::strtol(value.c_str(), &endptr, 8); if(*endptr == '\0') return true; // check if hex result = std::strtol(value.c_str(), &endptr, 16); if(*endptr == '\0') return true; return false; } inline bool strToULong(const std::string &value, unsigned long &result) { char *endptr; // check if decimal result = std::strtoul(value.c_str(), &endptr, 10); if(*endptr == '\0') return true; // check if octal result = std::strtoul(value.c_str(), &endptr, 8); if(*endptr == '\0') return true; // check if hex result = std::strtoul(value.c_str(), &endptr, 16); if(*endptr == '\0') return true; return false; } template struct Convert {}; template<> struct Convert { void decode(const std::string &value, bool &result) { std::string str(value); std::transform(str.begin(), str.end(), str.begin(), [](const char c){ return static_cast(::toupper(c)); }); if(str == "TRUE") result = true; else if(str == "FALSE") result = false; else throw std::invalid_argument("field is not a bool"); } void encode(const bool value, std::string &result) { result = value ? "true" : "false"; } }; template<> struct Convert { void decode(const std::string &value, char &result) { assert(value.size() > 0); result = value[0]; } void encode(const char value, std::string &result) { result = value; } }; template<> struct Convert { void decode(const std::string &value, unsigned char &result) { assert(value.size() > 0); result = value[0]; } void encode(const unsigned char value, std::string &result) { result = value; } }; template<> struct Convert { void decode(const std::string &value, short &result) { long tmp; if(!strToLong(value, tmp)) throw std::invalid_argument("field is not a short"); result = static_cast(tmp); } void encode(const short value, std::string &result) { std::stringstream ss; ss << value; result = ss.str(); } }; template<> struct Convert { void decode(const std::string &value, unsigned short &result) { unsigned long tmp; if(!strToULong(value, tmp)) throw std::invalid_argument("field is not an unsigned short"); result = static_cast(tmp); } void encode(const unsigned short value, std::string &result) { std::stringstream ss; ss << value; result = ss.str(); } }; template<> struct Convert { void decode(const std::string &value, int &result) { long tmp; if(!strToLong(value, tmp)) throw std::invalid_argument("field is not an int"); result = static_cast(tmp); } void encode(const int value, std::string &result) { std::stringstream ss; ss << value; result = ss.str(); } }; template<> struct Convert { void decode(const std::string &value, unsigned int &result) { unsigned long tmp; if(!strToULong(value, tmp)) throw std::invalid_argument("field is not an unsigned int"); result = static_cast(tmp); } void encode(const unsigned int value, std::string &result) { std::stringstream ss; ss << value; result = ss.str(); } }; template<> struct Convert { void decode(const std::string &value, long &result) { if(!strToLong(value, result)) throw std::invalid_argument("field is not a long"); } void encode(const long value, std::string &result) { std::stringstream ss; ss << value; result = ss.str(); } }; template<> struct Convert { void decode(const std::string &value, unsigned long &result) { if(!strToULong(value, result)) throw std::invalid_argument("field is not an unsigned long"); } void encode(const unsigned long value, std::string &result) { std::stringstream ss; ss << value; result = ss.str(); } }; template<> struct Convert { void decode(const std::string &value, double &result) { result = std::stod(value); } void encode(const double value, std::string &result) { std::stringstream ss; ss << value; result = ss.str(); } }; template<> struct Convert { void decode(const std::string &value, float &result) { result = std::stof(value); } void encode(const float value, std::string &result) { std::stringstream ss; ss << value; result = ss.str(); } }; template<> struct Convert { void decode(const std::string &value, std::string &result) { result = value; } void encode(const std::string &value, std::string &result) { result = value; } }; #ifdef __cpp_lib_string_view template<> struct Convert { void decode(const std::string &value, std::string_view &result) { result = value; } void encode(const std::string_view value, std::string &result) { result = value; } }; #endif template<> struct Convert { void encode(const char* const &value, std::string &result) { result = value; } void decode(const std::string &value, const char* &result) { result = value.c_str(); } }; template<> struct Convert { void encode(const char* const &value, std::string &result) { result = value; } }; template struct Convert { void encode(const char *value, std::string &result) { result = value; } }; class IniField { private: std::string value_; public: IniField() : value_() {} IniField(const std::string &value) : value_(value) {} IniField(const IniField &field) : value_(field.value_) {} ~IniField() {} template T as() const { Convert conv; T result; conv.decode(value_, result); return result; } template IniField &operator=(const T &value) { Convert conv; conv.encode(value, value_); return *this; } IniField &operator=(const IniField &field) { value_ = field.value_; return *this; } }; struct StringInsensitiveLess { bool operator()(std::string lhs, std::string rhs) const { std::transform(lhs.begin(), lhs.end(), lhs.begin(), [](const char c){ return static_cast(::tolower(c)); }); std::transform(rhs.begin(), rhs.end(), rhs.begin(), [](const char c){ return static_cast(::tolower(c)); }); return lhs < rhs; } }; template class IniSectionBase : public std::map { public: IniSectionBase() {} ~IniSectionBase() {} void setComment(const std::vector& c) { comment_.insert(comment_.end(), c.begin(), c.end()); } void setComment(const std::string& c) { comment_.push_back(c); } const std::vector& getComment() const { return comment_; } private: std::vector comment_; // Section comment }; using IniSection = IniSectionBase>; using IniSectionCaseInsensitive = IniSectionBase; template class IniFileBase : public std::map, Comparator> { private: char fieldSep_ = '='; char esc_ = '\\'; //std::vector commentPrefixes_ = { "#" , ";" }; std::vector commentPrefixes_ = { ";" }; bool multiLineValues_ = false; bool overwriteDuplicateFields_ = true; void eraseComment(const std::string &commentPrefix, std::string &str, std::string::size_type startpos = 0) { size_t prefixpos = str.find(commentPrefix, startpos); if(std::string::npos == prefixpos) return; // Found a comment prefix, is it escaped? if(0 != prefixpos && str[prefixpos - 1] == esc_) { // The comment prefix is escaped, so just delete the escape char // and keep erasing after the comment prefix str.erase(prefixpos - 1, 1); eraseComment( commentPrefix, str, prefixpos - 1 + commentPrefix.size()); } else { str.erase(prefixpos); } } void eraseComments(std::string &str) { for(const std::string &commentPrefix : commentPrefixes_) eraseComment(commentPrefix, str); } /** Tries to find a suitable comment prefix for the string data at the given * position. Returns commentPrefixes_.end() if not match was found. */ std::vector::const_iterator findCommentPrefix(const std::string &str, const std::size_t startpos) const { // if startpos is invalid simply return "not found" if(startpos >= str.size()) return commentPrefixes_.end(); for(size_t i = 0; i < commentPrefixes_.size(); ++i) { const std::string &prefix = commentPrefixes_[i]; // if this comment prefix is longer than the string view itself // then skip if(prefix.size() + startpos > str.size()) continue; bool match = true; for(size_t j = 0; j < prefix.size() && match; ++j) match = str[startpos + j] == prefix[j]; if(match) return commentPrefixes_.begin() + i; } return commentPrefixes_.end(); } void writeEscaped(std::ostream &os, const std::string &str) const { for(size_t i = 0; i < str.length(); ++i) { auto prefixpos = findCommentPrefix(str, i); // if no suitable prefix was found at this position // then simply write the current character if(prefixpos != commentPrefixes_.end()) { const std::string &prefix = *prefixpos; os.put(esc_); os.write(prefix.c_str(), prefix.size()); i += prefix.size() - 1; } else if (multiLineValues_ && str[i] == '\n') os.write("\n\t", 2); else os.put(str[i]); } } public: IniFileBase() = default; IniFileBase(const char fieldSep, const char comment) : fieldSep_(fieldSep), commentPrefixes_(1, std::string(1, comment)) {} IniFileBase(const std::string &filename) { load(filename); } IniFileBase(std::istream &is) { decode(is); } IniFileBase(const char fieldSep, const std::vector &commentPrefixes) : fieldSep_(fieldSep), commentPrefixes_(commentPrefixes) {} IniFileBase(const std::string &filename, const char fieldSep, const std::vector &commentPrefixes) : fieldSep_(fieldSep), commentPrefixes_(commentPrefixes) { load(filename); } IniFileBase(std::istream &is, const char fieldSep, const std::vector &commentPrefixes) : fieldSep_(fieldSep), commentPrefixes_(commentPrefixes) { decode(is); } ~IniFileBase() {} /** Sets the separator charactor for fields in the INI file. * @param sep separator character to be used. */ void setFieldSep(const char sep) { fieldSep_ = sep; } /** Sets the character that should be interpreted as the start of comments. * Default is '#'. * Note: If the inifile contains the comment character as data it must be prefixed with * the configured escape character. * @param comment comment character to be used. */ void setCommentChar(const char comment) { commentPrefixes_ = {std::string(1, comment)}; } /** Sets the list of strings that should be interpreted as the start of comments. * Default is [ "#" ]. * Note: If the inifile contains any comment string as data it must be prefixed with * the configured escape character. * @param commentPrefixes vector of comment prefix strings to be used. */ void setCommentPrefixes(const std::vector &commentPrefixes) { commentPrefixes_ = commentPrefixes; } /** Sets the character that should be used to escape comment prefixes. * Default is '\'. * @param esc escape character to be used. */ void setEscapeChar(const char esc) { esc_ = esc; } /** Sets whether or not to parse multi-line field values. * Default is false. * @param enable enable or disable? */ void setMultiLineValues(bool enable) { multiLineValues_ = enable; } /** Sets whether or not overwriting duplicate fields is allowed. * If overwriting duplicate fields is not allowed, * an exception is thrown when a duplicate field is found inside a section. * Default is true. * @param allowed Is overwriting duplicate fields allowed or not? */ void allowOverwriteDuplicateFields(bool allowed) { overwriteDuplicateFields_ = allowed; } /** Tries to decode a ini file from the given input stream. * @param is input stream from which data should be read. */ void decode(std::istream &is) { this->clear(); int lineNo = 0; IniSectionBase *currentSection = nullptr; std::string mutliLineValueFieldName = ""; std::string line; // iterate file line by line while(!is.eof() && !is.fail()) { std::getline(is, line, '\n'); eraseComments(line); bool hasIndent = line.find_first_not_of(indents()) != 0; trim(line); ++lineNo; // skip if line is empty if(line.size() == 0) continue; if(line[0] == '[') { // line is a section // check if the section is also closed on same line std::size_t pos = line.find("]"); if(pos == std::string::npos) { std::stringstream ss; ss << "l." << lineNo << ": ini parsing failed, section not closed"; throw std::logic_error(ss.str()); } // check if the section name is empty if(pos == 1) { std::stringstream ss; ss << "l." << lineNo << ": ini parsing failed, section is empty"; throw std::logic_error(ss.str()); } // retrieve section name std::string secName = line.substr(1, pos - 1); currentSection = &((*this)[secName]); // clear multiline value field name // a new section means there is no value to continue mutliLineValueFieldName = ""; } else { // line is a field definition // check if section was already opened if(currentSection == nullptr) { std::stringstream ss; ss << "l." << lineNo << ": ini parsing failed, field has no section" " or ini file in use by another application"; throw std::logic_error(ss.str()); } // find key value separator std::size_t pos = line.find(fieldSep_); if (multiLineValues_ && hasIndent && mutliLineValueFieldName != "") { // extend a multi-line value IniField previous_value = (*currentSection)[mutliLineValueFieldName]; std::string value = previous_value.as() + "\n" + line; (*currentSection)[mutliLineValueFieldName] = value; } else if(pos == std::string::npos) { std::stringstream ss; ss << "l." << lineNo << ": ini parsing failed, no '" << fieldSep_ << "' found"; if (multiLineValues_) ss << ", and not a multi-line value continuation"; throw std::logic_error(ss.str()); } else { // retrieve field name and value std::string name = line.substr(0, pos); trim(name); if (!overwriteDuplicateFields_ && currentSection->count(name) != 0) { std::stringstream ss; ss << "l." << lineNo << ": ini parsing failed, duplicate field found"; throw std::logic_error(ss.str()); } std::string value = line.substr(pos + 1, std::string::npos); trim(value); (*currentSection)[name] = value; // store last field name for potential multi-line values mutliLineValueFieldName = name; } } } } /** Tries to decode a ini file from the given input string. * @param content string to be decoded. */ void decode(const std::string &content) { std::istringstream ss(content); decode(ss); } /** Tries to load and decode a ini file from the file at the given path. * @param fileName path to the file that should be loaded. */ void load(const std::string &fileName) { std::ifstream is(fileName.c_str()); decode(is); } /** Encodes this inifile object and writes the output to the given stream. * @param os target stream. */ void encode(std::ostream &os) const { // iterate through all sections in this file for(const auto &filePair : *this) { const auto& sec = filePair.second; if (!sec.getComment().empty()) { for (auto comment : sec.getComment()) { std::istringstream iss(comment); std::string line; while (std::getline(iss, line)) os << "; " << line << '\n'; } } os.put('['); writeEscaped(os, filePair.first); os.put(']'); os.put('\n'); // iterate through all fields in the section for(const auto &secPair : filePair.second) { writeEscaped(os, secPair.first); os.put(fieldSep_); writeEscaped(os, secPair.second.template as()); os.put('\n'); } // Add a newline after each section os.put('\n'); } } /** Encodes this inifile object as string and returns the result. * @return encoded infile string. */ std::string encode() const { std::ostringstream ss; encode(ss); return ss.str(); } /** Saves this inifile object to the file at the given path. * @param fileName path to the file where the data should be stored. */ void save(const std::string &fileName) const { std::ofstream os(fileName.c_str()); encode(os); } }; using IniFile = IniFileBase>; using IniSection = IniSectionBase>; using IniFileCaseInsensitive = IniFileBase; using IniSectionCaseInsensitive = IniSectionBase; } #endif