Skip to content

Commit e11c47c

Browse files
authored
Add CLI argument parser (rfl::cli::read) (#613)
1 parent c2ab61a commit e11c47c

35 files changed

Lines changed: 2087 additions & 0 deletions

README.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ reflect-cpp and sqlgen fill important gaps in C++ development. They reduce boile
3838
- [Simple Example](#simple-example)
3939
- [More Comprehensive Example](#more-comprehensive-example)
4040
- [Tabular data](#tabular-data)
41+
- [CLI argument parsing](#cli-argument-parsing)
4142
- [Error messages](#error-messages)
4243
- [JSON schema](#json-schema)
4344
- [Enums](#enums)
@@ -292,6 +293,43 @@ This will resulting CSV will look like this:
292293
"Homer","Simpson","Springfield",1987-04-19,45,"homer@simpson.com"
293294
```
294295
296+
### CLI argument parsing
297+
298+
reflect-cpp can also parse command-line arguments directly into structs using `rfl::cli::read`:
299+
300+
```cpp
301+
#include <rfl/cli.hpp>
302+
303+
struct Config {
304+
std::string host_name;
305+
int port;
306+
bool verbose;
307+
std::vector<std::string> tags;
308+
};
309+
310+
int main(int argc, char* argv[]) {
311+
const auto config = rfl::cli::read<Config>(argc, argv).value();
312+
// ./app --host-name=localhost --port=8080 --verbose --tags=a,b,c
313+
}
314+
```
315+
316+
Field names are automatically converted from `snake_case` to `kebab-case` (`host_name` matches `--host-name`).
317+
318+
You can mark fields as positional arguments with `rfl::Positional<T>` and add single-character aliases with `rfl::Short<"x", T>`:
319+
320+
```cpp
321+
struct Config {
322+
rfl::Positional<std::string> input_file;
323+
rfl::Short<"o", std::string> output_dir;
324+
rfl::Short<"v", bool> verbose;
325+
int count;
326+
};
327+
328+
// ./app data.csv -o /tmp/out -v --count=10
329+
```
330+
331+
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.
332+
295333
### Error messages
296334
297335
reflect-cpp returns clear and comprehensive error messages:

docs/cli.md

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
# rfl::cli — Command-Line Argument Parser
2+
3+
Parse `argc`/`argv` into any reflectable struct via `rfl::cli::read<T>(argc, argv)`.
4+
5+
## Usage
6+
7+
```cpp
8+
#include <rfl/cli.hpp>
9+
10+
struct Config {
11+
std::string host_name;
12+
int port;
13+
bool verbose;
14+
std::optional<double> rate;
15+
std::vector<std::string> tags;
16+
};
17+
18+
int main(int argc, char* argv[]) {
19+
const auto result = rfl::cli::read<Config>(argc, argv);
20+
// ./app --host-name=localhost --port=8080 --verbose --tags=a,b,c
21+
}
22+
```
23+
24+
Field names undergo automatic `snake_case` -> `kebab-case` conversion:
25+
`host_name` matches `--host-name`.
26+
27+
## Positional arguments
28+
29+
Wrap a field with `rfl::Positional<T>` to accept it as a bare (non-flag) argument:
30+
31+
```cpp
32+
struct Config {
33+
rfl::Positional<std::string> input_file;
34+
rfl::Positional<std::string> output_file;
35+
bool verbose;
36+
};
37+
38+
// ./app input.txt output.txt --verbose
39+
```
40+
41+
Positional arguments are matched in declaration order. They can also be
42+
passed as named arguments: `--input-file=input.txt`.
43+
44+
The `--` separator forces all subsequent tokens into positional:
45+
46+
```
47+
./app --verbose -- --not-a-flag.txt
48+
```
49+
50+
## Short aliases
51+
52+
Wrap a field with `rfl::Short<"x", T>` to add a single-character alias:
53+
54+
```cpp
55+
struct Config {
56+
rfl::Short<"p", int> port;
57+
rfl::Short<"v", bool> verbose;
58+
std::string host;
59+
};
60+
61+
// ./app -p 8080 -v --host=localhost
62+
// ./app -p=8080 -v --host=localhost
63+
// ./app --port=8080 --verbose --host=localhost (long names still work)
64+
```
65+
66+
Short bool flags do not consume the next token as a value — `-v somefile`
67+
treats `somefile` as a positional argument, not as the value of `-v`.
68+
To explicitly set a bool short flag, use `=` syntax: `-v=true`, `-v=false`.
69+
70+
## Combining Positional and Short
71+
72+
`Positional` and `Short` can be used together in the same struct, but
73+
**cannot be nested** (`Positional<Short<...>>` is a compile-time error):
74+
75+
```cpp
76+
struct Config {
77+
rfl::Positional<std::string> input_file;
78+
rfl::Short<"o", std::string> output_dir;
79+
rfl::Short<"v", bool> verbose;
80+
int count;
81+
};
82+
83+
// ./app data.csv -o /tmp/out -v --count=10
84+
```
85+
86+
## Supported types
87+
88+
| Type | CLI format | Notes |
89+
|------|-----------|-------|
90+
| `std::string` | `--key=value` | |
91+
| `int`, `long`, ... | `--key=42` | |
92+
| `float`, `double` | `--key=1.5` | |
93+
| `bool` | `--flag` or `--flag=true` | No `=` implies `true` |
94+
| `enum` | `--key=value_name` | Via `rfl::string_to_enum` |
95+
| `std::optional<T>` | omit for `nullopt` | |
96+
| `std::vector<T>` | `--key=a,b,c` | Comma-separated; empty elements skipped |
97+
| Nested struct | `--parent.child=val` | Dot-separated path |
98+
| `rfl::Flatten<T>` | fields inlined | No prefix needed |
99+
| `rfl::Rename<"x", T>` | `--x=val` | Bypasses kebab conversion |
100+
| `rfl::Positional<T>` | bare token | Matched in declaration order |
101+
| `rfl::Short<"x", T>` | `-x value` or `-x=value` | Single-character alias |
102+
103+
## Architecture
104+
105+
Parsing proceeds in three stages:
106+
107+
1. **`parse_argv`** — categorizes raw tokens into `named`, `short_args`,
108+
and `positional` buckets (`ParsedArgs` struct). No type information needed.
109+
2. **`resolve_args`** — uses compile-time metadata from the target struct to
110+
map short aliases to long names, reclaim values from bool short flags,
111+
and merge positional arguments. Produces a flat `map<string, string>`.
112+
3. **`Reader`** — implements reflect-cpp's `IsReader` concept by presenting
113+
virtual tree nodes over the flat map. Each node is a `{map*, path}` pair —
114+
no data copying, just prefix-based lookup via `lower_bound`.
115+
116+
## Files
117+
118+
- `include/rfl/cli/read.hpp` — public API
119+
- `include/rfl/cli/Reader.hpp` — Reader + `parse_value` overloads
120+
- `include/rfl/cli/Parser.hpp` — Parser type alias
121+
- `include/rfl/cli/parse_argv.hpp``argv` -> `ParsedArgs`
122+
- `include/rfl/cli/resolve_args.hpp``ParsedArgs` -> `map<string, string>`
123+
- `include/rfl/cli.hpp` — aggregator header
124+
- `include/rfl/SnakeCaseToKebabCase.hpp` — processor
125+
- `include/rfl/Positional.hpp``Positional<T>` wrapper
126+
- `include/rfl/Short.hpp``Short<"x", T>` wrapper

include/rfl.hpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,15 @@
3636
#include "rfl/OneOf.hpp"
3737
#include "rfl/Pattern.hpp"
3838
#include "rfl/PatternValidator.hpp"
39+
#include "rfl/Positional.hpp"
3940
#include "rfl/Processors.hpp"
4041
#include "rfl/Ref.hpp"
4142
#include "rfl/Rename.hpp"
43+
#include "rfl/Short.hpp"
4244
#include "rfl/Size.hpp"
4345
#include "rfl/Skip.hpp"
4446
#include "rfl/SnakeCaseToCamelCase.hpp"
47+
#include "rfl/SnakeCaseToKebabCase.hpp"
4548
#include "rfl/SnakeCaseToPascalCase.hpp"
4649
#include "rfl/TaggedUnion.hpp"
4750
#include "rfl/Timestamp.hpp"

include/rfl/Positional.hpp

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
#ifndef RFL_POSITIONAL_HPP_
2+
#define RFL_POSITIONAL_HPP_
3+
4+
#include <type_traits>
5+
#include <utility>
6+
7+
#include "default.hpp"
8+
9+
namespace rfl {
10+
11+
/// Marks a field as positional for CLI argument parsing.
12+
/// For non-CLI formats (JSON, YAML, etc.), this is transparent.
13+
template <class T>
14+
struct Positional {
15+
/// The underlying type.
16+
using Type = T;
17+
18+
Positional() requires std::is_default_constructible_v<Type>
19+
: value_(Type()) {}
20+
21+
Positional(const Type& _value) : value_(_value) {}
22+
23+
Positional(Type&& _value) noexcept : value_(std::move(_value)) {}
24+
25+
Positional(Positional<T>&& _field) noexcept = default;
26+
27+
Positional(const Positional<T>& _field) = default;
28+
29+
template <class U>
30+
Positional(const Positional<U>& _field) : value_(_field.get()) {}
31+
32+
template <class U>
33+
Positional(Positional<U>&& _field) : value_(std::move(_field.value_)) {}
34+
35+
template <class U>
36+
requires std::is_convertible_v<U, Type>
37+
Positional(const U& _value) : value_(_value) {}
38+
39+
template <class U>
40+
requires std::is_convertible_v<U, Type>
41+
Positional(U&& _value) noexcept : value_(std::forward<U>(_value)) {}
42+
43+
/// Assigns the underlying object to its default value.
44+
template <class U = Type>
45+
requires std::is_default_constructible_v<U>
46+
Positional(const Default&) : value_(Type()) {}
47+
48+
~Positional() = default;
49+
50+
/// Returns the underlying object.
51+
const Type& get() const noexcept { return value_; }
52+
53+
/// Returns the underlying object.
54+
Type& get() noexcept { return value_; }
55+
56+
/// Returns the underlying object.
57+
Type& operator*() noexcept { return value_; }
58+
59+
/// Returns the underlying object.
60+
const Type& operator*() const noexcept { return value_; }
61+
62+
/// Returns the underlying object.
63+
Type& operator()() noexcept { return value_; }
64+
65+
/// Returns the underlying object.
66+
const Type& operator()() const noexcept { return value_; }
67+
68+
/// Assigns the underlying object.
69+
auto& operator=(const Type& _value) {
70+
value_ = _value;
71+
return *this;
72+
}
73+
74+
/// Assigns the underlying object.
75+
auto& operator=(Type&& _value) noexcept {
76+
value_ = std::move(_value);
77+
return *this;
78+
}
79+
80+
/// Assigns the underlying object.
81+
template <class U>
82+
requires std::is_convertible_v<U, Type>
83+
auto& operator=(const U& _value) {
84+
value_ = _value;
85+
return *this;
86+
}
87+
88+
/// Assigns the underlying object to its default value.
89+
template <class U = Type>
90+
requires std::is_default_constructible_v<U>
91+
auto& operator=(const Default&) {
92+
value_ = Type();
93+
return *this;
94+
}
95+
96+
/// Assigns the underlying object.
97+
Positional<T>& operator=(const Positional<T>& _field) = default;
98+
99+
/// Assigns the underlying object.
100+
Positional<T>& operator=(Positional<T>&& _field) = default;
101+
102+
/// Assigns the underlying object.
103+
template <class U>
104+
auto& operator=(const Positional<U>& _field) {
105+
value_ = _field.get();
106+
return *this;
107+
}
108+
109+
/// Assigns the underlying object.
110+
template <class U>
111+
auto& operator=(Positional<U>&& _field) {
112+
value_ = std::move(_field.value_);
113+
return *this;
114+
}
115+
116+
/// Assigns the underlying object.
117+
void set(const Type& _value) { value_ = _value; }
118+
119+
/// Assigns the underlying object.
120+
void set(Type&& _value) { value_ = std::move(_value); }
121+
122+
/// Returns the underlying object.
123+
Type& value() noexcept { return value_; }
124+
125+
/// Returns the underlying object.
126+
const Type& value() const noexcept { return value_; }
127+
128+
/// The underlying value.
129+
Type value_;
130+
};
131+
132+
} // namespace rfl
133+
134+
#endif

0 commit comments

Comments
 (0)