Skip to main content

Tutorial

This tutorial walks through all the major features of cli20, from simple flags to custom action pipelines.

Flags​

A flag is a boolean option that is true when present on the command line.

cli::Flag<"verbose", 'v'> verbose; // --verbose / -v, stores bool
cli::Help<> help; // --help / -h, print formatted help and exit
cli::Help<"usage", 'u'> usage; // custom long name and short name

Flags store their value as bool. Access it with .value():

if (args.verbose.value()) {
std::cout << "verbose mode\n";
}

Options​

Options take a value from the command line. cli20 provides a generic form and convenience aliases for common types.

Generic form​

cli::Option<int, "port", 'p'> port;
cli::ListOption<std::string, "include", 'I'> include_dirs;

Convenience aliases​

AliasEquivalent
cli::StringOption<"name", 'n'>cli::Option<std::string, "name", 'n'>
cli::IntOption<"count", 'c'>cli::Option<int, "count", 'c'>
cli::DoubleOption<"ratio">cli::Option<double, "ratio">
cli::PathOption<"output">cli::Option<std::filesystem::path, "output">

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

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

Positionals​

Positional arguments are matched by position rather than name.

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 T.
If nargs.max != 1, the storage type is std::vector<T>.

for (const auto& f : args.files.value()) {
std::cout << "file: " << f << '\n';
}

Built-in Help​

Add a single field and help is wired up automatically:

cli::Help<> help;

This expands to --help / -h. When the flag is present, cli20 prints a formatted help message and exits with success. Combined with cli::parse_or_exit(), no additional boilerplate is required.

For explicit control you can use Arg directly with an action pipeline:

cli::Arg<
"help", 'h',
cli::action::print_help | cli::action::exit_success,
cli::nargs::none>
help;

Subcommands​

Subcommands are typed schemas nested inside a parent schema.

struct BuildArgs {
cli::Flag<"release", 'r'> release;
cli::IntOption<"jobs", 'j'> jobs;
};

struct Args {
cli::Command<"build", BuildArgs> build;
};

Check whether the subcommand was provided with .provided():

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

Subcommands are recursive — a BuildArgs can itself contain cli::Command<...> fields.

Quick Start — full example​

#include <iostream>

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

struct BuildArgs {
cli::Flag<"release", 'r'> release{
{.help = "Build with optimizations"}};
cli::IntOption<"jobs", 'j'> jobs{
{.help = "Parallel jobs", .presence = cli::required}};
};

struct Args {
cli::Description description{"A tiny build tool powered by cli20."};

cli::Help<> help{{.help = "Show help"}};
cli::Flag<"verbose", 'v'> verbose{{.help = "Enable verbose logging"}};
cli::StringOption<"output", 'o'> output{{.help = "Output path"}};

cli::Positional<std::string, cli::nargs::one_or_more> inputs{
{.help = "Input files", .presence = cli::required}};

cli::Command<"build", BuildArgs> build{
{.help = "Run the build subcommand"}};
};

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

if (args.verbose.value()) {
std::cout << "verbose enabled\n";
}

for (const auto& input : args.inputs.value()) {
std::cout << "input: " << input << '\n';
}

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

return 0;
}

Going Beyond Sugar — action pipelines​

When the sugar API is not enough, use Arg directly with a custom action pipeline.

Actions are composed with |:

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

Each | appends a step to the pipeline. The explicit cli::Action<...> template form is equivalent:

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

The pipeline is split into three conceptual layers:

LayerNamespacePurpose
Conversioncli::conversion::*Parse the raw string into a typed value
Validationcli::validation::*Reject out-of-range or otherwise invalid values
Packingcli::pack::*Store the final value into the field

Use the sugar API for the common case and drop down to Arg only when you need stronger validation or custom conversion behavior.

For one-off lambda-based steps and reusable struct-based actions, see Actions & Pipelines: Defining Custom Pipeline Steps.