
cparse is a small C++20 library for writing hand-written recursive-descent parsers — and the code generators that often come with them. It is not a parser generator: you write the grammar as ordinary C++, one method per rule, and cparse supplies the parts every such parser needs.
Class (fedem::parser) | What it gives you |
|---|---|
Cursor | A read position over a file or stream: get / peek (throwing and safe… variants), automatic line / column tracking, store / restore for backtracking. |
Parser | The abstract driver. Override start() (your grammar's entry rule) and getGrammarName(); use the protected helpers — extractToken, extractCharacter, skipWhiteSpaces, skipComments, recordError, nested inputs, scope tracking for truncated input. |
Generator | Base for code generators: snake ↔ camel case conversion, header guards, namespace scopes, C++ string encoding. |
Indentation | A std::ostream manipulator for step-based indentation of generated code. |
Documentation, a reference manual and walkthroughs: <https://cparse.fedem.eu>.
A grammar for a comma-separated list of integers — 1, 2, 30, 400:
#include <cparse/Parser.hh>
#include <cctype>
#include <iostream>
#include <vector>
using fedem::parser::ParseError;
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 the list
skipWhiteSpaces( );
if( !readInt( ) )
{
recordError( ParseError::Kind::Syntax, "expected an integer after ','" );
return false;
}
}
}
private:
bool readInt( )
{
std::string digits;
while( std::isdigit( static_cast<unsigned char>( 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 1; }
IntList list;
bool const ok = list.parse( argv[1] );
for( auto const &e : list.errors( ) )
std::cerr << e.filename << ':' << e.line << ':' << e.col << ": " << e.message << '\n';
if( !ok ) return 1;
for( int v : list.values( ) ) std::cout << v << '\n';
}
$ ./intlist list.txt # "1, 2, x"
list.txt:1:7: expected an integer after ','
list.txt:1:7: unexpected file parsing result
parse() opens the file, runs start() and returns false if the grammar failed; errors() holds every diagnostic with its file:line:col (a failed start() also adds a generic "unexpected file parsing result" record after the grammar's own).
Without an explicit position, recordError() points at the character the grammar stopped at — here the x. To point at text already consumed (the start of a token, an opening bracket), save getCursor().getNextLineNumber() / getNextColumnNumber() before consuming it and pass them in.
ParseError lives in the namespace, not in Parser — qualify it, or bring it in with a using, as above.
The source tree has three complete examples, each with a smoke test:
examples/ | |
|---|---|
toy-calc | an arithmetic expression evaluator — precedence, parentheses, one expression per line of standard input |
key-value | an INI-style [section] / key = value reader with # comments |
csv-lite | a streaming CSV reader driven row by row with openStream() |
cparse builds with CMake (≥ 3.20) and a C++20 compiler; its only other dependency is GoogleTest, for the tests, when they are built.
With Conan:
conan create . # build, run the tests, package
conan create . -o with_tests=False
conan build . -o with_docs=True # also the Doxygen tree (build/Release/docs/html)
For development:
conan install . --build=missing
cmake --preset conan-release
cmake --build --preset conan-release
ctest --test-dir build/Release --output-on-failure
| CMake option | Default | |
|---|---|---|
TESTING | ON when top-level | GoogleTest suites in test/ (skipped if GTest is not found) |
EXAMPLE | ON when top-level | examples/ and their smoke tests |
CPARSE_INSTALL_COMPONENT | cparse | install component of libcparse.so |
CPARSE_DOCS_WARN_AS_ERROR | OFF | fail the docs target on a Doxygen warning |
cmake --build … --target docs writes the API reference with Doxygen (and Graphviz dot, if present).
Conan — the way fson, mstring and FFS take it:
def requirements(self):
self.requires("cparse/[>=1.2 <2]")
find_package(cparse CONFIG REQUIRED)
target_link_libraries(myparser PRIVATE cparse::cparse)
Embedded — add_subdirectory(cparse) and link the cparse target. As a subproject cparse turns off its tests, examples, docs target and CPack section, and leaves the build type to the host.
Installed — cmake --install puts the headers under include/cparse/, libcparse.so under lib/ and a pkg-config file:
g++ -std=c++20 myparser.cpp $(pkg-config --cflags --libs cparse)
Cursor built from a file name reads the whole file into memory; a parser can backtrack as often as it likes without touching the file again. Inputs are expected to be small (configuration, source files).libcparse.so.1 keeps its ABI within 1.x.project( … VERSION … ) line of CMakeLists.txt; the Conan recipe and the release tooling read it from there.| Path | |
|---|---|
src/cparse/ | the library |
test/ | GoogleTest suites |
examples/ | toy-calc, key-value, csv-lite |
Doxyfile.in | the docs target's configuration |
conanfile.py, cparse.pc.in | packaging |
MIT — see LICENSE, CONTRIBUTING.md and TRADEMARKS.md.