Introduction

arparse-cpp is a single header only library for parsing command line arguments in C++. It is also ideal for creating various CLI tools thanks to its clean and comprehensible API.

  • Available on Conan
  • Written in C++20
  • Unit tested with Catch2

Contributing

argparse-cpp is free and open source. You can find the source code here. Issues and feature requests can be posted on the GitHub issue tracker. If you'd like to contribute, consider opening a pull request.

License

The whole argparse-cpp project is released under MIT License.

Installation

There are different ways of installing the library. Choose whatever you think is the best for you.

Using conan package manager

Starting from release v0.1.0 it is possible to use conan in order to install the library from the artifactory remote

For these steps to work you will need both conan and CMake. Check their respective installation guides for further information on how to get those tools.

  1. Create a simple conanfile.txt in the root of your project containing the requirements

    [requires]
      argparse-cpp/0.1.0@dead/stable
    
    [generators]
      cmake
    
  2. Move into the build folder

    $ cd <build-folder>
    
  3. Add the artifactory remote to your conan client

    $ conan remote add argparse-cpp https://argparsecpp.jfrog.io/artifactory/api/conan/argparse-cpp-conan-local
    
  4. Run conan install

    $ conan install .. -r argparse-cpp
    
  5. Configure cmake to use the installed library from conan

    include(${CMAKE_BINARY_DIR}/conanbuildinfo.cmake)
    conan_basic_setup(NO_OUTPUT_DIRS TARGETS)
    ...
    target_link_libraries(your_exe PUBLIC CONAN_PKG::argparse-cpp)
    

Download source code from latest release

Click on the releases header on the right side of the GitHub repo or navigate here

Scroll down to the assets section and download the source code (zip or tar.gz).

Downloading the header file

Get the header file through wget.

$ wget https://raw.githubusercontent.com/dead-tech/argparse-cpp/main/include/argparse/argparse.hpp

Cloning the repo

  1. Clone the repo

    $ git clone https://github.com/dead-tech/argparse-cpp.git
    
  2. Change current directory to the cloned folder

    $ cd argparse-cpp
    
  3. Move the header file

    $ mv include/argparse.hpp <your-project-include-path>
    

Running the tests

This project uses Catch2 testing framework. You can find the whole test suite here.

If you would like to, you can run the test suite yourself by following the instructions below.

  1. Clone the repository.

    $ git clone https://github.com/dead-tech/argparse-cpp
    
  2. Change directory to the repository root.

    $ cd argparse-cpp
    
  3. Create a build folder and cd into it.

    $ mkdir build && cd build
    
  4. Install the conan dependecies.

    $ conan install ..
    

    If this does not work for you checkout the installation section for further instructions.

  5. Configure cmake and compile the tests

    $ cmake .. && make
    

    You can also specify a number of jobs as an argument to the make command to speed up the compilation process like so make -j5.

  6. Run the tests

    $ ./argparse-cpp_tests
    

Currently all the tests are expected to: Build Status

Basic Example

Simple example program that emulates an hypothetically compiler CLI. You may use this to get a general idea of what it would like to crete an application using this library.

#include <argparse/argparse.hpp>
#include <fstream>
#include <iostream>
#include <sstream>

int main(int argc, const char **argv)
{
    argparse::ArgumentParser parser(argc, argv);

    parser.add_argument("files")
            .set_type(argparse::ArgTypes::STRING)
            .set_default("test.txt")
            .set_help("Paths to the files to compile")
            .set_flags(argparse::ArgFlags::REQUIRED)
            .set_metavar("FILE_PATH")
            .set_nargs('+');

    parser.add_argument("--release", "-R")
            .set_type(argparse::ArgTypes::BOOL)
            .set_help("Build in release version");

    const auto args = parser.parse_args();

    const auto files = args.at("files").as<std::vector<std::string>>();
    const auto is_release = args.at("--release").as<bool>();

    const auto result = build_files(files, is_release);
}

After compiling the program can ben run at the command line as follows:

$ ./program_name args...

Note: You will get automatically generated help and version optional arguments that will respectively produce the following output

Version optional argument:

$ ./main --version
0.0.1

Help optional argument:

$ ./main --help
usage: ./main [-H] [--release --RELEASE] files FILE_PATH

required arguments:
  files FILE_PATH Paths to the files to compile

optional arguments:
  -H, --help            show this help message and exit
  --release, -R --RELEASE Build in release version

When run with the appropriate arguments it should print the following output:

$ ./main files.txt bar.json --release
Built files:
files.txt bar.json

ArgumentParser objects

Function signature

class ArgumentParser
{
public:
    ArgumentParser(const int argc, const char **argv, std::string version = "0.0.1");
}

The constructor creates a new ArgumentParser object. All the parameters except for the version are required. Each of the parameters is described more in detail below.

  • argc - The number of arguments passed to the program
  • argv - The array of arguments passed to the program
  • version - The version of the program (optional - defaults to "0.0.1")

Detail

argc
This parameter is used to determine the number of arguments passed to the program. Not only in C++ there is no practical way of eliminating this parameter from the function signature, but this makes it also convenient to test the library.

argv
This parameter represent the array of arguments passed to the program. As for argc not only in C++ there is no practical way of eliminating this parameter from the function signature, but this makes it also convenient to test the library or perhaps providing your own arguments for whatever reason.

version
This parameter is used to provide a custom version of the program. If it is left blank it will default to "0.0.1". Note that the library will automatically generate an optional argument, as a builtin, for displaying the version of the program on command. The builtin can be invoked with the --version or -V argument as shown below.

$ ./program_name --version
0.0.1

The add_argument() method

Function signature

template<utils::StringLike... Names>
mapped_type &add_argument(Names &&...names);
  • utils::StringLike - A C++20 concept that allows you to pass any type T which is convertible to a std::string

  • Names &&...names - A variadic pack of utils::StringLikes containing all the names for the argument, including the primary name and the optional aliases

  • mapped_type & - The return type of the method which is a reference to the argument object. By returning a non-const reference to the newly created argument object, you can modify the argument object by using the object chaining method, which our API is based on. mapped_type is just an aliased type:

    using map_type       = std::unordered_map<std::string, Arg>;
    using mapped_type    = map_type::mapped_type;
    

Constraints

Important: As shown by the static_assert below you must provide at least one argument as a name for the argument itself. If you're trying to register an optional argument one of the names must also start with a --. The latter name will be used as the primary name of the argument, for example to index and retrieve the argument in the internal storage. Whereas for positional arguments you should pass only one argument, the others will be discard and only the first one will be used.

static_assert(
    sizeof...(Names) > 0,
    "[argparse] error: add_argument() needs at least one argument as a name (starting with '--' for "
    "positional arguments)");

Runtime dispatch based on argument kind

As we have briefly mentioned above the same method can be used to create both optional and positional arguments. The system will automatically make its way between the two and dispatch to the appropriate method.

if (arg_kind == ArgKind::Positional) {
    return this->add_positional_argument(data);
} else if (arg_kind == ArgKind::Optional) {
    return this->add_optional_argument(data, primary_name.value());
}

The parse_args() method

Function signature

[[nodiscard]] map_type parse_args();

Explanation of how it works

  • map_type is equivalent to std::unordered_map<std::string, Arg>
  • The function will immediately create the usage and help messages to be display on command
    this->create_usage_message();
    this->create_help_message();
    
  • The function will also before even parsing the command line arguments check if any builtins are present and if so, execute them
    if (const auto builtin = this->get_builtin_if(); builtin.has_value()) {
        const auto fn = builtin.value();
        fn();
        exit(0);
    }
    
  • The function will then split the program arguments into positional and optional arguments
    const auto [positional_args, optional_args] = this->split_program_args();
    
  • The function will then parse the positional arguments
    this->parse_positional_args(positional_args);
    
  • The function will then throw if a not registered optional argument is found in the program args
    this->throw_if_unrecognized(optional_args);
    this->parse_optional_args(optional_args);
    

Whatever is returned from the function can be then accessed through the .at() method and then cast to the appropriate type as it is explained here.

Exceptions

Be also aware that this function may throw an exception in one of these cases:

  • If not enough positional arguments were provided
  • If an optional argument that is not registered was provided

NOTE: The return type of the method is marked as [[nodiscard]] which means that the result of the call to this method cannot be ignored and the value has necessarily to be stored in a variable.

set_type()

Rationale

This method allows you to set the type of type of the argument that has to be parsed from the command line arguments.

It is used to parse differently based on the type of the argument. For example, boolean arguments do not expect after their name any kind of value, whereas string or int arguments do.

List of supported argument types

It is worth be aware of that if you do not call set_type(), on a Arg instance, the type of the argument will default to argparse::ArgTypes::STRING.

enum class ArgTypes
{
    STRING = 0,
    INT,
    BOOL
};

Example usage

parser.add_argument("--string", "-S").set_type(argparse::ArgTypes::STRING);
parser.add_argument("--int", "-I").set_type(argparse::ArgTypes::INT);
parser.add_argument("--bool", "-B").set_type(argparse::ArgTypes::BOOL);

Source Code

Arg &set_type(const ArgTypes &type)
{
    this->type = type;
    return *this;
}

set_flags()

Rationale

This method allows you to set some flags for the argument that has to be parsed from the command line arguments.

We currently use a bitmask to encode all the various flags to ease the usage.

List of flags you may specify

It is worth be aware of that if you do not call set_flags(), on a Arg instance, the type of the argument will default to argparse::ArgFlags::DEFAULT which is only STORE_TRUE at the moment as shown below.

enum class ArgFlags : int64_t
{
    NONE        = 0,
    REQUIRED    = (1LL << 1),
    STORE_TRUE  = (1LL << 2),
    STORE_FALSE = (1LL << 3),

    DEFAULT = STORE_TRUE,
};

Example usage

parser.add_argument("--release", "-R")
    .set_flags(argparse::ArgFlags::REQUIRED | argparse::ArgFlags::STORE_TRUE);

Source Code

Arg &set_flags(const ArgFlags &flags)
{
    this->flags = flags;
    return *this;
}

set_help()

Rationale

This method allows you to set a custom help message that has to be printed next to the argument name when invoking the --help builtin.

Example usage

parser.add_argument("--release", "-R").set_help("Set build process in release mode");

Source Code

Arg &set_help(const std::string &help_message)
{
    this->help_message = help_message;
    return *this;
}

set_default()

Rationale

This method allows you to set a default value for an argument. This value will be used if the user does not provide a value for the argument.

NOTE: Be aware that if a required argument is not provived the default value will be used and no error will be thrown.

IMPORTANT: Executing a builting command such as --help or --version will quit the program with zero exit code.

Example usage

parser.add_argument("--jobs", "-J").set_default(1);

Source Code

template<SupportedArgumentType T>
Arg &set_default(T &&value)
{
    if constexpr (std::is_convertible_v<T, std::string>) {
        this->values.front() = std::forward<T>(value);
    } else if constexpr (std::is_same_v<T, bool>) {
        this->values.front() = utils::bool_to_str(std::forward<T>(value));
    } else {
        this->values.front() = std::to_string(std::forward<T>(value));
    }

    return *this;
}

This templated function is restricted in its types by the concept SupportedArgumentTypes. The latter allows you to pass as an argument to the call just the types that are supported by the library (see set_type() for more info).
Moreover the function uses C++17 if constexpr to generate at compile time the right branch of the if statement, reducing the overhead during runtime.

set_metavar()

Rationale

This method allows you to specify a custom metavar. A metavar is the string it is printed next to the argument name when invoking the --help builtin that helps you understand where you have to specify the value of the argument.

Example: program_name --out output_file

In the example above output_file is the metavar.

Example usage

parser.add_argument("--out", "-O").set_metavar("output_file");

Source Code

Arg &set_metavar(const std::string &metavar)
{
    this->metavar = metavar;
    return *this;
}

set_nargs()

Rationale

This method allows you to set how many values are expected for an argument.

Possible values

You may specify as an argument to this function one of the following values:

  • x -> any integer number
  • * -> which means at zero or more values
  • + -> which means at least one or more values

Example usage

parser.add_argument("files").set_nargs('*');

Source Code

Arg &set_nargs(const auto nargs)
{
    static constexpr auto is_number = std::is_same_v<decltype(nargs), const int>;
    static constexpr auto is_symbol = std::is_same_v<decltype(nargs), const char>;

    if constexpr (is_number) {
        this->nargs = nargs;
    } else if constexpr (is_symbol) {
        this->nargs = nargs - '0';
    } else {
        throw exceptions::ArgparseException(
            std::source_location::current(),
            "set_nargs() error: unsupported type: %\nSupported types are: int, std::string\n",
            typeid(nargs).name());
    }

    return *this;
}

Moreover the function uses C++17 if constexpr to generate at compile time the right branch of the if statement, reducing the overhead during runtime.

Note: Be aware that the function will throw an exception if the argument is not a number or one of the supported symbols symbol.

count()

Rationale

This method allows you to count the occurence of an argument in the command line arguments.

Example usage

parser.add_argument("--verbose").count();

Source Code

Arg &count()
{
    this->count_occurence = true;
    this->type            = ArgTypes::BOOL;
    return *this;
}

as()

Rationale

This method allows you to cast the Arg value (a.k.a. std::string) you get when invoking the at() method on the return value of the parse_args() method (more info) to the type of your needs.

List of supported types

  • int - std::is_integral_v<T> - actually every integral type
  • std::vector<int>
  • std::string
  • std::vector<std::string>
  • bool

Example usage

const auto args = parser.parse_args();
const auto value = args.at('--value').as<int>();

Source Code

template<typename ReturnType>
[[nodiscard]] decltype(auto) as() const
{
    static constexpr auto is_int        = std::is_integral_v<ReturnType> && !std::is_same_v<ReturnType, bool>;
    static constexpr auto is_vec_int    = std::is_same_v<ReturnType, std::vector<int>>;
    static constexpr auto is_string     = std::is_same_v<ReturnType, std::string>;
    static constexpr auto is_vec_string = std::is_same_v<ReturnType, std::vector<std::string>>;
    static constexpr auto is_bool       = std::is_same_v<ReturnType, bool>;

    if constexpr (is_int) {
        return utils::impl::str_to_int_helper(this->values.front());
    } else if constexpr (is_vec_int) {
        return utils::str_to_int(this->values);
    } else if constexpr (is_string) {
        return values.front();
    } else if constexpr (is_vec_string) {
        return std::vector(values.data(), values.data() + this->actual_size);
    } else if constexpr (is_bool) {
        return utils::str_to_bool(this->values.front());
    }

    throw exceptions::ArgparseException(
        std::source_location::current(),
        "as<%>() error: unsupported return type\nSupported types are: int, std::vector<int>, std::string, "
        "std::vector<std::string>, bool\n",
        typeid(ReturnType).name());
}

The function uses C++17 if constexpr to generate at compile time the right branch of the if statement, reducing the overhead during runtime.

NOTE: If the type passed as a template argument to this method is not one of the supported types, the function will throw an exception.