How can I parse a bracketed string to a list of string with a given delimitor

Viewed 157

My task is to parse a bracketed string, like

[foo | bar | foobar], to a vector of std::strings.

In this case, the vector should end up with the contents {"foo" , "bar", "foobar"}.

These brackets can be nested. For example, the given bracketed string

[[john | doe] | [ bob | dylan]]

would become { "[john | doe]" , "[bob | dylan] }"

The best I could manage so far is

int main(int argc, char ** argv)
{
    const std::string input {argv[1]};
    std::vector<std::string> res;

    qi::phrase_parse(input.cbegin(), input.cend(),
     '[' 
     >> *qi::lexeme[ +(qi::char_ - '|')  >> '|'] 
     > -qi::lexeme[ +(qi::char_  - ']') >> ']' ],
     qi::space ,
     res);

    for (const auto& v: res)
        std::cout << v  <<std::endl;

    return 0;
}

which fails miserably for the nested case. Can somebody please point me in the right direction?

Note #1: Nested cases can be more than one.

Note #2: I welcome any simpler solutions, even without using Boost Spirit.

3 Answers

Here's a simple C++ parser based on the assumption that brackets are balanced, i.e. every [ has a ] there.

bracket is the number of opening brackets. We make crucial decisions when that number is 1.

#include <iostream>
#include <vector>
#include <string>
#include <string_view>

bool edge(const int num){
    return num == 1;
}

int main(){
    std::vector<std::string> all;
    std::string line;
    // std::getline(std::cin, line);
    line = "[[john | doe] | [ bob | dylan]]";

    int bracket = 0;
    std::string::size_type start = 0;
    for(int i = 0; i < line.size(); i++){
        const char c = line[i];
        if(c == '['){
            bracket++;
            if(edge(bracket)){
                start = i + 1;
            }
        }
        if(c == ']'){
            if(edge(bracket)){
                all.push_back(line.substr(start, i - start));
            }
            bracket--;
        }
        if(c == '|' && edge(bracket)){
            all.push_back(line.substr(start, i - start));
            start = i + 1;
        }
    }
    for(std::string_view t : all){
        std::cout << t << std::endl;
    }
}

If you want nested lists of strings, first you'll need a result that can store nested lists. Luckily, in C++17 you can have vectors of forward references (as long as their defined at some point). So you can make a type that is a list, where every item is either a string or another list:

struct Expr : std::vector<
    boost::variant<
        std::string,
        Expr>>
{ 
    using std::vector<boost::variant<std::string, Expr>>::vector;
};

After the grammar is pretty simple. Note that it's recursive - Term can have an Expr nested into it:

WORD = /[^\[\|\]]+/
Term = WORD | Expr
Expr = '[' Term ('|' Term)* ']';

You can express each rule separately. Boost Spirit Qi conveniently has the % operator, which parses a delimited list and inserts it into a container.

using It = std::string::const_iterator;
using Sk = qi::space_type;
qi::rule<It, std::string(), Sk> word;
qi::rule<It, boost::variant<std::string, Expr>(), Sk> term;
qi::rule<It, Expr(), Sk> expr;

word = +(qi::char_ - '[' - '|' - ']'); 
term = word | expr;
expr = '[' >> (term % '|') >> ']';

Then qi::phrase_parse will do what you want:

Expr res;
qi::phrase_parse(input.cbegin(), input.cend(), expr, qi::space, res);

Demo: https://godbolt.org/z/5W993s

This simpler version seems to be what you want:

qi::phrase_parse(input.cbegin(), input.cend(),
    '[' 
    >> qi::lexeme[ +~qi::char_("|]") ] % '|' 
    >> ']',
    qi::space,
    res);

It will parse:

"foo "
"bar "
"foobar"

Maybe you didn't actually want the spaces as part of the matches. Then it can be even simpler:

qi::phrase_parse(input.cbegin(), input.cend(),
    '[' 
    >> qi::lexeme[ +(qi::graph - qi::char_("|]")) ] % '|' 
    >> ']',
    qi::space,
    res);

See it Live On Coliru

Note: if you have C++14 consider using X3: Live On Coliru. That will be a lot faster to compile

Related