Skip to content
Merged
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
34 changes: 17 additions & 17 deletions docs/docs/actions.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,14 @@ For **terminal (pack) steps** the story is different. A generic pack step like

```cpp
template <class Prev>
auto operator()(ActionCtx<std::optional<Prev>>&, ActionResult<Prev>) -> ActionResult<void>;
auto operator()(ActionCtx<Prev>&, ActionResult<Prev>) -> ActionResult<void>;
```

The storage type `std::optional<Prev>` is embedded in the first parameter,
and `Prev` itself is not known until the pipeline is validated. There is no
way to extract that type alias without an explicit `storage_type<Prev>`
declaration — you would need to solve for the template parameter of
`ActionCtx<?>`, which requires a trait or an additional overload.
The storage type `Prev` is embedded in the first parameter, and `Prev`
itself is not known until the pipeline is validated. There is no way to
extract that type alias without an explicit `storage_type<Prev>` declaration
— you would need to solve for the template parameter of `ActionCtx<?>`,
which requires a trait or an additional overload.

So the explicit traits are load-bearing for pack steps. Once `storage_type`
must exist anyway, keeping `accepts_input` and `after_type` explicit costs
Expand Down Expand Up @@ -88,8 +88,9 @@ string_view ──► [Conversion] ──► T ──► [Validation] ──►
└──────────────────────┴─────────────────────► ParseError
```

Any step can short-circuit the chain by returning a `ParseError`. Downstream
steps are skipped automatically.
`ignore` short-circuits the remaining steps in the chain. `error` still
flows through downstream steps, which lets terminal actions preserve or
reformat the failure instead of bypassing it.

---

Expand Down Expand Up @@ -143,7 +144,7 @@ action.

| Expression | Storage type | Behaviour |
|---|---|---|
| `pack::set_once` | `std::optional<T>` | stores once; rejects duplicates |
| `pack::set_once` | `T` | stores once; rejects duplicates |
| `pack::push` | `std::vector<T>` | appends on each occurrence |
| `pack::push_unique` | `std::vector<T>` | appends; rejects duplicate values |
| `pack::insert` | `std::set<T>` | inserts into set |
Expand All @@ -152,7 +153,7 @@ action.
| `pack::set_false` | `bool` | always stores `false` |
| `pack::toggle` | `bool` | flips on each occurrence |
| `pack::increment` | `std::size_t` | increments on each occurrence |
| `pack::store_into<T>` | `T*` | writes through a pointer |
| `pack::write_to<T>` | `T*` | writes through a pointer |
| `pack::callback<fn>` | `std::monostate` | calls `fn` with the value |

### Examples
Expand Down Expand Up @@ -214,16 +215,15 @@ constexpr auto pipeline =
| cli::validation::range<1, 32>
| cli::pack::set_once;

// pack::set_once writes into std::optional<int>, so the ctx must carry
// a reference to that storage.
std::optional<int> storage;
cli::ActionCtx<std::optional<int>> ctx{.arg = std::ref(storage)};
// pack::set_once writes into int, so the ctx must carry a reference to it.
int storage{};
cli::ActionCtx<int> ctx{.arg = std::ref(storage)};

auto result = pipeline.invoke(ctx, cli::ActionResult<std::string_view>::ok("8"));

if (result.has_value()) {
// The packed value is in storage, not in result.
// storage == std::optional<int>{8}
// storage == 8
} else {
// result.error.code, .subject, .detail carry the failure reason
}
Expand All @@ -241,8 +241,8 @@ For pack actions (which need the storage reference) use `ActionCtx<StorageType>`
initialised with designated initialisers:

```cpp
std::optional<int> storage;
cli::ActionCtx<std::optional<int>> ctx{.arg = std::ref(storage)};
int storage{};
cli::ActionCtx<int> ctx{.arg = std::ref(storage)};
```

---
Expand Down
18 changes: 12 additions & 6 deletions docs/docs/examples/advanced.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,10 @@ blocks behind cli20's higher-level aliases.

This example builds parsing behavior from explicit conversion, validation, and
storage steps instead of the higher-level aliases. It is the pattern to copy
when you need lists, uniqueness, or filesystem checks with precise control over
how values are stored. On success, it produces optional `config` and `jobs`
values plus collected `includes` and `defines`.
when you need lists, uniqueness, filesystem checks, or a runtime default for
an option value with precise control over how values are stored. On success, it
produces optional `config`, `jobs` with a default of `1` (`-j` with no value
uses a `nproc_dummy()` value), plus collected `includes` and `defines`.

## Try It

Expand All @@ -44,16 +45,21 @@ The virtual filesystem is configured like this in this environment:

<ExamplePlayground
name="advanced"
defaultArgs="--config config.toml --jobs 4 -I /include -D DEBUG"
defaultArgs="--config config.toml -I /include -D DEBUG"
setupFs={setupAdvancedFs}
/>

Things to try:

- **Happy path** — `--config config.toml --jobs 4 -I /include -D DEBUG`
- **Happy path** — `--config config.toml -I /include -D DEBUG`
- **Default jobs** — `--config config.toml` to see the `1` default
- **Runtime default** — `--config config.toml -j` to see `nproc_dummy()` return `4`
- **Explicit jobs** — `--config config.toml -j 8` to override the runtime default
- **Duplicate jobs** — `--config config.toml -j -j` to see duplicate detection fail
- **Multiple defines** — `--config config.toml -D FOO -D BAR -D FOO` to see `push_unique` deduplicate
- **Directory as config** — `--config /configs` to see `is_regular_file` reject it
- **Out-of-range jobs** — `--config config.toml --jobs 99` to see range validation fail
- **Out-of-range jobs** — `--config config.toml -j 99` to see range validation fail
- **Missing jobs value** — `--config config.toml -j` to see the example fall back to `nproc_dummy()`
- **Missing parent** — `-I /usr/include` to see `parent_exists` fail (`/usr` doesn't exist in the virtual FS)
- **Help** — `-h` or `--help`

Expand Down
6 changes: 3 additions & 3 deletions docs/docs/intro.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -102,11 +102,11 @@ auto main(int argc, char* argv[]) -> int {
std::cout << "verbose mode on\n";
}

if (args.output.value()) {
std::cout << "output: " << *args.output.value() << '\n';
if (args.output) {
std::cout << "output: " << *args.output << '\n';
}

std::cout << "input: " << *args.input.value() << '\n';
std::cout << "input: " << *args.input << '\n';
return 0;
}
```
Expand Down
14 changes: 7 additions & 7 deletions docs/docs/tutorial.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,11 @@ cli::ListOption<std::string, "include", 'I'> include_dirs;
| `cli::DoubleOption<"ratio">` | `cli::Option<double, "ratio">` |
| `cli::PathOption<"output">` | `cli::Option<std::filesystem::path, "output">` |

Scalar options store `std::optional<T>`. List options store `std::vector<T>`.
Scalar options store `T`. List options store `std::vector<T>`.

```cpp
if (args.output.value()) {
std::cout << "output: " << *args.output.value() << '\n';
if (args.output) {
std::cout << "output: " << *args.output << '\n';
}
```

Expand All @@ -58,12 +58,12 @@ if (args.output.value()) {
Positional arguments are matched by position rather than name.

```cpp
cli::Positional<std::string> src; // optional<string>
cli::Positional<std::string> src; // string
cli::Positional<std::string, cli::nargs::one_or_more> files; // vector<string>
cli::Positional<int> count;
```

If `nargs.max == 1`, the storage type is `std::optional<T>`.
If `nargs.max == 1`, the storage type is `T`.
If `nargs.max != 1`, the storage type is `std::vector<T>`.

```cpp
Expand Down Expand Up @@ -111,7 +111,7 @@ Check whether the subcommand was provided with `.provided()`:

```cpp
if (args.build.provided()) {
std::cout << "jobs: " << *args.build.jobs.value() << '\n';
std::cout << "jobs: " << *args.build.jobs << '\n';
}
```

Expand Down Expand Up @@ -158,7 +158,7 @@ auto main(int argc, char* argv[]) -> int {
}

if (args.build.provided()) {
std::cout << "jobs: " << *args.build.jobs.value() << '\n';
std::cout << "jobs: " << *args.build.jobs << '\n';
}

return 0;
Expand Down
45 changes: 38 additions & 7 deletions examples/advanced.cc
Original file line number Diff line number Diff line change
@@ -1,11 +1,42 @@
#include <filesystem>
#include <iostream>
#include <optional>
#include <string_view>

#include "cli/argument.hh"
#include "cli/parser.hh"

namespace fs = std::filesystem;

auto nproc_dummy() -> int {
// Example-only stand-in for a runtime nproc query. Kept deterministic so it
// works in environments where querying host CPU count is awkward.
return 4;
}

struct JobsOrNproc {
template <class Input>
static constexpr bool accepts_input =
cli::deduce_accepts_input<JobsOrNproc, Input>;

template <class Input>
using after_type = cli::deduce_after_type<JobsOrNproc, Input>;

template <class Prev>
using storage_type = void;

auto operator()(cli::ActionCtx<void> ctx,
cli::ActionResult<std::optional<std::string_view>> input) const
-> cli::ActionResult<int> {
if (!input.have_value()) return cli::propagate<int>(input);
if (input.value.has_value()) {
return cli::conversion::integer<int>.invoke(
ctx, cli::ActionResult<std::string_view>::ok(input.value.value()));
}
return cli::ActionResult<int>::ok(nproc_dummy());
}
};

struct Args {
cli::Description description{
"Action-pipeline example using Arg, filesystem conversion, and"
Expand All @@ -21,9 +52,11 @@ struct Args {
config{{.help = "Configuration file", .presence = cli::required}};

cli::Arg<"jobs", 'j',
cli::conversion::integer<int> | cli::validation::range<1, 64> |
cli::pack::set_once>
jobs{{.help = "Parallel job count (1-64)"}};
cli::Action<JobsOrNproc{}>{} | cli::validation::range<1, 64> |
cli::pack::set_once,
cli::nargs::zero_or_one>
jobs_arg{{.help = "Parallel job count (default 1; -j uses nproc_dummy())",
.default_value = 1}};

cli::Arg<"include", 'I',
cli::conversion::path | cli::validation::parent_exists |
Expand All @@ -41,10 +74,8 @@ struct Args {
auto main(int argc, char* argv[]) -> int {
const auto args = cli::parse_or_exit<Args>(argc, argv);

std::cout << "config: " << args.config.value()->string() << '\n';
if (args.jobs.value()) {
std::cout << "jobs: " << *args.jobs.value() << '\n';
}
std::cout << "config: " << args.config->string() << '\n';
std::cout << "jobs: " << *args.jobs_arg << '\n';
for (const fs::path& include_dir : args.includes.value()) {
std::cout << "include: " << include_dir.string() << '\n';
}
Expand Down
23 changes: 8 additions & 15 deletions examples/callback_entry_point.cc
Original file line number Diff line number Diff line change
Expand Up @@ -41,28 +41,21 @@ auto main(int argc, char* argv[]) -> int {
Args args;

args.verbose = cli::Flag<"verbose", 'v'>{{
.on_parse = [&state](const bool& value) { state.verbose = value; },
.on_parse = [&state](const bool& value) -> void { state.verbose = value; },
}};
args.jobs = cli::IntOption<"jobs", 'j'>{{
.on_parse =
[&state](const std::optional<int>& value) {
if (value) {
state.jobs = *value;
}
},
.on_parse = [&state](const int& value) -> void { state.jobs = value; },
}};
args.includes =
cli::ListOption<std::string, "include", 'I', cli::nargs::exactly<1>>{{
.on_parse =
[&state](const std::vector<std::string>& value) {
state.includes = value;
},
.on_parse = [&state](const std::vector<std::string>& value) -> void {
state.includes = value;
},
}};
args.inputs = cli::Positional<std::string, cli::nargs::one_or_more>{{
.on_parse =
[&state](const std::vector<std::string>& value) {
state.inputs = value;
},
.on_parse = [&state](const std::vector<std::string>& value) -> void {
state.inputs = value;
},
}};

const auto result = cli::Parser<Args>{}.parse(std::move(args), argc, argv);
Expand Down
2 changes: 1 addition & 1 deletion examples/compiler.cc
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ auto main(int argc, char* argv[]) -> int {
const auto args = cli::parse_or_exit<Args>(argc, argv);

if (args.optimize.value()) {
std::cout << "optimize: -O" << *args.optimize.value() << '\n';
std::cout << "optimize: -O" << *args.optimize << '\n';
}

if (const std::size_t v = args.verbose.value(); v > 0) {
Expand Down
8 changes: 4 additions & 4 deletions examples/env_fallback.cc
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,10 @@ auto main(int argc, char* argv[]) -> int {
if (args.verbose.value()) {
std::cout << "verbose: true\n";
}
std::cout << "threads: " << *args.threads.value() << '\n';
if (const auto& cfg = args.config.value()) {
std::cout << "config: " << *cfg << '\n';
std::cout << "threads: " << *args.threads << '\n';
if (args.config) {
std::cout << "config: " << *args.config << '\n';
}
std::cout << "job: " << *args.job.value() << '\n';
std::cout << "job: " << *args.job << '\n';
return 0;
}
2 changes: 1 addition & 1 deletion examples/module.cc
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ struct Args {
auto main(int argc, char* argv[]) -> int {
const Args args = cli::parse_or_exit<Args>(argc, argv);
if (args.port.value()) {
std::cout << "port: " << *args.port.value() << '\n';
std::cout << "port: " << *args.port << '\n';
}
for (const string& file : args.files.value()) {
std::cout << "file: " << file << '\n';
Expand Down
6 changes: 3 additions & 3 deletions examples/simple_api.cc
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ auto main(int argc, char* argv[]) -> int {
if (args.verbose.value()) {
std::cout << "verbose: true\n";
}
if (args.output.value()) {
std::cout << "output: " << *args.output.value() << '\n';
if (args.output) {
std::cout << "output: " << *args.output << '\n';
}
std::cout << "input: " << *args.input.value() << '\n';
std::cout << "input: " << *args.input << '\n';
return 0;
}
6 changes: 3 additions & 3 deletions examples/single_header.cc
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ struct Args {
auto main(int argc, char* argv[]) -> int {
const auto args = cli::parse_or_exit<Args>(argc, argv);
std::cout << "verbose: " << std::boolalpha << args.verbose.value() << '\n';
if (args.config.value()) {
std::cout << "config: " << *args.config.value() << '\n';
if (args.config) {
std::cout << "config: " << *args.config << '\n';
}
std::cout << "input: " << *args.input.value() << '\n';
std::cout << "input: " << *args.input << '\n';
return 0;
}
4 changes: 2 additions & 2 deletions examples/subcommand.cc
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ auto main(int argc, char* argv[]) -> int {
std::cout << "release: true\n";
}
if (args.build.jobs.value()) {
std::cout << "jobs: " << *args.build.jobs.value() << '\n';
std::cout << "jobs: " << *args.build.jobs << '\n';
}
for (const std::string& input : args.build.inputs.value()) {
std::cout << "input: " << input << '\n';
Expand All @@ -50,7 +50,7 @@ auto main(int argc, char* argv[]) -> int {

if (args.publish.provided()) {
std::cout << "command: publish\n";
std::cout << "registry: " << *args.publish.registry.value() << '\n';
std::cout << "registry: " << *args.publish.registry << '\n';
std::cout << "dry-run: " << std::boolalpha << args.publish.dry_run.value()
<< '\n';
}
Expand Down
4 changes: 2 additions & 2 deletions examples/unicode_echo.cc
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ auto main(int argc, char* argv[]) -> int {
#endif
const auto args = cli::parse_or_exit<Args>(argc, argv);

if (const auto& v = args.name.value(); v.has_value()) {
std::cout << "name=" << *v << '\n';
if (args.name) {
std::cout << "name=" << *args.name << '\n';
}
for (const auto& f : args.files.value()) {
std::cout << "file=" << f << '\n';
Expand Down
8 changes: 4 additions & 4 deletions examples/validation.cc
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@ struct Args {
auto main(int argc, char* argv[]) -> int {
const auto args = cli::parse_or_exit<Args>(argc, argv);

std::cout << "port: " << *args.port.value() << '\n';
std::cout << "profile: " << *args.profile.value() << '\n';
if (args.output.value()) {
std::cout << "output: " << args.output.value()->string() << '\n';
std::cout << "port: " << *args.port << '\n';
std::cout << "profile: " << *args.profile << '\n';
if (args.output) {
std::cout << "output: " << args.output->string() << '\n';
}
return 0;
}
Loading
Loading