Command-line argument parser for C++20
The goal of the project was to create a light, intuitive and simple to use command-line argument parser library for the C++20
and newer standards.
The CPP-AP
library does not require installing any additional tools or heavy libraries, like with boost::program_options
. Much like with the Doctest
framework - the only thing you need to do is copy the argument_parser.hpp
file into the include directory of your project and you're set to go.
Note
v1.0 of the library has been developed for the Team Programming course at the Wrocław University of Science and Technology.
Faculty: W04N - Faculty of Information and Communication Technology
Field of study: Algorithmic Computer Science
The project has received the 1st place at the 2024 CreatiWITy competition organized by the faculty. The article in Polish can be found on the faculty website. Please note that this is not a technical article :)
There are 3 main ways to include the CPP-AP library into a C++ project:
For CMake projects you can simply fetch the library in your CMakeLists.txt
file:
cmake_minimum_required(VERSION 3.12)
project(my_project LANGUAGES CXX)
# Include FetchContent module
include(FetchContent)
# Fetch CPP-AP library
FetchContent_Declare(
cpp-ap
GIT_REPOSITORY https://github.com/SpectraL519/cpp-ap.git
GIT_TAG master # here you can specify the desired tag or branch
)
FetchContent_MakeAvailable(cpp-ap)
# Define the executable for the project
add_executable(my_project main.cpp)
set_target_properties(my_project PROPERTIES
CXX_STANDARD 20
CXX_STANDARD_REQUIRED YES
)
# Link against the cpp-ap library
target_link_libraries(my_project PRIVATE cpp-ap)
If you do not use CMake you can dowload the desired library release, extract it in a desired directory and simply add the <cpp-ap-dir>/include
to the include paths of your project.
The core of the library is a single header file so to be able to use the library you can simply download the argument_parser.hpp
header and paste it into the include directory of your project.
Important
To actually use the library in your project simply include the single header in you main.cpp
file:
#include <ap/argument_parser.hpp>
To use the argument parser in your code you need to use the ap::argument_parser
class.
The parameters you can specify for a parser's instance are:
- Program name
- Program description
- Arguments
ap::argument_parser parser;
parser.program_name("Name of the program")
.program_description("Description of the program");
The parser supports both positional and optional arguments. Both argument types are identified by their names represented as strings. Arguments can be defined with only a primary name or with a primary and a secondary (short) name.
Note
The basic rules of parsing positional and optional arguments are described in the Parsing arguments section.
To add an argument to the parameter's configurations use the following syntax:
parser.add_<positional/optional>_argument<value_type>("argument");
or
parser.add_<positional/optional>_argument<value_type>("argument", "a");
Note
The library supports any argument value types which meet the following requirements:
- The
std::ostream& operator<<
is overloaded for the value type - The value type has a copy constructor and an assignment operator
Important
If the value_type
is not provided, std::string
will be used.
You can also add boolean flags:
parser.add_flag("enable_some_option", "eso").help("enables option: some option");
/* equivalent to:
parser.add_optional_argument<bool>("enable_some_option", "eso")
.default_value(false)
.implicit_value(true)
.nargs(0)
.help("enables option: some option");
*/
Boolean flags store true
by default but you can specify whether the flag should store true
or false
when used:
parser.add_flag<false>("disable_another_option", "dao").help("disables option: another option");
/* equivalent to:
parser.add_optional_argument<bool>("disable_another_option", "dao")
.default_value(true)
.implicit_value(false)
.nargs(0)
.help("disables option: another option");
*/
Common parameters
Parameters which can be specified for both positional and optional arguments include:
-
help
- the argument's description which will be printed when printing the parser class instance.parser.add_positional_argument<std::size_t>("number", "n") .help("a positive integer value");
-
choices
- a list of valid argument values. Thechoices
parameter takes aconst std::vector<value_type>&
as an argument.parser.add_optional_argument<char>("method", "m").choices({'a', 'b', 'c'});
Important
To use the choices
the value_type
must overload the equaility comparison operator: ==
;
-
action
- a function performed after reading an argument's value.Actions are represented as functions, which take the argument's value as an argument. There are two types of actions:
- Void actions -
void(value_type&)
- Valued actions -
value_type(const value_type&)
The default action is an empty void function.
Actions can be used to modify a value parsed from the command-line:
parser.add_optional_argument<double>("denominator", "d") .action<ap::void_action>([](double& value) { value = 1. / value; });
or en equivalent valued action:
parser.add_optional_argument<double>("denominator", "d") .action<ap::valued_action>([](const double& value) { return 1. / value; });
Actions can also be used to perform some value checking logic, e.g. the predefined
check_file_exists
which checks if a file with a given name exists:parser.add_optional_argument("input", "i") .action<ap::void_action>(ap::action::check_file_exists());
- Void actions -
Optional argument specific parameters
-
required
- if this option is set for an argument, failure of parsing it's value will result in an error.parser.add_optional_argument("output", "o").required();
-
bypass_required
- if this option is set for an argument, parsing it's value will overrite therequired
option for other optional arguments and all positional arguments. -
nargs
- sets the allowed number of values to be parsed for an argument. This can be set as a:- Concrete number:
parser.add_optional_argument("input", "i").nargs(1);
- Bound range:
parser.add_optional_argument("input", "i").nargs(1, 3);
- Partially bound range:
parser.add_optional_argument("input", "i").nargs(ap::nargs::at_least(1)); // n >= 1 parser.add_optional_argument("input", "i").nargs(ap::nargs::more_than(1)); // n > 1 parser.add_optional_argument("input", "i").nargs(ap::nargs::less_than(5)); // n < 5 parser.add_optional_argument("input", "i").nargs(ap::nargs::up_to(5)); // n <= 5
- Unbound range:
parser.add_optional_argument("input", "i").nargs(ap::nargs::any());
- Concrete number:
Note
The default nargs value is 1
.
-
default_value
- the default value for an argument which will be used if no values for this argument are parsedparser.add_opitonal_argument("output", "o").default_value("out.txt");
-
implicit_value
- a value which will be set for an argument if only it's flag is parsed from the command-line but no value follows// program.cpp parser.add_optional_argument("save", "s") .implicit_value("out.txt") .help("save the program's output to a file");
In this example if you run the program with only a
-s
or--save
flag and no value, the value will be set toout.txt
.
The CPP-AP
library has a few default arguments defined. To add a default argument to the parser use the following:
// add positional arguments - pass a std::vector of default positional arguments
parser.default_positional_arguments({...});
// here ... represents a list of ap::default_argument::positional values (or alternatively ap::default_posarg)
// add optional arguments - pass a std::vector of default optional arguments
parser.default_positional_arguments({...});
// here ... represents a list of ap::default_argument::optional values (or alternatively ap::default_optarg)
The supported default arguments are:
-
positional::input
:// equivalent to: parser.add_positional_argument<std::string>("input") .action<ap::void_action>(ap::action::check_file_exists()) .help("Input file path");
-
positional::output
:// equivalent to: parser.add_positional_argument("output").help("Output file path");
-
optional::help
:// equivalent to: parser.add_flag("help", "h").bypass_required().help("Display help message");
Note
As of now the on flag action functionality is not implemented in the library - this will be added in a future release.
To properly use the help argument in the current release add the following right beneath the parser.parse_args(argc, argv)
try-catch block:
if (parser.value<bool>("help")) {
std::cout << parser << std::endl;
std::exit(EXIT_SUCCESS);
}
-
optional::input
andoptional::multi_input
:// input - equivalent to: parser.add_optional_argument("input", "i") .required() .nargs(1) .action<ap::void_action>(ap::action::check_file_exists()) .help("Input file path"); // multi_input - equivalent to: parser.add_optional_argument("input", "i") .required() .nargs(ap::nargs::at_least(1)) .action<ap::void_action>(ap::action::check_file_exists()) .help("Input files paths");
-
optional::output
andoptional::multi_output
:// output - equivalent to: parser.add_optional_argument("output", "o") .required() .nargs(1) .help("Output file path"); // multi_otput - equivalent to: parser.add_optional_argument("output", "o") .required() .nargs(ap::nargs::at_least(1)) .help("Output files paths");
Note
The argument_parser::default_<positional/optional>_arguments
functions will be modified to use a variadic argument list instead of a std::vector
in a future release.
To parse the command-line arguments use the argument_parser::parse_args
method:
// power.cpp
#include <ap/argument_parser.hpp>
#include <cmath>
#include <iostream>
int main(int argc, char* argv[]) {
// create the parser class instance
ap::argument_parser parser;
parser.program_name("power calculator")
.program_description("calculates the value of an expression: base ^ exponent");
// add arguments
parser.add_positional_argument<double>("base").help("the exponentation base value");
parser.add_optional_argument<int>("exponent", "e")
.nargs(ap::nargs::any())
.help("the exponent value");
parser.default_optional_arguments({ap::default_argument::optional::help});
// parse command-line arguments
try {
parser.parse_args(argc, argv);
}
catch (const ap::argument_parser_error& err) {
std::cerr << "[ERROR] : " << err.what() << std::endl << parser << std::endl;
std::exit(EXIT_FAILURE);
}
// check for the help argument presence
if (parser.value<bool>("help")) {
std::cout << parser << std::endl;
std::exit(EXIT_SUCCESS);
}
// check if any values for the `exponent` argument have been parsed
if (not parser.has_value("exponent")) {
std::cout << "no exponent values given" << std::endl;
std::exit(EXIT_SUCCESS);
}
const double base = parser.value<double>("base");
const std::vector<int> exponent_values = parser.values<int>("exponent");
for (const int exponent : exponent_values) {
std::cout << base << " ^ " << exponent << " = " << std::pow(base, exponent) << std::endl;
}
return 0;
}
// compiled with:
// g++ -o power power.cpp -I <cpp-ap-include-dir>
Argument parsing rules:
-
Positional arguments are parsed first, in the order they were defined in and without a flag.
In the example above the first command-line argument must be the value for
positional_argument
:./power 2 # out: # no exponent values given
./power # out: # [ERROR] : No values parsed for a required argument [base] # power calculator # calculates the value of an expression: base ^ exponent # [base] : the exponentation base value # [exponent,e] : the exponent value # [help,h] : Display help message
Important
For each positional argument there must be exactly one value.
-
Optional arguments are parsed only with a flag:
./power 2 --exponent 1 2 3 # equivalent to: ./power 2 -e 1 2 3 # out: # 2 ^ 1 = 2 # 2 ^ 2 = 4 # 2 ^ 3 = 8
You can use the flag for each command-line value:
./power 2 -e 1 -e 2 -e 3
Not using a flag will result in an error:
./power 2 1 2 3 # out: # [ERROR] : Failed to deduce the argument for the given value `1` # power calculator # calculates the value of an expression: base & exponent # [base] : the exponentation base value # [exponent,e] : the exponent value # [help,h] : Display help message
Important
The parser behaviour depends on the argument definitions. The argument parameters are described int the Argument parameters section.
The library usage examples / demo projects can be found in the cpp-ap-demo repository.
First build the testing executable:
cmake -B build
cd build && make
or alternatively:
mkdir build && cd build
cmake ..
make
This will build the test executable run_tests
in the <project-root>/build/test
directory.
Tip
Building on Windows - use the -G "Unix Makefiles"
option when running CMake to build a GNU Make project instead of a default Visual Studio project.
Run the tests:
-
All tests:
./run_tests
-
A single test suite:
./run_tests -ts="<test-suite-name>"
Note
Test suites in the project have the same names as the files they're in.
Important
To ensure new line encoding compatibility the project uses unix new line encoding.
This can be set using the git config --global core.autocrlf true
command.
More details can be found here
Note
The project uses clang-format-17
.
To install this tool on ubuntu run sudo bash ./scripts/env/install_clang17_toolchain.sh
.
On windows you can download the LLVM package from the official LLVM GitHub release page
To format the code use run the following:
# Unix platforms
./scripts/format/unix.sh
# Windows: powershell
./scripts/format/windows.ps1
To run a forrmat check use the scripts mentioned above with a --check
flag.
The documentation for this project can be generated using Doxygen:
-
Make sure you have
Doxygen
installed on your machine. If not you can download it from here. -
Generate the documentation:
Open your terminal and use the following instructions:
cd <project-root> doxygen Doxyfile
As of now the project supports the GNU G++ and Clang++ compilers with C++20
support on Linux and Windows.
Note
To build the project using clang you will need to install the clang-17
toolchain using the script or website mentioned in the Formatting section.
The CPP-AP
project uses the MIT Licence