All configuration methods in Avida involve reading information from a file. When we examined events, we didn't have to worry about dealing with the file because all of that source code was automatically generated for us. It is not critical to fully understand the file system if you are not going to directly use it, but it will aid you in putting other objects into perspective.
The cFile class is a base class that handles opening a file, closing it, and reading from it one line at a time. The cInitFile class extends cFile (that is, it is derived from cFile) adding special functions to load the file into memory, remove comments from it, and search through it for keywords. Finally, the cGenesis class further extends cInitFile, including new functions that assume each line of the file has the format "KEYWORD VALUE" to allow the programmer to pass in a keyword and get back a value of the appropriate form.
All three of these classes are defined in the files "file.hh" and "file.cc", located in the directory "current/source/tools/". See also the cDataFile class, which is more focused on writing to a file than reading from it. Here, we'll step through each cFile-based class in order.
This is the fundamental file loading class. Here is a mildly edited version of its class declaration:
class cFile { protected: fstream fp; // An input stream associated with this file. cString filename; // The name of the file we're working with. bool is_open; // Have we successfully opened this file? bool verbose; // Should file be verbose about warnings to users? public: cFile(cString _fname) : filename(""), is_open(false) { Open(_fname); } ~cFile() { if (is_open == true) fp.close(); filename = ""; } // Manipulators bool Open(cString _fname, int mode=(ios::in|ios::nocreate)); bool Close(); bool ReadLine(cString & in_string); // Tests bool IsOpen() const { return is_open; } bool Good() const { return (fp.good()); } bool Eof() const { return (fp.eof()); } // Accessors void SetVerbose(bool _v=true) { verbose = _v; } const cString & GetFilename() const { return filename; } };
To use this class, you create a cFile object and pass in a filename. This will automatically run Open() on the file. At any point thereafter you have the option to Close() the file and open a new one. The IsOpen() method allows you to test if a file is currently open, and the Good() method tests if there are any problems with the file (i.e., it was deleted out from under the user.)
You can obtain one line at a time from an open file by using the ReadLine() method. When you call this method, you must give it a string that the method will modify. The method attempts to copy the next line in the file into that string, and then the method returns true or false indicating if the copy was successful. Failure (a return value of false) typically indicates that the user is at the end of the file, or something about the file has failed.
As an example of what a method description looks like, here is the code body for the Open() method:
bool cFile::Open(cString _fname, int flags) { if ( IsOpen() ) Close(); // If a file is already open, close it first. fp.open(_fname(), flags); // Open the new file. // Test if there was an error, and if so, try again! int err_id = fp.fail(); if( err_id != 0 ){ fp.clear(); fp.open(_fname(), flags); } // If there is still an error, determine its type and report it. err_id = fp.fail(); if ( err_id != 0 ){ cString error_desc = "?? Unknown Error??"; // See if we can determine a more exact error type. if (err_id == EACCES) error_desc = "Access denied"; else if (err_id == EINVAL) error_desc = "Invalid open flag or access mode"; else if (err_id == ENOENT) error_desc = "File or path not found"; // Print the error. cerr << "Unable to open file '" << _fname << "' : " << error_desc << endl; return false; } filename = _fname; is_open = true; // Return true only if there were no problems... return( fp.good() && !fp.fail() ); }
To specify a method (when not actively running it on an object of the appropriate class, as when we're defining it), the format used is "cClassName::MethodName()". While this method opens the file, it checks each step of the way for potential problems. If there is a problem, it tries to fix it and, failing that, simply reports it to the user. My goal in including this method here is merely to give you an idea of what such a method looks like.
The cFile class alone allows a user to open a file and collect information from it one line at a time, but that's it. There are no helpful functions that enable us to accomplish specific goals. The cInitFile class, however, builds on top of cFile, and gives us methods that will be useful for a variety of initialization files.
Here is an edited version of its declaration:
class cInitFile : public cFile { private: cStringList line_list; public: cInitFile(cString in_filename); ~cInitFile(); void Load(); // Load the file into memory so we can manipulate it. void Compress(); // Remove all Comments and Whitespace. void AddLine(cString & in_string); // Add a line to beginning of memory. cString RemoveLine(); // Remove first line from memory. cString GetLine(int line_num=0); // Get specified line from memory. int GetNumLines() const { return line_list.GetSize(); } // Find a keyword in the specified column. Stop at first occurrence and // set in_string to the full line it was found in. bool Find(cString & in_string, const cString & keyword, int col) const; // Find an entry the keyword in first column. Return the *remainder* of // the line. If not found, just return the default value given. cString ReadString(const cString & keyword, cString def="") const; };
The cInitFile class has a cStringList data member called line_list that can contain a collection of "strings", in this case lines loaded from the file in question. The Load() method is the one that copies all of the lines from the file into line_list, and then the Compress() method will remove all comments (in this case, anything after a # on a line), compress all sequential spaces and tabs down to a single space, and completely remove any lines that then have nothing left on them. The AddLine() and RemoveLine() methods allow you to directly change the contents of memory, while, GetLine() allows you to retrieve a line at a specified position. The next method in this group is GetNumLines(), which is exactly what it sounds -- it will return the number of lines currently in memory.
Finally, we have a pair of methods that allow us to search through the memory in a more useful manner. The Find() method seeks out a keyword in a specific column from the loaded file. It returns true or false depending on if that keyword was found, and it will fill out the input variable in_string with the contents of the full line that the keyword was found in. The other method, ReadString(), is only a slight variation on Find(). It takes a keyword, searches for it as the first word on any line in the file, and then returns the remainder of the line. If the keyword was nowhere to be found in the file, it will instead return a default value specified by the user. Below is the definition of these two functions, as found in file.cc.
bool cInitFile::Find(cString & in_string, const cString & keyword, int col) const { // Loop through all of the lines until we find our keyword. cStringIterator list_it(line_list); while ( list_it.AtEnd() == false ) { list_it.Next(); cString cur_string = list_it.Get(); if (cur_string.GetWord(col) == keyword) { in_string = cur_string; return true; } } return false; // Not Found... } cString cInitFile::ReadString(const cString & name, cString def) const { // See if we definitely can't find the keyword. if (name == "" || IsOpen() == false) return def; // Search for the keyword. cString cur_line; if ( Find(cur_line, name, 0) == false ) { if (verbose == true) { cerr << "Warning: " << name << " not in \"" << GetFilename() << "\", defaulting to: " << def << endl; } return def; } // Pop off the keyword, and return the remainder of the line. cur_line.PopWord(); return cur_line; }
This ReadString() method is one of the main ones that we use when dealing with the genesis file. In order to find the value of a specific setting in genesis, we can run ReadString() with the name of that setting as a keyword, and its value will be returned. We can specify a default for everything in case the user does not fill out genesis fully. This will all be explained in more detail in the next document.
The cGenesis class extends cInitFile to add additional functionality that is specific to reading in the genesis file. It does not contain new data, but it does have new methods. Here is a simplified version of the class declaration:
class cGenesis : public cInitFile { public: cGenesis(const cString & _fname); ~cGenesis(); // This Open command will run Open() from cInitFile, but then it will // also automatically Load() and Compress() the contents. int Open(cString _fname, int mode=(ios::in|ios::nocreate)); // Functions to add more data to a loaded genesis file (they use AddLine()) void AddInput(const cString & in_name, int in_value); void AddInput(const cString & in_name, const cString & in_value); // Functions to read in integer or floating point values set in genesis. int ReadInt(const cString & name, int base=0, bool warn=true) const; double ReadFloat(const cString & name, float base=0.0, bool warn=true) const; };
In this class, the Open() method overloads the one from cInitFile such that it will automatically also call cInitFile::Load() and cInitFile::Compress() inside of it.
The two AddInput() methods are used to add new values to the memory image of the genesis file that will take precedence over any loaded directly from the file. This is made use of when the user activates Avida with a command line option that has higher priority than a genesis setting. Note that even though both of these methods go by the identical name, C++ knows which one you are intending to call by the variables passed into it. In this case, either an integer or a string will be used as the second argument.
Finally, the ReadInt() and ReadFloat() methods are used to obtain settings from the genesis file that are in integer or floating point form respectively. All these really do are to call cInitFile::ReadString() and then convert the resulting string into the desired type.
So how do we use this cGenesis class to load in and make use of
the initialization file by the same name? For that, you'll need to read
about the cConfig class in the next document.