A lightweight C++ command-line argument parsing utility.
CParseParse is designed to be a lightweight C++ argument parsing solution with a few key attributes:
- Employs efficient multiple-phase argument parsing
- Supports various types of command-line arguments
- Implemented as a template library for easy install and use
- Compatible with C++11/14/17/20
- Permissive MIT License
Currently supported features
- Positional arguments:
$ ./my-program with some args
- Optional single-value arguments (both short and long):
$ ./my-program -l 1 --log-level 2
- Optional boolean flag arguments:
$ ./my-program --verbose
- Optional append-style arguments:
$ ./my-program --exclude-pattern dir/pat1* --exclude-pattern dir/pat2*
- Configurable program description and per-argument help text
- Configurable handler for
-h/--help
- Default values for omitted arguments
Features planned for the future
- Option to parse from configuration file instead of command-line
- Sub-parser sections
Start by cloning the repository:
git clone [email protected]:MatthewRasa/cparseparse.git
Then move into the project directory and install the headers via make:
make install
The setup is now complete! The installed files can be found under <PREFIX>/cparseparse/
(the default PREFIX
is /usr/local
).
The project files can also be uninstalled via make:
make uninstall
You can continue with the quick-start by heading directly to the Tutorial section. Alternatively, you can proceed with the additional post-setup steps below.
The unit tests included in the repository are useful for verifying that everything is working properly. If you with to run the tests, Catch2 is a required dependency.
The run-tests
target will build and run the tests:
make run-tests
Catch2 will print a test report detailing the results. A successful run should yield output similar to below:
make -C test
make[1]: Entering directory '/home/matt/Projects/cparseparse/test'
make[1]: 'unit-tests' is up to date.
make[1]: Leaving directory '/home/matt/Projects/cparseparse/test'
./test/unit-tests
===============================================================================
All tests passed (128 assertions in 6 test cases)
A sample program demonstrating use of CParseParse is included in the repository at example/src/sort-string.cc.
The sample program can be built with the example
target:
make example
Once built, the program executable will be placed under example/
:
./example/sort-string --help
This tutorial will walk through the features of CParseParse by writing a simple toy program. Installing CParseParse in the Setup section is a prerequisite.
We'll start by writing the canonical Hello World C++ program in a file called hello.cc
:
#include <iostream>
int main(int argc, char *argv[]) {
std::cout << "Hello World!" << std::endl;
return 0;
}
Compile and run the program as usual. E.g. with gcc:
g++ -Wall -Wextra -std=c++20 -o hello hello.cc
./hello
--------------------OUTPUT--------------------
Hello World!
To begin using the argument parser, we'll modify the Hello World program to instantiate an Argument_Parser
instance:
#include <cparseparse/argument-parser.h> // Include header file
#include <iostream>
using namespace cpparse; // Use the CParseParse namespace
int main(int argc, char *argv[]) {
Argument_Parser parser{}; // Instantiate parser instance
parser.set_description("Print 'Hello World!' to the console"); // Set the program description (optional)
try {
parser.parse_args(argc, argv); // Parse the command-line arguments
std::cout << "Hello World!" << std::endl;
} catch (const std::runtime_error &ex) {
std::cerr << ex.what() << std::endl;
return 1;
}
return 0;
}
While the argument parser is now present, no paramters have been specified. This means that when we compile and run the program as before with no arguments, we end up with the same result:
g++ -Wall -Wextra -std=c++20 -o hello hello.cc
./hello
--------------------OUTPUT--------------------
Hello World!
However, the Argument_Parser
instance is configured with a -h/--help
parameter by default, which we could pass as an argument. The default behavior in this case is to print the program help text and exit:
./hello -h
# OR
./hello --help
--------------------OUTPUT--------------------
Usage: ./hello [options]
Print 'Hello World!' to the console
Options:
-h, --help display this help text
One important point to note is that the parse_args()
call is wrapped in a try
/catch
statement. This is because parse_args()
may throw a runtime exception (e.g. when the user passes an invalid argument) and we want to handle this scenario gracefully. In our example program, we simply print the exception to the console and exit with a non-zero exit code:
./hello --verbose
--------------------OUTPUT--------------------
./hello: invalid option 'verbose', pass --help to display possible options
The exit code returned by echo $?
is 1
.
We'll now take a look at parsing positional arguments in our program. We'll change the concept of the original program slightly to print out a name instead of simply 'Hello World!'. We'll also make it so that the amount of exclamation points (!) is configurable. We can add positional parameters for the name and point count like so:
#include <cparseparse/argument-parser.h>
#include <iomanip> // For std::setfill and std::setw
#include <iostream>
using namespace cpparse;
int main(int argc, char *argv[]) {
Argument_Parser parser{};
parser.set_description("Say hello to someone");
auto &name_arg = parser.add_positional("name"); // Add positional parameter for 'name'
auto &points_arg = parser.add_positional("points"); // Add positional parameter for 'points'
try {
parser.parse_args(argc, argv);
const auto name = name_arg.as_type<std::string>(); // Parse 'name' as a string
const auto points = points_arg.as_type<unsigned int>(); // Parse 'points' as an unsigned int
std::cout << "Hello " << name << std::setfill('!') << std::setw(points) << "" << std::endl;
} catch (const std::runtime_error &ex) {
std::cerr << ex.what() << std::endl;
return 1;
}
return 0;
}
The add_positional()
function is used to declare a positional parameter with a given name and return a reference to the corresponding info object. After parse_args()
is called, we can use each info object's as_type()
template function to obtain the values of the user-supplied arguments. Similar to parse_args()
, the as_type()
function may throw a runtime exception if the parsing fails, so it must be wrapped in a try
/catch
statement.
If we compile and run this program same as before with no arguments, we'll now be told to add an additional argument:
g++ -Wall -Wextra -std=c++20 -o hello hello.cc
./hello
--------------------OUTPUT--------------------
./hello: requires positional argument 'name'
Similarly, only supplying one of the two necessary arguments yields an error:
./hello Matt
--------------------OUTPUT--------------------
./hello: requires positional argument 'points'
Only when we supply both positional arguments will the program execute:
./hello Matt 3
--------------------OUTPUT--------------------
Hello Matt!!!
Note that the type passed to the as_type()
template tells the argument parser how to parse this argument. In the case of name
, std::string
is passed which tells the parser to treat the name string as is. For points
, passing unsigned int
tells the parser to attempt to read points
as a unsigned integer. If the user passes a non-integer or a negative integer, the parsing will fail:
./hello Matt non-int
# OR
./hello Matt -1
--------------------OUTPUT--------------------
./hello: 'points' must be in range [0,4294967295]
The following types can be passed to as_type()
:
bool
char
- Any signed or unsigned integral type (
short
,int
,long
,std::uint32
, etc.) float
/double
std::string
By checking the program help with --help
, we can see that name
and points
are now listed as required arguments:
Usage: ./hello [options] <name> <points>
Say hello to someone
Positional arguments:
name
points
Options:
-h, --help display this help text
Unfortunately, this help text tells the user nothing about what name
and points
are supposed to be. We can fix this by adding help text for these parameters. The info object returned from add_positional()
provides the help()
function. We can modify our add_positional()
usage like so:
...
auto &name_arg = parser.add_positional("name").help("name of person to greet");
auto &points_arg = parser.add_positional("points").help("number of exclamation points");
...
The help()
function returns the same info object reference, which allows for convenient function call chaining.
Passing --help
now gives us this information:
Usage: ./hello [options] <name> <points>
Say hello to someone
Positional arguments:
name name of person to greet
points number of exclamation points
Options:
-h, --help display this help text
In this section, we'll take a look at parsing various types of optional arguments. There are three main forms that optional arguments can take:
We'll begin by looking at single-value arguments.
In our sample program, say that we no longer wish to require that the user provide a number of exclamation points. It might be more convenient to have a default number of points present, and then give the user an option to override this default. This can be accomplished by changing points
to an optional parameter:
#include <cparseparse/argument-parser.h>
#include <iomanip>
#include <iostream>
using namespace cpparse;
int main(int argc, char *argv[]) {
Argument_Parser parser{};
parser.set_description("Say hello to someone");
auto &name_arg = parser.add_positional("name").help("name of person to greet");
// Replace 'add_positional()' with 'add_optional()'
auto &points_arg = parser.add_optional("-p", "--points").help("number of exclamation points");
try {
parser.parse_args(argc, argv);
const auto name = name_arg.as_type<std::string>();
const auto points = points_arg.as_type<unsigned int>(1); // Use '1' as the default
std::cout << "Hello " << name << std::setfill('!') << std::setw(points) << "" << std::endl;
} catch (const std::runtime_error &ex) {
std::cerr << ex.what() << std::endl;
return 1;
}
return 0;
}
The first difference from before is the syntax of the add_optional()
call. Whereas add_positional()
takes a single name option, add_optional()
takes both a short name and a long name. The short name is a single character preceded by -
and the long name is the full argument name preceded by --
. Alternatively, the short name can be omitted to force the user to refer to the argument only by the long name.
The second difference is that the call to as_type()
now takes the value 1
, which is used as the default number of points when not specified by the user.
We can print the program help text to see the new requirements:
./hello --help
--------------------OUTPUT--------------------
Usage: ./hello [options] <name>
Say hello to someone
Positional arguments:
name name of person to greet
Options:
-h, --help display this help text
-p, --points POINTS number of exclamation points
We can now run the program with only the name
argument:
./hello Matt
--------------------OUTPUT--------------------
Hello Matt!
Alternatively, we can use -p/--points
to change the number of exclamation points. Optional arguments can be specified anywhere in the invocation, either before or after the positional arguments:
./hello Matt -p 7
# OR
./hello Matt --points 7
# OR
./hello -p 7 Matt
--------------------OUTPUT--------------------
Hello Matt!!!!!!!
To use other types of the optional arguments, we must pass a special selector value to add_optional()
. Since single-value arguments are the default type, we were able to omit this selector value before, but this time we'll include it for completeness. Let's add a flag argument called --show-time
that will print the current time to the user:
#include <cparseparse/argument-parser.h>
#include <chrono> // For time functions
#include <iomanip>
#include <iostream>
using namespace cpparse;
using Opt_Type = Optional_Info::Type; // Alias for Optional_Info::Type
using Clock = std::chrono::system_clock; // Alias for std::chrono::system_clock
int main(int argc, char *argv[]) {
Argument_Parser parser{};
parser.set_description("Say hello to someone");
auto &name_arg = parser.add_positional("name").help("name of person to greet");
auto &points_arg = parser.add_optional("-p", "--points", Opt_Type::SINGLE) // Use type SINGLE
.help("number of exclamation points");
auto &show_time_arg = parser.add_optional("--show-time", Opt_Type::FLAG) // Use type FLAG
.help("display current system time");
try {
parser.parse_args(argc, argv);
const auto name = name_arg.as_type<std::string>();
const auto points = points_arg.as_type<unsigned int>(1);
const auto show_time = show_time_arg.as_type<bool>(); // Parse flag argument
std::cout << "Hello " << name << std::setfill('!') << std::setw(points) << "" << std::endl;
if (show_time) {
const auto time = Clock::to_time_t(Clock::now());
std::cout << "The current time is: " << std::ctime(&time);
}
} catch (const std::runtime_error &ex) {
std::cerr << ex.what() << std::endl;
return 1;
}
return 0;
}
Flag arguments are boolean switches that become set when specified by the user. Note that --show-time
has no default value specified in as_type()
. This is because flag arguments have an implicit default value of false
.
By running the program with the --show-time
argument, we'll see the system time printed:
./hello --show-time Matt
--------------------OUTPUT--------------------
Hello Matt!
The current time is: Wed Nov 24 14:02:12 2021
The final form for optional arguments is append-style. This form allows the user to specify multiple values for a single argument. In our sample program, let's imagine we want to have it also greet some friends that are with us. We can add an optional append-style parameter -f/--friend
that will allow us to specify one or more friends:
#include <cparseparse/argument-parser.h>
#include <chrono>
#include <iomanip>
#include <iostream>
using namespace cpparse;
using Opt_Type = Optional_Info::Type;
using Clock = std::chrono::system_clock;
int main(int argc, char *argv[]) {
Argument_Parser parser{};
parser.set_description("Say hello to someone");
auto &name_arg = parser.add_positional("name").help("name of person to greet");
auto &points_arg = parser.add_optional("-p", "--points", Opt_Type::SINGLE)
.help("number of exclamation points");
auto &friends_arg = parser.add_optional("-f", "--friend", Opt_Type::APPEND) // Use type APPEND
.help("friend to greet as well");
auto &show_time_arg = parser.add_optional("--show-time", Opt_Type::FLAG)
.help("display current system time");
try {
parser.parse_args(argc, argv);
const auto name = name_arg.as_type<std::string>();
const auto points = points_arg.as_type<unsigned int>(1);
const auto show_time = show_time_arg.as_type<bool>();
const auto friends = friends_arg.as_type_all<std::string>(); // Parse all friend values
std::cout << "Hello " << name;
for (const auto &f : friends)
std::cout << ", " << f;
std::cout << std::setfill('!') << std::setw(points) << "" << std::endl;
if (show_time) {
const auto time = Clock::to_time_t(Clock::now());
std::cout << "The current time is: " << std::ctime(&time);
}
} catch (const std::runtime_error &ex) {
std::cerr << ex.what() << std::endl;
return 1;
}
return 0;
}
The add_optional()
invocation for -f/--friend
should look pretty similar to what we've seen before; the only difference being that we use APPEND
as the type. What's more different is how we go about parsing the new argument. Since the argument can hold multiple values, we need parsing functions capable of returning an entire list of values. The as_type_all()
function used above does exactly this; all values given for -f/--friend
are parsed as the specified type (std::string
) and returned in a std::vector
. Some other useful functions for dealing with append-style arguments are count()
and as_type_at()
, which we could have used in place of as_type_all()
.
Now, we can run the program again and specify some friends (either with -f
or --friend
):
./hello Matt -f April --friend Lillian
--------------------OUTPUT--------------------
Hello Matt, April, Lillian!
By default, the argument parser is initialized with an implicit -h/--help
flag. When the user passes this flag, the program help text is printed and std::exit(0)
is called. While this is a common way to handle the help flag, it may be desirable to override this default behavior in some instances.
If we want to modify what happens when the user passes the help flag, we can use the set_help_handler()
function in Argument_Parser
to provide our own callback. For example, to display a copyright notice after the usual help text:
...
Argument_Parser parser{};
parser.set_help_handler([](const Argument_Parser &parser) {
parser.print_help();
std::cout << std::endl << "Copyright me 2021" << std::endl;
std::exit(0);
});
...
We can then run the program with the help flag to invoke our custom handler:
./hello --help
--------------------OUTPUT--------------------
Usage: ./hello [options] <name>
Say hello to someone
Positional arguments:
name name of person to greet
Options:
-h, --help display this help text
-p, --points POINTS number of exclamation points
-f, --friend FRIEND friend to greet as well
--show-time display current system time
Copyright me 2021
Alternatively, if we wish to disable the implicit -h/--help
flag altogether, we can set the auto_help()
option to false
. This is done by passing a configuration options builder instance to Argument_Parser
and invoking the auto_help()
function:
...
Argument_Parser parser{Argument_Parser::Options{}.auto_help(false)};
...
CParseParse is documented via Doxygen and hosted via GitHub Pages. Check there for the full API reference.