Question

So I need some ideas on how nicely parse a text file in C++. The files that I am parsing have the following format :

  Command_A  list of arguments
  Command_B  list of arguments
  etc etc

Right now I am using an ifstream to open up the file and then I have this super long series of if-else statements to determine what to do for each type of command. This is proving to be a bit unwieldy (especially since some of the commands are for parsing other files...so I have nested if-elses with multiple ifstreams for the different files).

I was looking for another way of doing this but am not really sure what is the best approach. I was thinking about using a std::map where the keys are the command strings and the values are the function pointers but I am not familiar with storing function pointers in a map (especially if the different functions are of different return types, etc).

Below is basically what I am currently doing. I loop through the file and use "getline" to get the current line. Then I use a stringstream to parse the command. Then I use a very long list of if-elses to determine which function to call. Each line in the file also comes with a list of arguments so I use the stringstream to parse those and then pass those parameters into the function I call.

The problem here is two-fold

1) I have a very very large number of if-elses (around 50)

2) Some of the commands require me to parse new files and thus I have to open up another ifstream within the current ifstream. (see command_c)

So I'm looking for an easier/more efficient/prettier looking way to do this.

/*Open file and verify validity*/
std::ifstream parseFile(filename.c_str());
if(!parseFile.good())
{
    cerr<<"ERROR: File is either corrupt or does not exist."<<endl;
    exit(1); //Terminate program
}

//Loop over file line by line
std::string line;
while(!parseFile.eof())
{
    std::getline(parseFile, line);
    std::stringstream ss;
    std::string command;
    ss.str(line);
    ss >> command;

    if(command == "COMMAND_A")
    {
         float x,y,z;
         ss >> x >> y >> z;

         FunctionA(x,y,z);
    }
    else if(command == "COMMAND_B")
    {
        float a,b,c,d,e,f;
        ss >> a >> b >> c >> d >> e >> f;

        FunctionB(a,b,c,d,e,f);
    } 
    else if(command == "Command_C")
    {
        string nextFile;
        ss >> nextFile;

        ParseFile(nextFile); //This is not recursive...this is another function
    }
    else if(...)
    {
      ...
    }

   //  etc, etc (this continues on for a long time)
  }
parseFile.close();
Was it helpful?

Solution

You could have a command map and register a bunch of functions:

#include<fstream>
#include<functional>
#include<iostream>
#include<map>
#include<sstream>

int main() {

    typedef std::function<bool (std::istringstream&)> command_function;
    typedef std::map<std::string, command_function> command_map;

    command_map map;

    // register commands
    map.insert(command_map::value_type("print", [](std::istringstream& s) {
        std::string line;
        if( ! getline(s, line)) return false;
        std::cout << line << '\n';
        return true;
    }));

    map.insert(command_map::value_type("add", [](std::istringstream& s) {
        double a;
        double b;
        if( ! (s >> a >> b)) return false;
        std::cout << "a + b = " << a + b  << '\n';
        return true;
    }));

    // sample data
    std::istringstream file(
        "print Hello World\n"
        "add 1 2\n");

    // command parsing
    std::string line;
    while(getline(file, line)) {
        std::istringstream line_stream(line);
        std::string command;
        if(line_stream >> command >> std::ws) {
            auto pos = map.find(command);
            if(pos != map.end())
                pos->second(line_stream);
        }
    }
    return 0;
}

OTHER TIPS

I've written many types of parsers, and I find that it's often a good idea to write a fairly generic function that takes a line and produces a list of strings (e.g. std::vector<std::string>, and then process the first element in that list as a "what do we do next", and let each command use the arguments as it likes (e.g. translate to float, use as a filename, etc).

This can then be combined with a table-based system, where a function [or object] is associated with the string. For example std::map<std::string, BaseCommand> table;.

Then you end up with something like this:

class CommandA : BaseCommand
{
public:
    virtual int Run(const std::vector<std::string>& argv);
};

table["CommandA"] = new CommandA;
table["CommandB"] = new CommandB;
... 

std::vector<std::string> argv = parseLine(line); 
if (table.find(argv[0]) != table.end())
{
    int result = table[argv[0]].second->Run(argv);
    if (result < 0)
    {
        ... do error handling here... 
    }
}

Of course, there are many different ways you COULD do this, and this is just one possible solution.

Yes, put the functions in a map. The key to doing this is std::function<void()>. Unfortunately, the void() means it holds functions that take no parameters, and return nothing. Obviously, your functions have parameters. So what we do, is store functions that each take a std::stringstream& (the line), parse out the parameters they need, and then call the function. The easiest way to do this is simply to use inline lambdas. Lambdas that take stringstreams and return nothing look like this: [](std::stringstream& ss) {code}.

Additionally, I use this function for easy retrieving of your parameters:

template<class T>
T get(std::stringstream& ss) 
{
    T t; 
    ss<<t; 
    if (!ss) // if it failed to read
        throw std::runtime_error("could not parse parameter");
    return t;
}

Here's the map:

std::unordered_set<std::string, std::function<void(std::stringstream&))> cmd_map= 
    "COMMAND_A", [](std::stringstream& ss)
        {FunctionA(get<float>(ss), get<float>(ss), get<float>(ss));},
    "COMMAND_B", [](std::stringstream& ss)
        {FunctionB(get<float>(ss), get<float>(ss), get<float>(ss), get<float>(ss), get<float>(ss), get<float>(ss));},
    "COMMAND_C", [](std::stringstream& ss)
        {FunctionA(get<string>(ss));},

And here's the parser:

//Loop over file line by line
std::string line;
while(std::getline(parseFile, line)) //use this instead of eof
{
    std::stringstream ss(line);
    std::string command;
    ss >> command;

    auto it = cmd_map.find(command);
    if (it != cmd_map.end())
    {
        try 
        {
            (*it)(); //call the function
        } catch(std::runtime_error& err) {
            std::cout << "ERROR: " << err.what() << '\n';
        }
    } else {
        std::cout << "command " << command << " not found";
    }
}
parseFile.close();
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top