# cparse
**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>**.
## Example
A grammar for a comma-separated list of integers — `1, 2, 30, 400`:
```cpp
#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';
}
```
```text
$ ./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()` |
## Building
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:
```sh
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:
```sh
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).
## Using cparse
**Conan** — the way fson, mstring and FFS take it:
```python
def requirements(self):
self.requires("cparse/[>=1.2 <2]")
```
```cmake
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:
```sh
g++ -std=c++20 myparser.cpp $(pkg-config --cflags --libs cparse)
```
## Notes
- A `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.
- The version is the literal on the `project( … VERSION … )` line of
`CMakeLists.txt`; the Conan recipe and the release tooling read it
from there.
## Layout
| 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 |
## License
MIT — see [LICENSE](LICENSE), [CONTRIBUTING.md](CONTRIBUTING.md) and
[TRADEMARKS.md](TRADEMARKS.md).