Table of Contents
Recently, I am very interested in the parser generator like Flex/Bison and Boost Spirit v3 but Flex/Bison is C API and Boost Spirit x3 is still beta. I found out expression parsing is no simple task for parser generator as operator precedence has to be taken of. I decided to write my own simple parser generator for ini file and leave C++ expression alone. In other words, I just copy and paste the C++ expression into the generated C++ code. The reason for interest in parser generator is I am designing a new XML Schema scheme which is imperative, I use a simple ini format to explore its feasibility.
The Ini Schema use ini file format as well. My XML Schema uses XML format as well. The 1st token is the name and 2 token consists of C++ data type and validation expression separated by ;
# ini schema
StartDate=string;true
EndDate=string;Exists("StartDate") && EndDate() >= StartDate()
# ini file
StartDate=2016-03-01
EndDate=2016-03-02
The EndDate
validation states StartDate
must exist and EndDate
must be greater or equal to StartDate
. Exists("StartDate")
is redundant here as the StartDate()
call will check value existence. What if StartDate
is specified after EndDate
in the ini file?
# ini file: Order does not matter
EndDate=2016-03-02
StartDate=2016-03-01
The order does not matter as the validation checks are done after all lines are read.
Note: This is simplified ini file format which does not have sections.
The following schema is Alpha
color channel whose value is restricted between 0
and 255
and a CheckFolder
value which does not have any validation constraint and is optional, so we just put true
. If CheckFolder
is a mandatory value, we should put Exists("CheckFolder")
.
# ini schema
Alpha=int;Alpha() >= 0 && Alpha() <= 255
CheckFolder=bool;true
This is how to use the generated class, MyIniFile
.
MyIniFile ini_file;
std::string error;
if(ini_file.ParseFile("C:\\temp\\test.ini", error))
{
std::cout << ini_file.StartDate() << std::endl;
std::cout << ini_file.EndDate() << std::endl;
std::cout << ini_file.Alpha() << std::endl;
std::cout << std::boolalpha << ini_file.CheckFolder() << std::endl;
}
else
{
std::cerr << error << std::endl;
}
This is the generated header-only class, MyIniFile
.
Note: I use minicsv
class to parse the ini file.
#include <string>
#include <map>
#include <fstream>
#include <stdexcept>
#include "minicsv.h"
#include "Color.h"
class MyIniFile
{
private:
std::map<std::string, std::string> m_NameValueMap;
std::string m_File;
public:
bool ParseFile(const std::string& file)
{
m_NameValueMap.clear();
csv::ifstream is(file.c_str());
is.set_delimiter('=', "$$");
is.enable_terminate_on_blank_line(false);
if (is.is_open())
{
m_File = file;
while (is.read_line())
{
std::string name;
std::string value;
is >> name;
value = is.get_rest_of_line();
m_NameValueMap[csv::trim(name, " \t")] = csv::trim(value, " \t");
}
is.close();
return Validate();
}
else
{
std::ostringstream oss;
oss << "File cannot be opened:" << file;
throw std::runtime_error(oss.str().c_str());
}
return true;
}
private:
bool WriteFile(const std::string& key, const std::string& val)
{
std::vector<std::pair<std::string, std::string> > vec;
csv::ifstream is(m_File.c_str());
is.set_delimiter('=', "$$");
is.enable_terminate_on_blank_line(false);
if (is.is_open())
{
bool found=false;
while (is.read_line())
{
std::string name;
std::string value;
is >> name;
value = is.get_rest_of_line();
if (csv::trim(name, " \t") == key)
{
value = val;
found = true;
}
vec.push_back(std::make_pair(name, value));
}
if(!found)
{
vec.push_back(std::make_pair(key, val));
}
is.close();
csv::ofstream os(m_File.c_str());
os.set_delimiter('=', "$$");
if (os.is_open())
{
for (size_t i = 0; i<vec.size(); ++i)
{
os << vec[i].first << vec[i].second << NEWLINE;
}
os.flush();
os.close();
}
else
{
std::ostringstream oss;
oss << "File cannot be opened for writing:" << m_File;
throw std::runtime_error(oss.str().c_str());
}
}
else
{
std::ostringstream oss;
oss << "File cannot be opened for reading:" << m_File;
throw std::runtime_error(oss.str().c_str());
}
return true;
}
public:
bool Exists(const std::string& name)
{
typedef std::map<std::string, std::string> Map;
typedef Map::const_iterator MapConstIter;
MapConstIter it = m_NameValueMap.find(name);
return it != m_NameValueMap.end();
}
std::string StartDate()
{
if(!Exists("StartDate"))
{
throw std::runtime_error("StartDate does not exist");
}
std::string val;
std::istringstream iss(m_NameValueMap["StartDate"]);
iss >> val;
return val;
}
std::string GetSafeStartDate(std::string default_val)
{
if(Exists("StartDate"))
return StartDate();
else
return default_val;
}
bool IsValidStartDate()
{
bool ret = false;
try
{
ret = true;
}
catch(std::exception&)
{
}
return ret;
}
bool SetStartDate(std::string val)
{
std::ostringstream oss;
oss << val;
std::string str_val = oss.str();
if (str_val != m_NameValueMap["StartDate"])
{
m_NameValueMap["StartDate"] = str_val;
}
return WriteFile("StartDate", str_val);
}
std::string EndDate()
{
if(!Exists("EndDate"))
{
throw std::runtime_error("EndDate does not exist");
}
std::string val;
std::istringstream iss(m_NameValueMap["EndDate"]);
iss >> val;
return val;
}
std::string GetSafeEndDate(std::string default_val)
{
if(Exists("EndDate"))
return EndDate();
else
return default_val;
}
bool IsValidEndDate()
{
bool ret = false;
try
{
ret = Exists("StartDate") && EndDate() >= StartDate();
}
catch(std::exception&)
{
}
return ret;
}
bool SetEndDate(std::string val)
{
std::ostringstream oss;
oss << val;
std::string str_val = oss.str();
if (str_val != m_NameValueMap["EndDate"])
{
m_NameValueMap["EndDate"] = str_val;
}
return WriteFile("EndDate", str_val);
}
int Alpha()
{
if(!Exists("Alpha"))
{
throw std::runtime_error("Alpha does not exist");
}
int val;
std::istringstream iss(m_NameValueMap["Alpha"]);
iss >> val;
return val;
}
int GetSafeAlpha(int default_val)
{
if(Exists("Alpha"))
return Alpha();
else
return default_val;
}
bool IsValidAlpha()
{
bool ret = false;
try
{
ret = Alpha() >= 0 && Alpha() <= 255;
}
catch(std::exception&)
{
}
return ret;
}
bool SetAlpha(int val)
{
std::ostringstream oss;
oss << val;
std::string str_val = oss.str();
if (str_val != m_NameValueMap["Alpha"])
{
m_NameValueMap["Alpha"] = str_val;
}
return WriteFile("Alpha", str_val);
}
bool CheckFolder()
{
if(!Exists("CheckFolder"))
{
throw std::runtime_error("CheckFolder does not exist");
}
bool val = false;
std::string s(m_NameValueMap["CheckFolder"]);
if(s=="Y"||s=="1"||s=="true") val=true;
else if(s=="N"||s=="0"||s=="false") val=false;
return val;
}
bool GetSafeCheckFolder(bool default_val)
{
if(Exists("CheckFolder"))
return CheckFolder();
else
return default_val;
}
bool IsValidCheckFolder()
{
bool ret = false;
try
{
ret = true;
}
catch(std::exception&)
{
}
std::string s(m_NameValueMap["CheckFolder"]);
return ret&&(s=="Y"||s=="1"||s=="true"||s=="N"||s=="0"||s=="false");
}
bool SetCheckFolder(bool val)
{
std::ostringstream oss;
oss << std::boolalpha << val;
std::string str_val = oss.str();
if (str_val != m_NameValueMap["CheckFolder"])
{
m_NameValueMap["CheckFolder"] = str_val;
}
return WriteFile("CheckFolder", str_val);
}
Color TintedColor()
{
if(!Exists("TintedColor"))
{
throw std::runtime_error("TintedColor does not exist");
}
Color val;
std::istringstream iss(m_NameValueMap["TintedColor"]);
iss >> val;
return val;
}
Color GetSafeTintedColor(Color default_val)
{
if(Exists("TintedColor"))
return TintedColor();
else
return default_val;
}
bool IsValidTintedColor()
{
bool ret = false;
try
{
ret = true;
}
catch(std::exception&)
{
}
return ret;
}
bool SetTintedColor(Color val)
{
std::ostringstream oss;
oss << val;
std::string str_val = oss.str();
if (str_val != m_NameValueMap["TintedColor"])
{
m_NameValueMap["TintedColor"] = str_val;
}
return WriteFile("TintedColor", str_val);
}
bool Validate()
{
std::ostringstream oss;
if(!IsValidStartDate())
{
oss << "StartDate validation fails!" << std::endl;
}
if(!IsValidEndDate())
{
oss << "EndDate validation fails!" << std::endl;
}
if(!IsValidAlpha())
{
oss << "Alpha validation fails!" << std::endl;
}
if(!IsValidCheckFolder())
{
oss << "CheckFolder validation fails!" << std::endl;
}
if(!IsValidTintedColor())
{
oss << "TintedColor validation fails!" << std::endl;
}
std::string error = oss.str();
if (!error.empty())
{
throw std::runtime_error(error.c_str());
}
return true;
}
};
# ini schema
TintedColor=Color;true
# ini file
TintedColor=253,140,98
File save are done through the setters. The order of entries and whitespaces of the original file is preserved. The example below saves the alpha value.
ini_file.SetAlpha(120);
Custom user type is supported out of the box if C++ streams' <<
and >>
operators are overloaded for the type. We use Color
struct as an example. Note: we use minicsv
stringstream
to help do the conversion. Just include Color.h
in the auto-generated MyIniFile
, we are good to compile!
#include "minicsv.h"
struct Color
{
Color() : r(0), g(0), b(0){}
Color(int _r, int _g, int _b) : r(_r), g(_g), b(_b){}
Color(const Color& other) : r(other.r), g(other.g), b(other.b){}
int r;
int g;
int b;
};
inline std::ostream & operator<<(std::ostream & os, Color const & val)
{
csv::ostringstream csv_os;
csv_os.set_delimiter(',', "^^");
csv_os << val.r << val.g << val.b;
return os << csv_os.get_text();
}
inline std::istream & operator>>(std::istream & is, Color & val)
{
std::string str;
is >> str;
csv::istringstream csv_is(str.c_str());
csv_is.set_delimiter(',', "^^");
if(csv_is.read_line())
{
csv_is >> val.r >> val.g >> val.b;
}
return is;
}
Using C++ expression for validation check is a security risk for XML schema as the hacker can put arbitrary C++ code in the XML schema. And generating the XML parser using C++ compiler is very slow: this is not acceptable for high-load server needs to handle many XML file. After this consideration, I decide to use Lua for the expression because I can limit the functions which can be called.
This is not a full-fledged parser generator for production use. This is merely for validating my XML schema design which is rather imperative than declarative. In my XML schema, element and attribute also consists of a data type and constraint. My XML schema works conceptually the same for textual and binary XML, knowing the data type can greatly speed up the binary XML processing. Grab the latest code at Github.
- 2016-04-02: Initial Release
- 2016-04-07: File save capability through setters with order of entries and whitespace of the original file preserved. Custom user type supported out of the box