From 6d5fc2ff77f92bcbbc080ec8ddcdebaa3952591d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc=20W=C3=A4ckerlin?= Date: Fri, 7 Jan 2005 00:35:17 +0000 Subject: [PATCH] initial version --- mrw/configfile.cpp | 202 +++++++++++++++++++++ mrw/configfile.hpp | 362 ++++++++++++++++++++++++++++++++++++++ mrw/configfile.ini | 16 ++ mrw/configfile.ini.result | 24 +++ mrw/configfile_check.sh | 3 + mrw/configfile_test.cpp | 66 +++++++ mrw/file.hpp | 47 +++++ mrw/tokenizer_test.cpp | 4 +- 8 files changed, 723 insertions(+), 1 deletion(-) create mode 100644 mrw/configfile.cpp create mode 100644 mrw/configfile.hpp create mode 100644 mrw/configfile.ini create mode 100644 mrw/configfile.ini.result create mode 100755 mrw/configfile_check.sh create mode 100644 mrw/configfile_test.cpp create mode 100644 mrw/file.hpp diff --git a/mrw/configfile.cpp b/mrw/configfile.cpp new file mode 100644 index 0000000..5548ac0 --- /dev/null +++ b/mrw/configfile.cpp @@ -0,0 +1,202 @@ +/** @file + + $Id$ + + $Date$ + $Author$ + + @copy © Marc Wäckerlin + @license LGPL, see file COPYING + + $Log$ + Revision 1.1 2005/01/07 00:31:38 marc + initial version + + + 1 2 3 4 5 6 7 8 + 5678901234567890123456789012345678901234567890123456789012345678901234567890 +*/ + +#include +#include +#include +#include + +mrw::ConfigFileReader::Value& +mrw::ConfigFileReader::operator()(const std::string& section, + const std::string& name, + const std::string& default_) + throw(std::bad_exception) { + _values[section].insert(std::make_pair(name, Value(default_))); + return _values[section].find(name)->second; +} + +mrw::ConfigFileReader& mrw::ConfigFileReader::load(const std::string& filename) + throw(std::exception) { + _filename = filename; + return reload(); +} + +mrw::ConfigFileReader& mrw::ConfigFileReader::reload() throw(std::exception) { + return parse(readFile()); +} + +std::string mrw::ConfigFileReader::readFile() const throw(std::exception) { + std::string fileContents; // declaration as first allows optimization + std::ifstream file(_filename.c_str()); + if (!file) + throw mrw::invalid_argument("Error file not found: '"+_filename+"'"); + file.seekg(0, std::ios::end); + int size(file.tellg()); + fileContents.resize(size); // make buffer long enough to store all + file.seekg(0, std::ios::beg); + file.read(fileContents.begin().operator->(), size); // hack to get the buffer + if (!file.good() && file.eof()) + throw mrw::invalid_argument("Error reading file: '"+_filename+"'"); + return fileContents; +} + +#include +using namespace std; +mrw::ConfigFileReader& mrw::ConfigFileReader::parse(const std::string& file) + throw(std::exception) { + _values.erase(_values.begin(), _values.end()); + std::string section; + for (std::string::size_type pos(0); + (pos=file.find_first_of("[=#", pos))!=std::string::npos; ++pos) { + switch (file[pos]) { + case '[': _sections[section]=pos; section=parseSection(file, pos); break; + case '=': _values[section].insert(parseValue(file, pos)); break; + case '#': parseComment(file, pos); break; + }; + } + _sections[section]=file.size(); + return *this; +} + +std::string mrw::ConfigFileReader::parseSection(const std::string& file, + std::string::size_type& pos) + const throw(std::exception) { + std::string::size_type start(pos); + if ((pos=file.find(']', pos))==std::string::npos) + throw mrw::invalid_argument + ("section not terminated in file '"+_filename+"'\n" + "environment is: "+file.substr(start-10>0?start-10:0)); + return file.substr(start+1, pos-start-1); +} + +mrw::ConfigFileReader::Values::mapped_type::value_type +mrw::ConfigFileReader::parseValue(const std::string& file, + std::string::size_type& pos) + const throw(std::exception) { + // find the begin of the variable name + std::string::size_type startName(file.rfind('\n', pos)); + if (startName==std::string::npos) startName=0; + startName=file.find_first_not_of("\n\t ", startName); + if (startName==pos) + throw mrw::invalid_argument + ("empty variable name in file '"+_filename+"'\n" + "environment is: "+file.substr(startName-10>0?startName-10:0, pos+10)); + // find the end of the variable name + std::string::size_type endName(file.find_last_not_of("\n\t ", pos-1)+1); + // find the begin of the variable contents + std::string::size_type startVal(file.find_first_not_of("\n\t ", ++pos)); + if (startVal==std::string::npos) + return std::make_pair // end of file, empty value + (file.substr(startName, endName-startName), Value("", pos, pos)); + // find the end of the variable contents + std::string::size_type realStart(startVal); // doesn't cut the first quote + std::string::size_type endVal(file[startVal]=='"' ? // quoted with " + file.find('"', ++startVal) : + file[startVal]=='\'' ? // quoted with ' + file.find('\'', ++startVal) : + file.find_first_of("[=#", startVal)); // normal + if (endVal!=std::string::npos && file[endVal]=='=') + endVal=file.rfind('\n', endVal); // jump before following variable name + endVal = file.find_last_not_of("\n\t ", // remove trailing white spaces + endVal!=std::string::npos?endVal-1:endVal)+1; + std::string::size_type realEnd(endVal!=std::string::npos && // quotes? + (file[realStart]=='"' && file[endVal]=='"' || + file[realStart]=='\'' && file[endVal]=='\'') + ? endVal+1 : endVal); + if (endVal<=startVal) + return std::make_pair // empty value + (file.substr(startName, endName-startName), Value("", pos, pos)); + return std::make_pair // normal case + (file.substr(startName, endName-startName), + Value(file.substr(startVal, endVal-startVal), realStart, pos=realEnd)); +} + +std::string mrw::ConfigFileReader::parseComment(const std::string& file, + std::string::size_type& pos) + const throw(std::bad_exception) { + std::string::size_type start(pos); + pos = file.find('\n', pos); + return file.substr(start, pos-start); +} + +// GNU g++ prior to 3.4 does not implement covariant returns +#if __GNUC__ == 3 && __GNUC_MINOR__ < 4 +mrw::ConfigFileReader& +#else +mrw::ConfigFileWriter& +#endif +mrw::ConfigFileWriter::reload() throw(std::exception) { + return parse(_file=readFile()); +} + +mrw::ConfigFileWriter& mrw::ConfigFileWriter::save() throw(std::exception) { + if (_filename=="") return *this; + // work in changes + bool changed(false); + for (Values::iterator sec(_values.begin()); sec!=_values.end(); ++sec) + for (Values::mapped_type::iterator var(sec->second.begin()); + var!=sec->second.end(); ++var) + if (var->second.changed) { + var->second.changed = false; + changed = true; + // check if we need quoting + std::string value(var->second.value); + if (value.find_first_of("[=#")!=std::string::npos) + if (value.find('"')==std::string::npos) + value = '"'+value+'"'; + else if (value.find('\'')==std::string::npos) + value = '\''+value+'\''; + else + throw mrw::invalid_argument("Configuration value is not quotable: " + +value); + // recalculate all positions + if (var->second.start && + value.size() != var->second.end-var->second.start) { + int diff(value.size()-var->second.end+var->second.start); + for (Values::iterator sec2(_values.begin()); + sec2!=_values.end(); ++sec2) + for (Values::mapped_type::iterator var2(sec2->second.begin()); + var2!=sec2->second.end(); ++var2) + if (var2->second.start>var->second.end) { + var2->second.start += diff; + var2->second.end += diff; + } + for (Sections::iterator sec2(_sections.begin()); + sec2!=_sections.end(); ++sec2) + if (sec2->second>=var->second.end) sec2->second += diff; + } + // set the new value + if (var->second.start) // simple change + _file.replace(var->second.start, var->second.end-var->second.start, + value); + else if (_sections.find(sec->first)!=_sections.end()) // new value + _file.insert(_sections[sec->first], "\n"+var->first+" = "+value + +(_file[_sections[sec->first]]=='\n'?"":"\n")); + else { // even a new section + _file+="\n["+sec->first+"]\n"+var->first+" = "+value; + _sections[sec->first] = _file.size(); + } + } + if (!changed) return *this; + // save to file + std::ofstream file(_filename.c_str()); + if (!(file<<_file)) + throw mrw::invalid_argument("Cannot write file '"+_filename+"'"); + return *this; +} diff --git a/mrw/configfile.hpp b/mrw/configfile.hpp new file mode 100644 index 0000000..8ba0e5e --- /dev/null +++ b/mrw/configfile.hpp @@ -0,0 +1,362 @@ +/** @file + + $Id$ + + $Date$ + $Author$ + + @copy © Marc Wäckerlin + @license LGPL, see file COPYING + + $Log$ + Revision 1.1 2005/01/07 00:31:38 marc + initial version + + +*/ +#ifndef __MRW_CONFIGFILE_HPP__ +#define __MRW_CONFIGFILE_HPP__ + +#include +#include + +namespace mrw { + + /** @defgroup config Configuration File Handler + + Read configuration parameters from a file. + + @section configSyntax Configuration File Syntax + + @subsection configSyntaxSection Sections: [Section] + + Syntax (regular expression): + \\[([^[=#]*)\\] + + A configuration file consists of none or more sections. The + sections start with tag [Section Name], where + Section Name is the name of the section. + The default section has an empty name. + + @subsection configSyntaxVariables Variables: name = value + + Syntax (regular expression): + ^[^[\\n\\t ]*([^[=#\\n]]+)[\\n\\t ]*=[\\n\\t ]*([^[=#]]*)[\\n\\t ]* + or + ^[^[\\n\\t ]*([^[=#\\n]]+)[\\n\\t ]*=[\\n\\t ]*("[^"]]*")[\\n\\t ]* + or + ^[^[\\n\\t ]*([^[=#\\n]]+)[\\n\\t ]*=[\\n\\t ]*('[^']]*')[\\n\\t ]* + + In the section, there are variable definitions in the form of + name=value, where name is the name of + the variable and value is it's contents. White + spaces at the begin or end of the name and the value are + stripped. Whitespaces inside a name or value remain + unchanged. Please note that a variable name starts at the begoin + of line and ends before the equal sign, while the contents + starts after the equal sign and ends before the next token + starts. If you need a token inside a value, or a variable must + start or end with white spaces, you can enclose it in either + " or ' quotes. If a value is quoted + and the quoting is not terminated, then the contents is taken up + to the end of file. Please note that the same quote must not + occur inside the value. All possible tokens are: [, + # and =. All other characters are not + treated in a special way. + + @subsection configSyntaxComments Comments: # + + Comments start with # and end at the end of the + line. + + @subsection configSyntaxSpecial Special Characters \\n, [, ], #, =, ", ' + + I have chosen a syntax that is downwards compatible with the + common Unix configuration file syntax, but with a grammar as + loose as possible and with as few restrictions as + possible. Special characters have only special meanings in + certain contexts. In other contexts, they can be used with no + restrictions. + + The following characters are treated in a special way: + + - [ and ] enclose a section name. A + section name may contain any arbitrary character, including new + line and white spaces, except a ]. + + - \\n (begin of line or file) and = + enclose a variable name. In other circumstances (except in + comments), a new line has no special meaning. A = + inside a value is allowed only if the value is quoted. A + variable name may contain any arbitrary character, including + white spaces, except a new line or =. + + - = and one out of [, # + and the begin of a line that contains = enclose a + variable contents if the contents is not quoted. A variable's + contents may contain any arbitrary character, including new line + and white spaces, except a ], # or the + begin of a line that contains =. + + - " or ' may enclose a variables + contents. These characters have only a special meaning, if a + variable's contents starts with one of them. Then the variable's + contents may contain any arbitrary character, except the same + quote itself, which marks the end of the contents. + + - # and \\n (end of line or file) + enclose a comment. A variable name may contain any arbitrary + character, including white spaces, except a new line. + + @subsection configSyntaxRestrictions Restrictions + + @attention Due to the actual syntax, it is impossible that a + variable's contents contains the following combination: + " @b and ' @b and one out of + [, = and # + + @attention If a variable's contents contains heading or trailing + white spaces or one out of [, = and + #, then it must be quoted either with + " or with '. + + @section configFile Example File + + @subsection configFileSamp1 Nice Example + + @verbatim + [Section 1] + name1 = value1 + name2 = value2 + [Section 2] + name1 = value1 + name2 = value2 + @endverbatim + + @subsection configFileSamp2 Enhanced Example + + @verbatim + # this is in the global section, named "" + this is a variable name = this is a variable contents + + [Global] # start of section "Global" + + multi line text = this is a text + that is longer than one + line! + + 1234 = "var=17" # if you need an equal inside a text, enclose it + # either in " or in ' + @endverbatim + + @section configFileCode Code Sample + + @code + try { + const char* home(getenv("HOME")); + mrw::ConfigFileReader config(home?home+"/.myprogram":".myprogram"); + std::string userName(config("User Info", "Name")); + } catch (...) { // file read or evaluation error + } + @endcode + */ + //@{ + + /// Parse configuration file and access the contents. + /** Parse a configuration file that consists of sections containing + variables with values. This class does not store anything in the + file, even if variables have changed. + @see If you want %to save changes %to the file, use + @ref ConfigFileWriter. + @see For details, see @ref configSyntax. */ + class ConfigFileReader { + public: + /// A configuration file value. + class Value { + public: + /// Get the value string. + /** Get the value string. Trailing and leading white spaces have + been truncated. If the variable was quoted, quotes have been + removed. + @return the variable's value as string */ + operator const std::string&() const throw() { + return value; + } + /// Get the value string. + /** Alternative access to the variable's contents. Trailing and + leading white spaces have been truncated. If the variable + was quoted, quotes have been removed. + @return the variable's value as string */ + const std::string& operator()() const throw() { + return value; + } + /// Assign a new value. + /** Assign a new value. The new value is lost if you use the + ConfigFileReader, but it is stored in the destructor, if you + use the ConfigFileWriter instead. */ + Value& operator=(const std::string& newValue) throw() { + changed = true; + value = newValue; + return *this; + } + /// Compare the value to a string. + bool operator==(const std::string& o) const throw() { + return o==value; + } + /// Compare a string to the value. + friend bool operator==(const std::string& o, const Value& v) throw() { + return v==o; + } + protected: + friend class ConfigFileReader; + friend class ConfigFileWriter; + Value(const std::string& v, + std::string::size_type s, std::string::size_type e) throw(): + value(v), changed(false), start(s), end(e) { + } + Value(const std::string& v) throw(): + value(v), changed(true), start(0), end(0) { + } + std::string value; + bool changed; + std::string::size_type start; + std::string::size_type end; + private: + Value(); // not implemented, must be initialized + Value& operator=(const Value&); // not implemented, no assignment + }; + /// Uninitialized construction. + /** Uninitialized construction. Use this constructor, if you want + to call @ref load() later. */ + ConfigFileReader() throw() {} + /// Parse a file at construction. + /** This should be the normal case: load and parse the file at + construction. + @param filename the name of the file to read + @throw mrw::invalid_argument + - if the file does not exist + - if reading the file fails + - if there is an unterminated section name in the file + - if there is an empty variable name in the file */ + ConfigFileReader(const std::string& filename) throw(std::exception): + _filename(filename) { + reload(); + } + /// Copy construct from other ConfigFileReader. + ConfigFileReader(const ConfigFileReader& o) throw(): + _filename(o._filename), _values(o._values) { + } + virtual ~ConfigFileReader() throw() {} + /// Copy from other ConfigFileReader. + ConfigFileReader& operator=(const ConfigFileReader& o) { + _filename = o._filename; + _values = o._values; + return *this; + } + /// Access a value read from the file. + /** Access a value read from the file. + @param section the section in the configuration file + @param name the variable name + @param default_ the default value that is set, if the variable is + not configured + @return the Value you are looking for */ + Value& operator()(const std::string& section, const std::string& name, + const std::string& default_) + throw(std::bad_exception); + /// Load and parse a new file. + /** Load and parse a new file. + @throw mrw::invalid_argument + - if the file does not exist + - if reading the file fails + - if there is an unterminated section name in the file + - if there is an empty variable name in the file */ + ConfigFileReader& load(const std::string& filename) throw(std::exception); + /// Reload the last file again. + /** Reload the last file again. + @throw mrw::invalid_argument + - if the file does not exist + - if reading the file fails + - if there is an unterminated section name in the file + - if there is an empty variable name in the file */ + virtual ConfigFileReader& reload() throw(std::exception); + protected: + typedef std::map< std::string, std::map > Values; + typedef std::map Sections; + std::string readFile() const throw(std::exception); + ConfigFileReader& parse(const std::string& file) throw(std::exception); + std::string parseSection(const std::string& file, + std::string::size_type& pos) + const throw(std::exception); + Values::mapped_type::value_type parseValue(const std::string& file, + std::string::size_type& pos) + const throw(std::exception); + std::string parseComment(const std::string& file, + std::string::size_type& pos) + const throw(std::bad_exception); + std::string _filename; + Values _values; + Sections _sections; + }; + + /** @brief Parse configuration file and offer read / write + access to the contents. + + Parse a configuration file that consists of sections containing + variables with values. This class stores the file in the destructor + if variables have changed. + @see If you want %to leave the file untouched, use + @ref ConfigFileReader. + @see For details, see @ref configSyntax. */ + class ConfigFileWriter: virtual public ConfigFileReader { + public: + /// Uninitialized construction. + /** @copydoc ConfigFileReader() */ + ConfigFileWriter() throw() {} + /// Parse a file at construction. + /** @copydoc ConfigFileReader(const std::string& filename) */ + ConfigFileWriter(const std::string& filename) throw(std::exception){ + load(filename); + } + /// Copy construct from other ConfigFileWriter. + ConfigFileWriter(const ConfigFileWriter& o) throw(): + ConfigFileReader(o) { + } + /// The file is stored at destruction. + virtual ~ConfigFileWriter() throw() { + save(); + } + /// Copy from other ConfigFileWriter. + ConfigFileWriter& operator=(const ConfigFileWriter& o) { + ConfigFileReader::operator=(o); + return *this; + } +// GNU g++ prior to 3.4 does not implement covariant returns +# if __GNUC__ == 3 && __GNUC_MINOR__ < 4 + /// Reload the last file again. + /** @copydoc mrw::ConfigFileReader::reload() + @bug Bug in GNU g++ compiler: + return type should be ConfigFileWriter&, + but the compiler sais:
+ ../mrw/configfile.hpp:295: + sorry, unimplemented: adjusting pointers for + covariant returns
+ The problem is fixed since GNU g++ 3.4, so the signature + will be changed as soon as you upgrade to the new compiler + version. */ + virtual ConfigFileReader& reload() throw(std::exception); +# else + /// Reload the last file again. + /** @copydoc mrw::ConfigFileReader::reload() */ + virtual ConfigFileWriter& reload() throw(std::exception); +# endif + /// Saves changes back to the file. + /** All changed parameters are stored in the configuration + file. The rest of the file is left untouched. */ + ConfigFileWriter& save() throw(std::exception); + protected: + std::string _file; + }; + + //@} +} +#endif diff --git a/mrw/configfile.ini b/mrw/configfile.ini new file mode 100644 index 0000000..4c01846 --- /dev/null +++ b/mrw/configfile.ini @@ -0,0 +1,16 @@ + xxx=yyy # this is a comment +[Section] + + abc= + def=hallo welt + ghi= + jkl= mn + op qr + st + + [Other Section] + + 1234="5678=90" + +here we are = some contents # comments + here= \ No newline at end of file diff --git a/mrw/configfile.ini.result b/mrw/configfile.ini.result new file mode 100644 index 0000000..0fdd5ad --- /dev/null +++ b/mrw/configfile.ini.result @@ -0,0 +1,24 @@ + xxx=0 # this is a comment +[Section] + + abc=1 + def="Und=Tschuess" + ghi=3 + jkl= 4 + + +guguseli zwei = dadaa +guguseli drei = dadaa +guguseli = dadaa +[Other Section] + + 1234=5 + +here we are = 6 # comments + here=7 +yes = . +no no no = . +no no = . +[New Section] +a first one = sgadd +one more = . diff --git a/mrw/configfile_check.sh b/mrw/configfile_check.sh new file mode 100755 index 0000000..5fc004f --- /dev/null +++ b/mrw/configfile_check.sh @@ -0,0 +1,3 @@ +#! /bin/bash + +test -z "`diff configfile2.ini configfile.ini.result`" && rm configfile2.ini diff --git a/mrw/configfile_test.cpp b/mrw/configfile_test.cpp new file mode 100644 index 0000000..95ae1ef --- /dev/null +++ b/mrw/configfile_test.cpp @@ -0,0 +1,66 @@ +/** @file + + $Id$ + + $Date$ + $Author$ + + @copy © Marc Wäckerlin + @license LGPL, see file COPYING + + $Log$ + Revision 1.1 2005/01/07 00:31:38 marc + initial version + + +*/ + +#include +#include +#include +#include +#include +#include + +class ConfigFileTest: public CppUnit::TestFixture { +public: + void CheckFile() { + mrw::File::copy("configfile.ini", "configfile2.ini"); + mrw::ConfigFileWriter config("configfile2.ini"); + CPPUNIT_ASSERT(config("", "xxx", ".")=="yyy"); + CPPUNIT_ASSERT(config("Section", "abc", ".")==""); + CPPUNIT_ASSERT(config("Section", "def", ".")=="hallo welt"); + CPPUNIT_ASSERT(config("Section", "ghi", ".")==""); + CPPUNIT_ASSERT(config("Section", "jkl", ".")=="mn\n op qr\n st"); + CPPUNIT_ASSERT(config("Other Section", "1234", ".")=="5678=90"); + CPPUNIT_ASSERT(config("Other Section", "here we are", ".") + =="some contents"); + CPPUNIT_ASSERT(config("Other Section", "here", ".")==""); + config("", "xxx", ".")="0"; + config("Section", "abc", ".")="1"; + CPPUNIT_ASSERT(config("New Section", "a first one", "sgadd")=="sgadd"); + config("Section", "def", ".")="Und=Tschuess"; + config("Section", "ghi", ".")="3"; + config("Section", "jkl", ".")="4"; + config("Other Section", "1234", ".")="5"; + config("Other Section", "here we are", ".")="6"; + config("Other Section", "here", ".")="7"; + CPPUNIT_ASSERT(config("Other Section", "no no", ".")=="."); + CPPUNIT_ASSERT(config("Other Section", "no no no", ".")=="."); + CPPUNIT_ASSERT(config("Other Section", "yes", ".")=="."); + CPPUNIT_ASSERT(config("Section", "guguseli", "dadaa")=="dadaa"); + CPPUNIT_ASSERT(config("Section", "guguseli zwei", "dadaa")=="dadaa"); + CPPUNIT_ASSERT(config("Section", "guguseli drei", "dadaa")=="dadaa"); + CPPUNIT_ASSERT(config("New Section", "one more", ".")=="."); + } + CPPUNIT_TEST_SUITE(ConfigFileTest); + CPPUNIT_TEST(CheckFile); + CPPUNIT_TEST_SUITE_END(); +}; +CPPUNIT_TEST_SUITE_REGISTRATION(ConfigFileTest); + +int main() { + CppUnit::TextUi::TestRunner runner; + runner.addTest(CppUnit::TestFactoryRegistry::getRegistry().makeTest()); + return runner.run() ? 0 : 1; +} diff --git a/mrw/file.hpp b/mrw/file.hpp new file mode 100644 index 0000000..9e7248e --- /dev/null +++ b/mrw/file.hpp @@ -0,0 +1,47 @@ +/** @file + + $Id$ + + $Date$ + $Author$ + + @copy © Marc Wäckerlin + @license LGPL, see file COPYING + + $Log$ + Revision 1.1 2005/01/07 00:31:38 marc + initial version + + + 1 2 3 4 5 6 7 8 + 5678901234567890123456789012345678901234567890123456789012345678901234567890 +*/ +#ifndef __MRW_FILE_HPP__ +#define __MRW_FILE_HPP__ + +#include +#include +#include + +namespace mrw { + class File { + public: + static void copy(std::string from, std::string to) throw (std::exception) { + std::ifstream is(from.c_str()); + if (!is) + throw mrw::invalid_argument("Cannot read file: '"+from+"'"); + is.seekg(0, std::ios::end); + int size(is.tellg()); + std::string contents(size, 0); // make buffer long enough to store all + is.seekg(0, std::ios::beg); + is.read(contents.begin().operator->(), size); // hack to get the buffer + if (!is.good() && is.eof()) + throw mrw::invalid_argument("Cannot read file: '"+from+"'"); + std::ofstream os(to.c_str()); + os<COPYING $Log$ + Revision 1.2 2005/01/07 00:35:17 marc + initial version + Revision 1.1 2004/12/17 16:26:58 marc initial version @@ -24,7 +27,6 @@ #include #include -#include class TokenizerTest: public CppUnit::TestFixture { public: void CheckNonGreedy() {