This library is for you if:
- you need to parse command line arguments in your project,
- you want a simple syntax and lightweight library,
- you can use C++20 in your project,
An example is worth a thousand words. Here is the syntax for defining and parsing flags.
#include "xdk/flags/flags.h"
namespace {
constexpr std::string_view kHelp = R"(
Runs a server on the given port (default is 8080).
--port : specify the port to use.
--help/-h : prints this help.
)";
}
int main(int argc, char** argv) {
struct Flags : xdk::Flags<Flags> {
Flag<"--port", int> port{8080};
Flag<"--help", bool, "-h"> help;
};
auto [flags, args, errors] = Flags::Parse(argc, argv);
if (errors) {
std::cerr << "Invalid arguments:\n" << errors;
return EXIT_FAILURE;
}
if (args.size() > 1) {
std::cerr << args[0] << " doesn't take any argument.\n";
return EXIT_FAILURE;
}
if (flags.help) {
std::cout << args[0] << "\n" //
<< args[0] << " --port 8080\n" //
<< kHelp.substr(1); // skip first newline.
return EXIT_SUCCESS;
}
std::cout << "Running on port " << flags.port.value << `\n';
// ...
return EXIT_SUCCESS;
}Flags are simple C++ object whose names are unconstrained by the flag's name or
alias. They can be declared locally in the main() function. There are no
macros involved. Validation and error reporting is explicit, as well as help
string.
The following typical features for command line flags are supported:
- Supports long/short forms for flags e.g.
--long/-lvia aliases. - Supports repeated flags, e.g.
-l one -l two -l threeas a{"one", "two", "three"}. - Supports optional flags.
- Supports default flag values.
- Supports
--to stop flag parsing - Reports invalid values that can't be streamed into the flag's type.
- Reports missing flag values.
The following other typical features flags are not supported:
- No mechanism for help strings.
- No mechanism for complex validation.
- No mechanism for commands and subcommands.
- No support for merging of flags, e.g.
-l -twritten as-lt. - No support for
=i.e.--flag=value.
If you want those features, you need powerful - but more complex - library such as CLI11.
Displaying help strings automatically is not an easy task. Nice documentation must be nicely formatted, which depend on the terminal's width. Documentation may be localized. This is better left to dedicated tools, like man pages.
For simple program, you can simply maintain the documentation as a string in your main file. You can use an ordered map associating flag names to their description. If you want to make sure it contains an entry for each flags, you can use the introspection API.
The introspection API is also useful for producing a list of completions for shell integration of your binary.
The Flags::Parse method only reports when:
- it can not parse a command line argument into a flag's type;
- it can not find a value for a non-boolean flag.
For example, if you have a Flag<"--port", int>, and you try to parse:
--port not_an_intor--port--port --other_flag
Then you will get a parsing error.
There is no other validation, such as mandatory flags, checking that a value is in a range, or that some flags are mutually exclusive. Such validation is best performed by you directly in code, at the beginning of the program. As for help strings above, this also allows for localized error reporting.
Assuming you are using Bazel, add the following to your
MODULE.bzl. Make sure to update to the latest
commit.
bazel_dep(name = "xdk_flags")
git_override(
module_name = "xdk_flags",
commit = "34a5c32ae3a4db2ee956ec5a1bfa2d1c46751d8a",
remote = "https://github.com/Xadeck/flags.git",
)You also need to enable C++20 for compilation. Unless you are comfortable
setting a toolchain, the easiest way to do so is to add the following to your
.bazelrc:
build --cxxopt=-std=c++20The library is of the opinion that flags should only be defined as the entry
point of a binary, in its main() function. Mechanism like Abseil
flags are convenient for deep
dependency injection but they come at a maintenance cost. This library doesn't
try to wade in those waters, so we'll assume you have a cc_binary with one
source defining a main() function. In your BUILD file, add a dependency to
this library to the binary, for example:
cc_binary(
name = "server",
srcs = ["server.cc"],
deps = [
"@xdk_flags//xdk/flags",
# ...
],
)In your main file, here server.cc, include the header:
#include "xdk/flags/flags.h"In your code, define a struct that inherits from xdk::Flags -notice the
plural- using itself as the template parameter, a pattern known as
CRTP. You
can call it Flags too if you want, and you can define it inside your main
function, or in the anonymous namespace.
struct Flags : xdk::Flags<Flags> {
// ...
};That struct must have only fields of the Flag template type 1 -notice
the singular- whose first parameter is a string matching what will be used on
the command line, and second parameter is a type.
struct Flags : xdk::Flags<Flags> {
Flag<"--port", int> port;
// ...
};The string must not be empty, must start with a - and not be the exact string
--. This is enforced at compile-time with a static assert. The name of the
field does not need to match the flag, so you can use a descriptive variable
name, and a short flag name.
struct Flags : xdk::Flags<Flags> {
Flag<"-p", int> port;
// ...
};You can use any moveable type that supports operator>>(std::istream&), any
std::vector of such a type, and any std::optional of such a type. Non
copyable and non default constructible types are usable.
The Flag class accepts an optional third template parameter, which is also a
string, must also not be empty, start with a - and not be the exact string
--. This can be used to specify an alias for the flag. The typical
usage is to define a short version of the flag:
struct Flags : xdk::Flags<Flags> {
Flag<"--port", int, "-p"> port;
// ...
};If not specified, this parmater defaults to the value of the first parameter.
Finally, the Flag constructor supports all constructors for the associated
type, so you can initialize the field with a default value:
struct Flags : xdk::Flags<Flags> {
Flag<"--center", std::pair<float, float>> center{1.f, 5.f};
// ...
};Once you have defined your Flags class, you can use its Parse() method to
get an instance, initialized from command line arguments. It returns a tuple of
3 elements, that you typically destructure like this:
int main(int arc, char** argv) {
// ...
auto [flags, args, errors] = Flags::Parse(argc, argv);The first element is an instance of your Flags class. The second element is a
std::vector<const char*>. The third element is akin to a vector of error
tuples, which will be described later. Parsing always returns an instance of
your Flags type.
- If
errorsis empty, the fields offlagshave all been properly initialized from the command line arguments, andargscontains all the arguments that were neither flag names nor flag values. If your application doesn't support positional arguments, simply report and error ifargsis not empty. - If
errorsis not empty, flags are invalid, and the corresponding fields have an undefined value. You must report errors to the user, as described later on this page.
Once you have the flags instance, you access the values of command line
arguments simply through its fields, via the value accessor. Flag<>
implicitly converts to the underlying type, and supports the -> operator, so
all forms below are valid:
struct Flags : xdk::Flags {
Flag<"--file", std::string> file;
};
auto [flags, _, errors] = Flags::Parse(argc, argv);
std::string s = flags.file.value; // explicit access of value
std::string t = flags.file; // implicit conversion to underlying type
size_t l = flags.file->size(); // access of members via ->Flag values can be modified, but we recommend treating them as constants.
If a flag is of type std::vector<T> where T is moveable and supports
operator>>(std::istream&, then it will contain an element per instance of the
flag on the command line. That is, if you have:
struct Flags : xdk::Flags<Flags> {
Flag<"--value", std::vector<int>, "-v"> values;
};Then parsing --value 2 -v 1 - v 3 will produce flags.values = {2,1,3}. Interestingly, you
can sort the vector in place:
If a flag must be specified on the command line, because there is no
reasonable default value, use a std::optional for the flag's type, and check
that is has a value.
If you need, to be able to determine if a flag was specified in the command
line, and yet provide a default value, then use the std::optional::value_or
method, as in the example below.
struct Flags : xdk::Flags<Flags> {
Flag<"--port", std::optional<int>, "-p"> port;
// ...
};
auto [flags, _, errors] = Flags::Parse(argc, argv);
if (!flags.port.has_value()) std::cout << "Using default port 8080.\n";
int port = flags.port.value_or(8080);If you can use C++ 23's
or_else, you
can combine the above into:
int port = flags.port.or_else([]() {
std::cout << "Using default port 8080.\n";
return 8080;
});When parsing the flags as follows:
int main(int arc, char** argv) {
// ...
auto [flags, args, errors] = Flags::Parse(argc, argv);The errors object is akin to an std::vector with a convenient
conversion-to-boolean operator, and output-to-stream operator. This allows for
a minimal approach to report errors.
if (errors) { // of `if (!errors.empty()) `
std::cerr << "Invalid arguments:\n" << errors;
return EXIT_FAILURE;
}For a custom reporting of errors, traverse the errors struct, which has 3
fields pos, arg and val. The last one distinguishes the error cases:
- unknown flag:
valequals toFlagInfo::Error::kUnknown - invalid flag value:
valequals to a string that can't be parsed into the given flag's type - missing flag value:
valequals tonullptr
In each case, pos is the index in argv of the flag causing an error, and
arg is argv[pos].
The Parse() method takes an optional third argument to indicate that unknown
flags are not errors, and are simply added to args instead.
This pattern is useful to implement a sub-command mechanism - e.g. git status
or git commit, where the first element of args is a command. In such a
case, you have flags for the main command (git) and flags for each possible
sub-command (status or commit).
To handle such a case, use multiple Flags classes, one for the main command
and one for each sub-command. Parse argv using the first one and ignoring
unknown flags. Then identify the subcommand using args[0], and parse the
returned args object with its flags, updating the existing args and
errors. Here is a pseudo-code, illustrating the approach:
GitFlags : Flags<GitFlags> {
Flag<"-v", bool> verbose{false};
};
auto [git, args, errors] = GitFlags::Parse(argv, false);
if (args[0] == "status"sv) {
struct StatusFlags : Flags<StatusFlags> {
Flag<"-s", bool> short_format{false};
};
auto status = StatusFlags::Parse(args, errors);
// ..
// use `status.short_format` and `git.verbose` flags
// ..
}
if (args[0] == "commit"sv) {
struct CommitFlags : Flags<CommitFlags> {
Flag<"-a", bool> all{false};
};
auto commit = CommitFlags::Parse(args, errors);
// ..
// use `commit.all` and `git.verbose` flags
// ..
}You can use the Flags::FlagInfos() method on your Flags type to get a
vector of FlagInfo objects, which are structs with 3 fields: name, alias
and type. The first two ones are clear. The last one is a
std::type_info pointer
describing a flag's underlying type.
This API allows you to store documentation for command line flags into a map, or a set of external files, or what ever you like, and implement a test that all flags have a corresponding docstring, and conversely.
Footnotes
-
not respecting this constraint is undefined behavior, most likely a crash. ↩