For smaller files, I like to load the entire thing, split it into statements, and then split each statement into tokens like this:
std::vector<std::string> lines = SplitString(LoadFileAsString("blah.txt"), '\n', IgnoreEmptyLines);
std::unordered_map<std::string, std::string> configMap;
for(const std::string &line : lines)
{
//Ignore comments.
if(line.front() == '#') continue;
//Split line into tokens.
std::vector<std::string> tokens = SplitString(line, '=');
if(tokens.size() == 2)
{
configMap[tokens[0]] = tokens[1];
}
else
{
//Parse error or different line format...
}
}
Which can then be wrapped into its own function:
std::unordered_map<std::string, std::string> configMap = LoadFileAsStringMap("config.txt", '\n', '=');
That's just for basic key-value pairs. If I wanted more complicated structures, I'd take the same basic idea (breaking up the logic into smaller re-usable functions), but instead I treat the entire file as one string, breaking up by whitespace, and I leave the equals and even the newlines in as tokens, and treat the file as one continuous string of tokens.
Then you walk through the entire set of tokens, one by one, but keep track of the current state ("I'm inside of an object called 'MyObject', I'm nested four deep, this variable is named 'HP', etc..."), which lets you detect "wrong" things. For example, if you reach the end of the list of tokens but 'state' is still in a New Object, then you've clearly forgotten an 'End', and need to report it as a syntax error. Likewise if you're inside a New Object, and encounter another New Object, then you either report a syntax error (or interpert it to mean to nest one object inside the other, if your format requires that behavior).