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:
| Class | What it does |
|---|---|
Cursor | A read-only stream position with get/peek, save/restore, and automatic line/column tracking. |
Parser | The abstract driver. Subclass it, override start(), and use the protected helpers to consume input and record errors. |
Generator | Base class for code generators. Provides case conversion (snake ↔ camel), header-guard generation, C++ string encoding. |
Indentation | A 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.
What to read next
- Grammar Patterns — cursor save/restore for backtracking,
enterScope/leaveScopefor nested constructs, how to parse from a string instead of a file (pushCursorwith anistringstream). - Code Generation — using
GeneratorandIndentationto write out C++ (or any other language) from the AST you built instart(). - Reference › Parser — the full list of protected helpers.
- FAQ — common pitfalls, especially around exception handling and EOF detection.

