Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -34,12 +34,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/CamelCaseToSnakeCase.hpp"
#include "rfl/TaggedUnion.hpp"
Expand Down
125 changes: 125 additions & 0 deletions include/rfl/Positional.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
#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 { return value_; }

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

/// Returns the underlying object.
const Type& operator()() const { 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() { return value_; }

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

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

} // namespace rfl

#endif
Loading
Loading