A header-only C++26 command-line argument parser driven entirely by static reflection. Describe your options as a plain struct, attach behaviour with annotations, and cli::parse does the rest — no macros, no builder boilerplate, no runtime registration.
#include <cli/cli.hpp>
#include <print>
struct [[=cli::program("greet"),
=cli::version("0.1.0"),
=cli::about("Say hello to someone.")]]
Opts {
[[=cli::desc("Who to greet"), =cli::positional, =cli::required]]
std::string who;
[[=cli::desc("Greeting volume (0-11)"),
=cli::short_name('v'),
=cli::value_name("N"),
=cli::range(0, 11)]]
int volume = 5;
[[=cli::desc("Use SHOUTING"), =cli::short_name('s')]]
bool shout = false;
};
int main(int argc, char** argv) {
auto opts = cli::parse<Opts>(argc, argv);
auto msg = std::format("Hello, {}!", opts.who);
if (opts.shout) std::transform(msg.begin(), msg.end(), msg.begin(), ::toupper);
std::println("{}", msg);
}$ greet --help
greet
Say hello to someone.
Usage: greet [options]
Positional arguments:
who Who to greet
Options:
-v, --volume N Greeting volume (0-11)
-s, --shout Use SHOUTING
| libcli | CLI11 | argparse | cxxopts | |
|---|---|---|---|---|
| Options struct is the source of truth | ✅ | ❌ (builder API) | ❌ (builder API) | ❌ (builder API) |
| Zero macros | ✅ | ✅ | ✅ | ✅ |
| No runtime registration step | ✅ | ❌ | ❌ | ❌ |
Subcommands via std::variant |
✅ | ❌ (subparser API) | ❌ | ❌ |
| Help and parse table generated at compile time | ✅ | ❌ | ❌ | ❌ |
| Standard required | C++26 | C++11 | C++17 | C++11 |
The trade-off is at the bottom row: libcli needs a compiler with P2996 static reflection, which today means GCC 16.1 or newer. In exchange, every option name, type, default, and validator is derived from your struct at compile time, so there is no schema/code drift to keep in sync.
Pre-release (0.0.x). The annotation surface is stable; the parser implementation is still filling in features tagged // TODO (see the CHANGELOG for what's wired and what's stubbed). Feedback and PRs welcome.
include(FetchContent)
FetchContent_Declare(
libcli
GIT_REPOSITORY https://github.com/aaronaranda/libcli.git
GIT_TAG main)
FetchContent_MakeAvailable(libcli)
target_link_libraries(my_app PRIVATE libcli::cli)find_package(libcli 0.1 REQUIRED)
target_link_libraries(my_app PRIVATE libcli::cli)Build options:
| Option | Default | Effect |
|---|---|---|
LIBCLI_BUILD_TESTS |
OFF | Build googletest-based test suite. |
LIBCLI_BUILD_EXAMPLES |
OFF | Build the example programs under examples/. |
LIBCLI_BUILD_DOCS |
OFF | Generate Doxygen HTML into build/docs/html/. |
LIBCLI_BUILD_BENCHMARKS |
OFF | Build benchmark suite (currently empty). |
- A C++26 compiler with static reflection support (
-freflection):- GCC ≥ 16.1 — known good.
- Clang/MSVC do not yet implement P2996.
- CMake ≥ 4.3.
| Feature | Status |
|---|---|
| Positional arguments | ✅ |
| Required vs optional members | ✅ |
Short (-v) and long (--volume) flags |
✅ |
Boolean flags (presence → true) |
✅ |
Repeatable counter flags (-vvv) |
✅ |
| Vector-typed options (multi-value) | ✅ |
--name=value form |
✅ |
Subcommands via std::variant<...> |
✅ |
--help / -h auto-routing per subcommand |
✅ |
Program-level program/version/about/epilog/usage annotations |
✅ |
cli::range(min, max) validator |
⏳ stubbed, not enforced |
cli::choices(...) validator |
⏳ stubbed, not enforced |
cli::validate_with<&fn> |
⏳ stubbed |
cli::on_match<&fn> action callbacks |
⏳ stubbed |
cli::env("VAR") env-var fallback |
⏳ stubbed |
Clustered shorts (-abc = -a -b -c) |
⏳ |
std::optional<T> member type |
⏳ |
try_parse<T> returning std::expected |
⏳ |
See CHANGELOG.md for what landed in each release.
- Guide — narrative walk through the annotation surface.
- Examples — small programs covering positional, flags, subcommands, etc.
- Doxygen HTML — enable
-DLIBCLI_BUILD_DOCS=ONand build thedocstarget.
PRs and issues welcome. See CONTRIBUTING.md for build/test instructions and style notes.
MIT.