Skip to content
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ reflect-cpp and sqlgen fill important gaps in C++ development. They reduce boile
- [Simple Example](#simple-example)
- [More Comprehensive Example](#more-comprehensive-example)
- [Tabular data](#tabular-data)
- [CLI argument parsing](#cli-argument-parsing)
- [Error messages](#error-messages)
- [JSON schema](#json-schema)
- [Enums](#enums)
Expand Down Expand Up @@ -292,6 +293,43 @@ This will resulting CSV will look like this:
"Homer","Simpson","Springfield",1987-04-19,45,"homer@simpson.com"
```
### CLI argument parsing
reflect-cpp can also parse command-line arguments directly into structs using `rfl::cli::read`:
```cpp
#include <rfl/cli.hpp>
struct Config {
std::string host_name;
int port;
bool verbose;
std::vector<std::string> tags;
};
int main(int argc, char* argv[]) {
const auto config = rfl::cli::read<Config>(argc, argv).value();
// ./app --host-name=localhost --port=8080 --verbose --tags=a,b,c
}
```

Field names are automatically converted from `snake_case` to `kebab-case` (`host_name` matches `--host-name`).

You can mark fields as positional arguments with `rfl::Positional<T>` and add single-character aliases with `rfl::Short<"x", T>`:

```cpp
struct Config {
rfl::Positional<std::string> input_file;
rfl::Short<"o", std::string> output_dir;
rfl::Short<"v", bool> verbose;
int count;
};

// ./app data.csv -o /tmp/out -v --count=10
```
Nested structs, `std::optional`, `std::vector`, enums, `rfl::Flatten` and `rfl::Rename` are all supported. Refer to the [documentation](https://rfl.getml.com/cli) for details.
### Error messages
reflect-cpp returns clear and comprehensive error messages:
Expand Down
126 changes: 126 additions & 0 deletions docs/cli.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
# rfl::cli — Command-Line Argument Parser

Parse `argc`/`argv` into any reflectable struct via `rfl::cli::read<T>(argc, argv)`.

## Usage

```cpp
#include <rfl/cli.hpp>

struct Config {
std::string host_name;
int port;
bool verbose;
std::optional<double> rate;
std::vector<std::string> tags;
};

int main(int argc, char* argv[]) {
const auto result = rfl::cli::read<Config>(argc, argv);
// ./app --host-name=localhost --port=8080 --verbose --tags=a,b,c
}
```

Field names undergo automatic `snake_case` -> `kebab-case` conversion:
`host_name` matches `--host-name`.

## Positional arguments

Wrap a field with `rfl::Positional<T>` to accept it as a bare (non-flag) argument:

```cpp
struct Config {
rfl::Positional<std::string> input_file;
rfl::Positional<std::string> output_file;
bool verbose;
};

// ./app input.txt output.txt --verbose
```

Positional arguments are matched in declaration order. They can also be
passed as named arguments: `--input-file=input.txt`.

The `--` separator forces all subsequent tokens into positional:

```
./app --verbose -- --not-a-flag.txt
```

## Short aliases

Wrap a field with `rfl::Short<"x", T>` to add a single-character alias:

```cpp
struct Config {
rfl::Short<"p", int> port;
rfl::Short<"v", bool> verbose;
std::string host;
};

// ./app -p 8080 -v --host=localhost
// ./app -p=8080 -v --host=localhost
// ./app --port=8080 --verbose --host=localhost (long names still work)
```

Short bool flags do not consume the next token as a value — `-v somefile`
treats `somefile` as a positional argument, not as the value of `-v`.
To explicitly set a bool short flag, use `=` syntax: `-v=true`, `-v=false`.

## Combining Positional and Short

`Positional` and `Short` can be used together in the same struct, but
**cannot be nested** (`Positional<Short<...>>` is a compile-time error):

```cpp
struct Config {
rfl::Positional<std::string> input_file;
rfl::Short<"o", std::string> output_dir;
rfl::Short<"v", bool> verbose;
int count;
};

// ./app data.csv -o /tmp/out -v --count=10
```

## Supported types

| Type | CLI format | Notes |
|------|-----------|-------|
| `std::string` | `--key=value` | |
| `int`, `long`, ... | `--key=42` | |
| `float`, `double` | `--key=1.5` | |
| `bool` | `--flag` or `--flag=true` | No `=` implies `true` |
| `enum` | `--key=value_name` | Via `rfl::string_to_enum` |
| `std::optional<T>` | omit for `nullopt` | |
| `std::vector<T>` | `--key=a,b,c` | Comma-separated; empty elements skipped |
| Nested struct | `--parent.child=val` | Dot-separated path |
| `rfl::Flatten<T>` | fields inlined | No prefix needed |
| `rfl::Rename<"x", T>` | `--x=val` | Bypasses kebab conversion |
| `rfl::Positional<T>` | bare token | Matched in declaration order |
| `rfl::Short<"x", T>` | `-x value` or `-x=value` | Single-character alias |

## Architecture

Parsing proceeds in three stages:

1. **`parse_argv`** — categorizes raw tokens into `named`, `short_args`,
and `positional` buckets (`ParsedArgs` struct). No type information needed.
2. **`resolve_args`** — uses compile-time metadata from the target struct to
map short aliases to long names, reclaim values from bool short flags,
and merge positional arguments. Produces a flat `map<string, string>`.
3. **`Reader`** — implements reflect-cpp's `IsReader` concept by presenting
virtual tree nodes over the flat map. Each node is a `{map*, path}` pair —
no data copying, just prefix-based lookup via `lower_bound`.

## Files

- `include/rfl/cli/read.hpp` — public API
- `include/rfl/cli/Reader.hpp` — Reader + `parse_value` overloads
- `include/rfl/cli/Parser.hpp` — Parser type alias
- `include/rfl/cli/parse_argv.hpp` — `argv` -> `ParsedArgs`
- `include/rfl/cli/resolve_args.hpp` — `ParsedArgs` -> `map<string, string>`
- `include/rfl/cli.hpp` — aggregator header
- `include/rfl/SnakeCaseToKebabCase.hpp` — processor
- `include/rfl/Positional.hpp` — `Positional<T>` wrapper
- `include/rfl/Short.hpp` — `Short<"x", T>` wrapper
3 changes: 3 additions & 0 deletions include/rfl.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,15 @@
#include "rfl/OneOf.hpp"
#include "rfl/Pattern.hpp"
#include "rfl/PatternValidator.hpp"
#include "rfl/Positional.hpp"
#include "rfl/Processors.hpp"
#include "rfl/Ref.hpp"
#include "rfl/Rename.hpp"
#include "rfl/Short.hpp"
#include "rfl/Size.hpp"
#include "rfl/Skip.hpp"
#include "rfl/SnakeCaseToCamelCase.hpp"
#include "rfl/SnakeCaseToKebabCase.hpp"
#include "rfl/SnakeCaseToPascalCase.hpp"
#include "rfl/TaggedUnion.hpp"
#include "rfl/Timestamp.hpp"
Expand Down
134 changes: 134 additions & 0 deletions include/rfl/Positional.hpp
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Centimo you probably forked before we did this, but we recently added operator* as an alternative to .value(), .get() and operator(). Moreover, we made sure that there is a non-const .get() for all classes. These changes do not seem to be reflected here. Could you add that?

Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
#ifndef RFL_POSITIONAL_HPP_
#define RFL_POSITIONAL_HPP_

#include <type_traits>
#include <utility>

#include "default.hpp"

namespace rfl {

/// Marks a field as positional for CLI argument parsing.
/// For non-CLI formats (JSON, YAML, etc.), this is transparent.
template <class T>
struct Positional {
/// The underlying type.
using Type = T;

Positional() requires std::is_default_constructible_v<Type>
: value_(Type()) {}

Positional(const Type& _value) : value_(_value) {}

Positional(Type&& _value) noexcept : value_(std::move(_value)) {}

Positional(Positional<T>&& _field) noexcept = default;

Positional(const Positional<T>& _field) = default;

template <class U>
Positional(const Positional<U>& _field) : value_(_field.get()) {}

template <class U>
Positional(Positional<U>&& _field) : value_(std::move(_field.value_)) {}

template <class U>
requires std::is_convertible_v<U, Type>
Positional(const U& _value) : value_(_value) {}

template <class U>
requires std::is_convertible_v<U, Type>
Positional(U&& _value) noexcept : value_(std::forward<U>(_value)) {}

/// Assigns the underlying object to its default value.
template <class U = Type>
requires std::is_default_constructible_v<U>
Positional(const Default&) : value_(Type()) {}

~Positional() = default;

/// Returns the underlying object.
const Type& get() const noexcept { return value_; }

/// Returns the underlying object.
Type& get() noexcept { return value_; }

/// Returns the underlying object.
Type& operator*() noexcept { return value_; }

/// Returns the underlying object.
const Type& operator*() const noexcept { return value_; }

/// Returns the underlying object.
Type& operator()() noexcept { return value_; }

/// Returns the underlying object.
const Type& operator()() const noexcept { return value_; }

/// Assigns the underlying object.
auto& operator=(const Type& _value) {
value_ = _value;
return *this;
}

/// Assigns the underlying object.
auto& operator=(Type&& _value) noexcept {
value_ = std::move(_value);
return *this;
}

/// Assigns the underlying object.
template <class U>
requires std::is_convertible_v<U, Type>
auto& operator=(const U& _value) {
value_ = _value;
return *this;
}

/// Assigns the underlying object to its default value.
template <class U = Type>
requires std::is_default_constructible_v<U>
auto& operator=(const Default&) {
value_ = Type();
return *this;
}

/// Assigns the underlying object.
Positional<T>& operator=(const Positional<T>& _field) = default;

/// Assigns the underlying object.
Positional<T>& operator=(Positional<T>&& _field) = default;

/// Assigns the underlying object.
template <class U>
auto& operator=(const Positional<U>& _field) {
value_ = _field.get();
return *this;
}

/// Assigns the underlying object.
template <class U>
auto& operator=(Positional<U>&& _field) {
value_ = std::move(_field.value_);
return *this;
}

/// Assigns the underlying object.
void set(const Type& _value) { value_ = _value; }

/// Assigns the underlying object.
void set(Type&& _value) { value_ = std::move(_value); }

/// Returns the underlying object.
Type& value() noexcept { return value_; }

/// Returns the underlying object.
const Type& value() const noexcept { return value_; }

/// The underlying value.
Type value_;
};

} // namespace rfl

#endif
Loading
Loading