Skip to main content

Advanced

This is the reference example for explicit action-pipeline composition. Use it when the sugar aliases are close but not quite enough.

It is the page to open when you need to understand the lower-level building blocks behind cli20's higher-level aliases. It also demonstrates cli::action::custom<fn> for a one-off lambda-based action step.

Source​

Source: examples/advanced.cc

examples/advanced.cc
#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;
}

constexpr auto jobs_or_nproc =
[](cli::ActionCtx<void> ctx,
cli::ActionResult<std::optional<std::string_view>> input)
-> cli::ActionResult<int> {
if (!input.have_value()) return cli::fail<int>(input.error);
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"
" collection-valued options."};

cli::Arg<"help", 'h', cli::action::print_help | cli::action::exit_success,
cli::nargs::none>
help{{.help = "Show help and exit"}};

cli::Arg<"config", 'c',
cli::conversion::path | cli::validation::exists |
cli::validation::is_regular_file | cli::pack::set_once>
config{{.help = "Configuration file", .presence = cli::required}};

cli::Arg<"jobs", 'j',
cli::action::custom<jobs_or_nproc> | 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 |
cli::pack::push,
cli::nargs::one_or_more>
includes{{.help = "Include directories to add"}};

cli::Arg<"define", 'D',
cli::conversion::string | cli::validation::not_blank |
cli::pack::push_unique,
cli::nargs::one_or_more>
defines{{.help = "Unique preprocessor definitions"}};
};

auto main(int argc, char* argv[]) -> int {
const auto args = cli::parse_or_exit<Args>(argc, argv);

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';
}
for (const std::string& define : args.defines.value()) {
std::cout << "define: " << define << '\n';
}
return 0;
}

What This Example Shows​

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, 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​

The virtual filesystem is configured like this in this environment:

/
├── config.toml (regular file)
└── configs/ (directory)
cli20@cli20-dev: ~
cli20@cli20-dev:~$ advanced

Things to try:

  • 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 -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