This guide walks through building the smallest useful cparse grammar: a comma-separated integer list, like 1, 2, 30, 400. The example fits on one page and touches every core concept — deriving from Parser, driving the cursor, extracting tokens, recording errors — without introducing anything you can defer to the User Guide.

If you have not built cparse yet, see Installation first.

What cparse gives you

cparse is not a parser. It is the reusable driver for a parser. You write the grammar; cparse gives you a cursor to read the input, a mechanism to save and restore that cursor for backtracking, a way to collect errors with file:line:col locations, and — if you also generate code — a small set of case-conversion and indentation helpers.

Concretely, you get four public classes:

ClassWhat it does
CursorA read-only stream position with get/peek, save/restore, and automatic line/column tracking.
ParserThe abstract driver. Subclass it, override start(), and use the protected helpers to consume input and record errors.
GeneratorBase class for code generators. Provides case conversion (snake ↔ camel), header-guard generation, C++ string encoding.
IndentationA std::ostream manipulator for step-based indentation in generated code.

All four live in namespace fedem::parser. This guide only uses the first two.

The whole program

Here is a complete grammar for a comma-separated integer list. Read it through once, then we will unpack each piece.

#include <cparse/Parser.hh>
#include <cctype>
#include <iostream>
#include <vector>

class IntList : public fedem::parser::Parser
{
public:
    std::string getGrammarName() const noexcept override
    {
        return "int-list";
    }

    std::vector<int> const& values() const noexcept { return values_; }

protected:
    bool start() override
    {
        skipWhiteSpaces();
        if (!readInt()) return true;             // empty input is valid

        while (true)
        {
            skipWhiteSpaces();
            if (!extractCharacter(',')) return true;   // end of list
            skipWhiteSpaces();
            if (!readInt())
            {
                recordError(ParseError::Kind::Syntax,
                            "expected integer after ','",
                            getCursor().getLineNumber(),
                            getCursor().getColumnNumber());
                return false;
            }
        }
    }

private:
    bool readInt()
    {
        std::string digits;
        while (std::isdigit(getCursor().safePeek()))
            digits += getCursor().safeGet();

        if (digits.empty()) return false;
        values_.push_back(std::stoi(digits));
        return true;
    }

    std::vector<int> values_;
};

int main(int argc, char** argv)
{
    if (argc != 2) { std::cerr << "usage: intlist <file>\n"; return 2; }

    IntList p;
    bool const ok = p.parse(argv[1]);

    for (auto const& e : p.errors())
        std::cerr << e.filename << ':' << e.line << ':' << e.col
                  << ": " << e.message << '\n';

    if (!ok) return 1;

    for (int v : p.values()) std::cout << v << '\n';
    return 0;
}

Given a file containing 1, 2, 30, 400, this prints one integer per line. Given 1, 2, oops, it exits with 1 and reports:

input.txt:1:6: expected integer after ','

Reading the grammar

Deriving from Parser. You override two things: getGrammarName(), a diagnostic label used in trace output, and start(), the entry point called once per parse.

start() returns bool. true means the input was accepted; false means a fatal grammar error. Non-fatal errors (warnings, for instance) go through recordError() and do not affect the return value.

The cursor is protected. Inside start() you have getCursor() and a family of extractToken() / extractCharacter() helpers that consume input if it matches. getCursor().peek() and .get() throw at end-of-input; the safeGet / safePeek variants return the end-of-input sentinel \032 instead. Use the safe variants inside your own loops.

skipWhiteSpaces() and skipComments() are provided by the base class with sensible defaults. Override them if your grammar has a different notion of whitespace (Python's significant indentation, for example, or nested block comments).

Errors go into a collector. recordError() appends to a std::vector<ParseError> that the caller reads via parser.errors(). Each entry carries a Kind (Syntax / Type / Faulty / Internal / Warning), a message, and file/line/column coordinates. See ParseError for the enum details.

Fatal errors call exit(). Inside the parser, exit(code) throws a private exception caught by parse(), which then returns false. Use it when the grammar has reached a state from which no recovery makes sense.

Trying it

Compile the program above against a system install of cparse:

g++ -std=c++20 intlist.cpp $(pkg-config --cflags --libs cparse) -o intlist

echo "1, 2, 30, 400"  > input.txt
./intlist input.txt

echo "1, 2, oops"     > input.txt
./intlist input.txt   # error goes to stderr, exit code 1

The test suite in the source tree (test/ParserTests.cpp) contains several small grammars in this style and is a useful reference for patterns not covered here.

  • Grammar Patterns — cursor save/restore for backtracking, enterScope/leaveScope for nested constructs, how to parse from a string instead of a file (pushCursor with an istringstream).
  • Code Generation — using Generator and Indentation to write out C++ (or any other language) from the AST you built in start().
  • Reference › Parser — the full list of protected helpers.
  • FAQ — common pitfalls, especially around exception handling and EOF detection.