A lightweight header-only library for parsing command line arguments in an elegant manner. Argparse allows you to define variables as a one-liner without redefining their type or names while representing all the program arguments in a struct that can be easily passed to functions.
#include "argparse/argparse.hpp"
struct MyArgs : public argparse::Args {
std::string &anonymous = arg("an anonymous positional string argument");
std::string &src_path = arg("src_path", "a positional string argument");
int &k = kwarg("k", "A keyworded integer value");
float &alpha = kwarg("a,alpha", "An optional float value").set_default(0.5f);
bool &verbose = flag("v,verbose", "A flag to toggle verbose");
};
int main(int argc, char* argv[]) {
auto args = argparse::parse<MyArgs>(argc, argv);
if (args.verbose)
args.print(); // prints all variables
return 0;
}
Example output when setting the verbose
flag in the example above, it will print the capturing arguments with the args.print()
function:
$ ./argparse_test hello source -k 4 --verbose
arg_0(an anon...) : hello
src_path(a posit...) : source
-k : 4
-a,--alpha : 0.5
-v,--verbose : true
-?,--help : false
Argparse distinguishes 3 different types of arguments:
Type | Function |
---|---|
arg(help) |
anonymous positional arguments |
arg(key,help) |
named positional arguments |
kwarg(key,help,implicit) |
keyworded-arguments that require a key and a value, e.g. --variable 0.5 . |
flag(key,help) |
a boolean argument that is by default false , but can be set to true by defining it on the commandline (e.g. --verbose ) |
Argparse supports the following syntax:
--long
--long=value
--long value
-a
-ab
-abc=value
-abc value
Where on the last 2 lines, a
and b
are considered flags
, while c
is considered a kwarg
and is set to value
. In addition, an argument may be a comma-separated vector.
Args
and Kwargs
may have a default value, which will be used when the argument is not present on the commandline. These can be passed through the set_default
function, it accepts either a string or the type of the parameter itself:
std::string &dst_path = arg("An optional positional argument").set_default("output.png");
float &alpha = kwarg("a,alpha", "An optional float parameter with float as default").set_default(0.5f);
float &beta = kwarg("b,beta", "An optional float parameter with string as default").set_default("0.5");
std::vector<int> &numbers = kwarg("n,numbers", "An optional vector of integers").set_default(std::vector<int>{1,2});
std::vector<int> &values = kwarg("v,values", "An optional vector of integers, with string as default").set_default("3,4");
Kwargs
may have an implicit value, meaning that when the argument is present on the commandline, but no value is set, it will use the implicit value. Implicit values are passed as string.
int &k = kwarg("k", "An implicit int parameter", /*implicit*/"3");
float &alpha = kwarg("a,alpha", "A implicit float parameter", /*implicit*/"0.5");
std::vector<int> &numbers = kwarg("n,numbers", "A implicit int vector", /*implicit*/"1,2,3");
Examples:
On the commandline the implicit values can be overwritten by using the =
sign followed by the value. Examples:
$ argparse_test -k
k = 3
$ argparse_test -k=9
k = 9
$ argparse_test --numbers
numbers = 1,2,3
$ argparse_test --numbers=3,4,5
numbers = 3,4,5
Argparse supports std::vector
. There are 2 ways in which the vector can be read from the commandline, either a vector can be parsed from a comma-separated string, or by setting the multi_argument()
flag to aggregate multiple program arguments into the vector, e.g. when using the ./*
in bash to list all the files in a directory.
std::vector<int> &numbers = kwarg("n,numbers", "An int vector");
std::vector<std::string> &tags = kwarg("t,tags", "A word vector");
std::vector<std::string> &files = kwarg("files", "multiple arguments").multi_argument();
Example usage:
$ argparse_test --numbers 3,4,5,6
$ argparse_test --numbers=3,4,5,6
$ argparse_test --tags hello
$ argparse_test --tags="1st tag,2nd tag" # if the strings contain spaces
$ argparse_test --files a b c
$ argparse_test --files ./* # files will now contain a list of the files in the current directory
In case there are other positional arguments, Argparse will make sure that they are correctly assigned. For example, consider the following example:
std::string &A = arg("Source path");
std::vector<std::string> &B = arg("Variable paths").multi_argument();
std::string &C = arg("Last");
And the following input:
$ argparse_test a b b b c
Argparse will assign the non-multiple arguments first, such that A=a
, C=c
and B=b,b,b
In situations where setting a default value is not sufficient, Argparse supports std::optional
, and (smart)pointers, these can be used in situations where you'd like to distinguish whether an argument was set by the user. When declaring a raw pointer or a std::shared_ptr
, the default value for these are automatically set to nullptr
(or std::nullopt
for std::optional
).
std::shared_ptr<float> &alpha = kwarg("a,alpha", "An optional smart-pointer float parameter");
std::optional<float> &beta = kwarg("b,beta", "An optional float parameter with std::optional return");
float* &gamma = kwarg("g,gamma", "An optional raw pointer float parameter");
For example:
$ ./argparse_test --alpha 0.4
-a,--alpha : 0.4
-b,--beta : none
-g,--gamma : none
One of the reasons for creating this library was to natively support Enums using magic_enum. If it is found on the system, Argparse supports automatic conversion from commandline to enum
. Consider the following example:
enum Color {
RED,
BLUE,
GREEN,
};
struct MyArgs : public argparse::Args {
Color &color = kwarg("c,color", "An Enum input");
};
int main(int argc, char* argv[]) {
auto args = argparse::parse<MyArgs>(argc, argv);
args.print(); // prints all variables
return 0;
}
Running it will automatically convert the input to the Color
enum (case-insensitive):
$ ./argparse_test --color blue
It will only accept a is_valid input for enums, and the help-tip will display the available options for this enum:
$ ./argparse_test --help
...
-c,--color : An Enum input [allowed: <red, blue, green>, required]
...
Argparse supports subcommands by creating a separate argparse::Args
instance for them. Argparse currently supports 2 ways of defining the logic for subcommands:
- By defining an
int run()
function within the Subcommand struct that will be executed once the user requests the specified subcommand. - By defining an if statement to check if the subcommand was called and then executing the logic. By doing so you can keep the logic of the subcommand within the main function.
To add the subcommand to your program, add a line to your main program arguments specifying the class and the name of the subcommand using the
subcommand
function as shown below (using theint run()
method):
struct CommitArgs : public argparse::Args {
bool &all = flag("a,all", "Tell the command to automatically stage files that have been modified and deleted, but new files you have not told git about are not affected.");
std::string &message = kwarg("m,message", "Use the given <msg> as the commit message.");
// This will be executed via `args.run_subcommands()` if the user calls the `commit` subcommand
int run() override {
std::cout << "running commit with the with the following message: " << this->message << std::endl;
return 0;
}
};
struct PushArgs : public argparse::Args {
std::string &source = arg("Source repository").set_default("origin");
std::string &destination = arg("Destination repository").set_default("master");
void welcome() override {
std::cout << "Push code changes to remote" << std::endl;
}
// This will be executed via `args.run_subcommands()` if the user calls the `push` subcommand
int run() override {
std::cout << "running push with the following parameters" << std::endl;
print();
return 0;
}
};
struct MyArgs : public argparse::Args {
bool &version = flag("v,version", "Print version");
CommitArgs &commit = subcommand("commit");
PushArgs &push = subcommand("push");
};
int main(int argc, char* argv[]) {
auto args = argparse::parse<MyArgs>(argc, argv);
if (args.version) {
std::cout << "argparse_subcomands version 1.0.0" << std::endl;
return 0;
}
return args.run_subcommands();
}
Alternatively, you can define the logic for the subcommand within the main function as shown below without having to define the int run()
method in the subcommand struct
//
int main(int argc, char* argv[]) {
auto args = argparse::parse<MyArgs>(argc, argv);
if (args.commit.is_valid) {
std::cout << "running commit with the with the following message: " << args.commit.message << std::endl;
} else if (args.push.is_valid) {
std::cout << "running push with the following parameters" << std::endl;
args.push.print();
} else {
std::cout << "No subcommand given" << std::endl;
}
return 0
}
Usage of the above example:
$ ./argparse_subcommands commit -am "hello world"
running commit with the with the following message: hello world
$ ./argparse_subcommands push origin dev
running push with the following parameters
arg_0(Source ...) : origin
arg_1(Destina...) : dev
--help : false
$ ./argparse_subcommands commit --help
Usage: commit [options...]
Options:
-a,--all : Tell the command to automatically stage files that have been modified and deleted, but new files you have not told git about are not affected. [implicit: "true", default: false]
-m,--message : Use the given <msg> as the commit message. [required]
--help : print help [implicit: "true", default: false]
When using a custom class, Argparse will try to create the class using the constructor with an std::string
as parameter. See examples/argparse_example.cpp
for an example using a custom class.
When invalid arguments are passed to the commandline, argparse will simply print the error and exit the program by default. However, you can choose to let argparse throw a catchable exception instead by setting the raise_on_error
flag to true
on the parse
function. For example:
int main(int argc, char* argv[]) {
try {
auto args = argparse::parse<MyArgs>(argc, argv, /*raise_on_error*/ true);
} catch (const std::runtime_error &e) {
std::cerr << "failed to parse arguments: " << e.what() << std::endl;
return -1;
}
return 0;
}
The --help
is automatically added in ArgParse. Consider the following example usage when executing argparse_test
(int examples/argparse_example.cpp
):
$ ./argparse_example --help
Welcome to Argparse
Usage: ./argparse_example arg_0 arg_1 [options...]
arg_0 : Source path [required]
arg_1 : Destination path [default: default_destination]
Options:
-k : An implicit int parameter [implicit: "3", required]
-a,--alpha : An optional float parameter with default value [default: 0.6]
-b,--beta : An optional float parameter with std::optional return [default: none]
-n,--numbers : An int vector, comma separated [required]
--files : multiple arguments [required]
-c,--color : An Enum input [allowed: <red, blue, green>, required]
-v,--verbose : A flag to toggle verbose [implicit: "true", default: false]
--help : print help [implicit: "true", default: false]
You may notice the Welcome to Argparse
message, this message was created by overwriting the welcome
function (see examples/argparse_example.cpp
)
In case it fails to parse the input string, it will display an error and exit. E.g. here we'll set k
to be notanumber
:
$ ./argparse_test src dst -k notanumber
Invalid argument, could not convert "notanumber" for -k (An implicit int parameter)
Since it is a header-only library, you can simply copy the include/argparse.hpp
file into your own project.
Alternatively, you can build&install it using the following commands:
mkdir build && cd build
cmake ..
make
sudo make install
It can be included in your cmake project as follows:
find_package(argparse REQUIRED)
target_link_libraries(${PROJECT_NAME} PUBLIC argparse::argparse)
-
Why references?
This is a good question. In order to support implicit parameters, multiple parameters and invariance to the order of input, Argparse needs to know all the possible input arguments before assigning them. And since the goal of this library is to define a variable only once, I needed a way to modify the contents of the returned value after it has seen all the arguments. Returning by reference makes this possible. In the future, when guaranteed copy-elision is implemented for primitive types, the reference can be removed.