Skip to content

gershnik/argum

Repository files navigation

Argum

Fully-featured, powerful and simple to use C++ command line argument parser.

Language Standard License Tests

Features and goals

  • Supports the commonly used Unix and Windows command line conventions (Posix, GNU extensions to it, Python's Argparse, Microsoft syntax for its better designed utilities etc.). It should be possible to process complicated command lines like the ones of git or clang using this library. See Syntax Description on wiki for details on accepted syntax and configurability.
  • Sequential processing. The arguments are handled in the order they are specified so you can handle things differently based on order if you want to.
  • Adaptive processing. You can also modify parser definitions while processing - this way you can adapt the argument handling on the fly rather than have to pre-configure everything at once.
  • Simple to use.
    • This library does not attempt to shove all the arguments into a map like data structure and neither does it attempt to define increasingly convoluted ways to "bind" arguments to variables in your code and convert them to internal types. Instead you provide callback lambdas (or any other function objects, of course) that put the data where you need it and convert how you want it. Beyond toy examples this usually results in simpler code and less mental effort to write.
    • Simple and extensible way to do rule based syntax validation. There are no rigid "groups" with predefined mutual exclusion only.
  • Configurability beyond just "globally replace - with /". You can specify what prefixes to use on short options, long options, which prefixes are equivalent, disable short or long options behaviors altogether, what separator to use for arguments instead of = and more.
    • The default syntax is the common Unix/GNU one
    • Additional, pre-built configurations are available for GNU "long options only" and a couple of common Windows syntaxes.
    • You can define your own configurations from scratch or modify one of the above (e.g. add + as short option prefix in addition to -)
  • Can handle response files.
  • Can operate using exceptions or expected values (similar to boost::outcome or proposed std::expected)
    • Can be used with exceptions and RTTI completely disabled
  • No dependencies beyond C++ standard library. Can be used via a single file header.
    • Requires C++20 or above.
    • Does not use iostreams in any way.
  • Equivalent support for char and wchar_t.
  • Allows for localization with user-supplied message translations.
  • Modularity. Everything is not shoved into a one giant "parser" class. You can combine different parts of the library in different ways if you really need to build something unusual. Similarly, things like printing help messages expose functions that print various parts of the whole to allow you to build your own help while reusing the tedious bits.

Examples

Two equivalent examples - one using exceptions and one error codes - of file processing utility of some kind are given below. They demonstrates many of the most important features of the library. More examples can be found on the Wiki.

Using exceptions

Click to expand
#include "argum.h"
#include <iostream>

using namespace Argum;
using namespace std;

enum Encoding { defaultEncoding, Base64, Hex };

int main(int argc, char * argv[]) {

    vector<string> sources;
    string destination;
    optional<Encoding> encoding = defaultEncoding;
    optional<string> compression;
    int compressionLevel = 9;

    const char * progname = (argc ? argv[0] : "my_utility");

    Parser parser;
    parser.add(
        Positional("source").
        help("source file"). 
        occurs(zeroOrMoreTimes).
        handler([&](const string_view & value) { 
            sources.emplace_back(value);
    }));
    parser.add(
        Positional("destination").
        help("destination file"). 
        occurs(once). 
        handler([&](string_view value) { 
            destination = value;
    }));
    parser.add(
        Option("--help", "-h").
        help("show this help message and exit"). 
        handler([&]() {
            cout << parser.formatHelp(progname);
            exit(EXIT_SUCCESS);
    }));
    ChoiceParser encodingChoices;
    encodingChoices.addChoice("default");
    encodingChoices.addChoice("base64");
    encodingChoices.addChoice("hex");
    parser.add(
        Option("--format", "-f", "--encoding", "-e").
        help("output file format"). 
        argName(encodingChoices.description()).
        handler([&](string_view value) {
            encoding = Encoding(encodingChoices.parse(value));
    }));
    parser.add(
        Option("--compress", "-c").
        argName("ALGORITHM").
        requireAttachedArgument(true). //require -cALGORITHM or --compress=ALGORITHM syntax
        help("compress output with a given algorithm (default gzip)"). 
        handler([&](const optional<string_view> & value) {
            encoding = nullopt;
            compression = value.value_or("gzip");
    }));
    parser.add(
        Option("--level", "-l").
        argName("LEVEL").
        help("compression level, requires --compress"). 
        handler([&](const string_view & value) {
            compressionLevel = parseIntegral<int>(value);
    }));
    parser.addValidator(
        oneOrNoneOf(
            optionPresent("--format"),
            anyOf(optionPresent("--compress"), optionPresent("--level"))
        ), 
        "options --format and --compress/--level are mutually exclusive"
    );
    parser.addValidator(
        !optionPresent("--level") || optionPresent("--compress"), 
        "if --level is specified then --compress must be specified also"
    );

    try {
        parser.parse(argc, argv);
    } catch (ParsingException & ex) {
        cerr << ex.message() << '\n';
        cerr << parser.formatUsage(progname) << '\n';
        return EXIT_FAILURE;
    }

    if (encoding)
        cout << "need to encode with encoding: " << *encoding <<'\n';
    else 
        cout << "need to compress with algorithm: " << *compression 
             << " at level: " << compressionLevel <<'\n';
    cout << "sources: {" << join(sources.begin(), sources.end(), ", ") << "}\n";
    cout << "into: " << destination <<'\n';
}

Using expected values

Click to expand
#include "argum.h"
#include <iostream>

using namespace Argum;
using namespace std;

enum Encoding { defaultEncoding, Base64, Hex };

int main(int argc, char * argv[]) {

    vector<string> sources;
    string destination;
    optional<Encoding> encoding = defaultEncoding;
    optional<string> compression;
    int compressionLevel = 9;

    const char * progname = (argc ? argv[0] : "my_utility");

    Parser parser;
    parser.add(
        Positional("source").
        help("source file"). 
        occurs(zeroOrMoreTimes).
        handler([&](const string_view & value) { 
            sources.emplace_back(value);
    }));
    parser.add(
        Positional("destination").
        help("destination file"). 
        occurs(once). 
        handler([&](string_view value) { 
            destination = value;
    }));
    parser.add(
        Option("--help", "-h").
        help("show this help message and exit"). 
        handler([&]() {
            cout << parser.formatHelp(progname);
            exit(EXIT_SUCCESS);
    }));
    ChoiceParser encodingChoices;
    encodingChoices.addChoice("default");
    encodingChoices.addChoice("base64");
    encodingChoices.addChoice("hex");
    parser.add(
        Option("--format", "-f", "--encoding", "-e").
        help("output file format"). 
        argName(encodingChoices.description()).
        handler([&](string_view value) -> Expected<void> {
            auto result = encodingChoices.parse(value);
            if (auto error = result.error()) return error;
            encoding = Encoding(*result);
            return {};
    }));
    parser.add(
        Option("--compress", "-c").
        argName("ALGORITHM").
        requireAttachedArgument(true). //require -cALGORITHM or --compress=ALGORITHM syntax
        help("compress output with a given algorithm (default gzip)"). 
        handler([&](const optional<string_view> & value) {
            encoding = nullopt;
            compression = value.value_or("gzip");
    }));
    parser.add(
        Option("--level", "-l").
        argName("LEVEL").
        help("compression level, requires --compress"). 
        handler([&](const string_view & value) -> Expected<void> {
            auto result = parseIntegral<int>(value);
            if (auto error = result.error()) return error;
            compressionLevel = *result;
            return {};
    }));
    parser.addValidator(
        oneOrNoneOf(
            optionPresent("--format"),
            anyOf(optionPresent("--compress"), optionPresent("--level"))
        ), 
        "options --format and --compress/--level are mutually exclusive"
    );
    parser.addValidator(
        !optionPresent("--level") || optionPresent("--compress"), 
        "if --level is specified then --compress must be specified also"
    );

    auto result = parser.parse(argc, argv);
    if (auto error = result.error()) {
        cerr << error->message() << '\n';
        cerr << parser.formatUsage(progname) << '\n';
        return EXIT_FAILURE;
    }

    if (encoding)
        cout << "need to encode with encoding: " << *encoding <<'\n';
    else 
        cout << "need to compress with algorithm: " << *compression 
             << " at level: " << compressionLevel <<'\n';
    cout << "sources: {" << join(sources.begin(), sources.end(), ", ") << "}\n";
    cout << "into: " << destination <<'\n';
}

Integration

You can integrate Argum into your code in the following ways

Single header

Download argum.h from the Releases page, drop it into your project and #include it. This is the simplest way.

Module

If you are lucky (or unlucky?) to have a compiler and build system that support modules you can try to use experimental module file. Download argum-module.ixx from Releases page and integrate it into you project. Note, that of the compilers I have access to, only MSVC and GCC currently supports modules to any usable extent and, even there, many things appear to be broken. If you encounter internal compiler errors please complain to your compiler vendor. Use at your own risk.

CMake via FetchContent

With modern CMake you can easily integrate Argum as follows:

include(FetchContent)
FetchContent_Declare(argum
        GIT_REPOSITORY git@github.com:gershnik/argum.git
        GIT_TAG <desired tag like v2.5>
        GIT_SHALLOW TRUE
)
FetchContent_MakeAvailable(argum)
...
target_link_libraries(mytarget
PRIVATE
  argum::argum
)

ℹ️ What is FetchContent?

CMake from download

Alternatively you can clone this repository somewhere and do this:

add_subdirectory(PATH_WHERE_YOU_DOWNALODED_IT_TO, argum)
...
target_link_libraries(mytarget
PRIVATE
  argum::argum
)

Building and installing on your system

You can also build and install this library on your system using CMake.

  1. Download or clone this repository into SOME_PATH
  2. On command line:
cd SOME_PATH
cmake -S . -B build 
cmake --build build

#Optional
#cmake --build build --target run-test

#install to /usr/local
sudo cmake --install build
#or for a different prefix
#cmake --install build --prefix /usr

Once the library has been installed it can be used int the following ways:

Basic use

Set the include directory to <prefix>/include where <prefix> is the install prefix from above.

CMake package

find_package(argum)

target_link_libraries(mytarget
PRIVATE
  argum::argum
)

Via pkg-config

Add the output of pkg-config --cflags argum to your compiler flags.

Note that the default installation prefix /usr/local might not be in the list of places your pkg-config looks into. If so you might need to do:

export PKG_CONFIG_PATH=/usr/local/share/pkgconfig

before running pkg-config

Configuration

Whichever method you use in order to use Argum your compiler needs to be set in C++20 mode. Argum should compile cleanly even on a highest warnings level.

If you don't use CMake, on MSVC you need to have _CRT_SECURE_NO_WARNINGS defined to avoid its bogus "deprecation" warnings.

Error reporting mode

Argum can operate in 3 modes selected at compile time:

  • Using exceptions (this is the default). In this mode parsing errors produce exceptions.
  • Using expected values, with exceptions enabled. In this mode parsing errors are reported via Expected<T> return values. This class similar to proposed std::expected or boost::outcome. Trying to access a result.value() when it contains an error throws an equivalent exceptions. This mode is enabled via ARGUM_USE_EXPECTED macro.
  • With exceptions disabled. In this mode expected values used as above but trying to access a result.value() when it contains an error calls std::terminate. This mode can be manually enabled via ARGUM_NO_THROW macro. On Clang, GCC and MSVC Argum automatically detects if exceptions are disabled during compilation and switches to this mode.

Note that these modes only affect handling of parsing errors. Logic errors such as passing incorrect parameters to configure parser always assert in debug and std::terminate in non-debug builds.

Customizing termination function

By default, when being passed invalid arguments or when attempting to "throw" with exceptions disabled Argum calls assert in debug mode and std::terminate in release. You can customize this behavior. To do so define ARGUM_CUSTOM_TERMINATE for the compilation. If you do this, you will need to provide your own implementation of [[noreturn]] inline void terminateApplication(const char * message).

For reference this is the default implementation

Code
[[noreturn]] inline void Argum::terminateApplication(const char * message) { 
    fprintf(stderr, "%s\n", message); 
    fflush(stderr); 
    #ifndef NDEBUG
        assert(false);
    #else
        std::terminate(); 
    #endif
}

FAQ

Why another command line library?

There are quite a few command line parsing libraries for C++ out there including Boost.Program_options, Lyra, TCLAP and many others. Unfortunately, beyond toy applications I found none of them simultaneously easy to integrate, easy to use and easy to tweak. Specifically:

  • Such a library needs to be header-only (and ideally a single file) one that can be quickly used in any little command line project without setting up and building a library dependency.
  • Simple examples work well with all libraries, but trying to implement something in real life soon requires you to fight the library defaults, add ad-hoc validation code, figure out workarounds etc. etc.
  • Handle sequential parsing. It is not uncommon to have meaningful order of options and positional arguments and parsers that don't allow making decisions based on order are just standing in the way.
  • Configurability. Most libraries do let you change prefixes but usually in a clumsy way that doesn't really work well on Windows or other situations where you need custom syntax.
  • Modularity. Most often this is an issue if you want a different way to display help while not redoing everything from scratch. Most libraries hide everything inside and only expose high level "print help" method with some custom header and footer. Also nobody exposes internal lexer/tokenizer which would allow different parsing semantics.

Some libraries do better on some of these but none I could find do everything well. Hence this project.

Why options cannot have more than 1 argument? ArgParse allows that

Having multiple options arguments is a very bad idea. Consider this. Normally with Posix/GNU approach when an option argument itself looks like an option you can always use some workaround syntax to disambiguate. For example if you have option --foo and -f and their argument -x you can say: --foo=-x and -f-x to avoid treating -x as an unknown option. With multiple arguments this becomes impossible. People using ArgParse occasionally hit this issue and are surprised. Argum follows standard Unix approach of having at most one argument per option.

If you really, really need more than one argument to an option consider requiring to pass them as comma or semicolon separated list. This is also a de-facto standard Unix approach. See for example getsubopt.

Why isn't it using [C++20 feature X]?

This is simply due to the fact that, currently, not all compilers and standard libraries have support for all C++20 features. Notably Ranges support is lacking on many. Once C++20 support improves this library will attempt to adjust when appropriate.