Code View

cparse / cparse-1.3.0.2 / README.md
Preview
# 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).