Tutorial

Consider the following toy application, tutorial-playground-part-1.cpp:

#include <argo/Argo.hpp>
#include <cassert>
#include <iostream>
#include <string>

using namespace argo;

struct Options
{
   std::vector<int> numbers;
};

void pp(const std::string &name, const std::vector<int> &numbers)
{
   std::cout << name << ": ";
   for (const auto number: numbers) std::cout << number << " ";
   std::cout << "\n";
}

int main(int argc, char **argv)
{
   Options options{};

   Arguments args{};
   {
      handler::Option numbers{"--numbers", options.numbers};
      numbers.nargs("+");
      numbers.help("Numbers to display");
      numbers.required();
      args.add(numbers);
   }
   const auto result = args.parse(argc, argv);
   switch (result.status)
   {
   case ReturnCode::Error:
      std::cerr << "Error: " << result.message << std::endl;
      return 1;
   case ReturnCode::SuccessAndAbort:
      return 0;
   case ReturnCode::SuccessAndContinue:
      break;
   }
   pp("numbers", options.numbers);
   return 0;
}

The file can be found under the examples.

Compilation

We compile this file making sure (at least) C++11 is enabled. For example, on macOS or Linux with Clang or GCC:

c++ -std=c++11 -I<path/to/Argo>/single_include main.cpp -o main

Now let’s break down the code.

Creating a parser

We start by creating an Arguments object:

Arguments args{};

This object will be configured step-by-step so that it has enough information to parse the command line options. This configuration is done by adding handlers.

Adding an option

We create a handler::Option which we would like to handle the --number option:

handler::Option numbers{"--numbers", options.numbers}

This will store the passed numbers in the options.numbers variable as soon as the option is triggered during parsing. Note that Argo will take care of any required conversions: built-in types are supported and custom types can easily be added, see Type conversions. By default, an option expects a single value. This behavior is referred to as the cardinality property of an option and is controlled by one of the handler::Option::nargs() methods. Since we want to allow one or more values, we set the cardinality accordingly:

numbers.nargs("+");

Warning

The nargs method has two overloads: one accepting an std::string and one accepting an unsigned int. In this case, we invoke the std::string overload, so make sure not to invoke it with a char as this will be implicitly converted to an unsigned int and call the wrong method. In this case, that would lead to --number expecting 43 values (the ASCII value).

Next, we configure the option by adding a help description that will show up when invoking our application with --help:

numbers.help("Numbers to display");
numbers.required();

We’ve also marked the option as required since by default all handlers are assumed to be optional.

Note that handlers are also configured step-by-step, but these steps can be chained together. For example, the entire post-creation configuration can be done in a single line:

numbers.nargs("+").help("Numbers to display").required();

which would be equivalent.

With our handler fully configured, we can now add it to the parser:

args.add(number);

Note that args.add actually returns a bool, indicating whether the option is properly configured. This is because conflicts are possible (e.g., adding different handlers that trigger on the same argument). Wrongly configured handlers will always cause the parsing to fail.

Parsing arguments

We’re now ready to invoke the actual parsing of the command line arguments:

const auto result = args.parse(argc, argv);

The result of the parsing process is stored in an object of the Result class which contains a status and message which - in case something went wrong - contains an error description. Since actions such as converting and storing variables take place during the parsing process, we only need to inspect the result.status:

switch (result.status)
{
case ReturnCode::Error:
   std::cerr << "Error: " << result.message << std::endl;
   return 1;
case ReturnCode::SuccessAndAbort:
   return 0;
case ReturnCode::SuccessAndContinue:
   break;
}
pp("numbers", options.numbers);

As can be seen, there are three possible states for ReturnCode:

  • Error: the parsing failed. In this case, the application quits with an error message

  • SuccessAndAbort: the parsing was successful, but a handler requested that the application quits. This is usually used by handlers implementing print outs such as --help and --version

  • SuccessAndContinue: the parsing was successful, and the application may continue

In the latter case, the numbers are pretty printed. That’s it, we now have a fully functioning application which - although it is just a toy app - boasts some nice features!

Let’s play around with it.

Playground #1

A good way to explore our toy application and to gain a deeper understanding of the underlying behavior is by seeing how it reacts to different input.

You can follow along by checking out the tutorial source files in the examples folder.

Let’s start by providing some valid input:

$ ./tutorial-playground-part-1 --numbers 1 -3 2 5 9 1e2
numbers: 1 -3 2 5 9 100

Both positive and negative numbers are allowed. Scientific notation and metric prefixes are also supported: 1e2 is converted to 100.

Now let’s repeat this with a different calling flavour:

$ ./tutorial-playground-part-1 --numbers 1 --numbers -3 2 --numbers=5,9 --numbers=1e2
numbers: 1 -3 2 5 9 100

Same result!

Often, situations where things go wrong are much more interesting. Let’s check how the parser reacts when we throw in some floating point numbers:

$ ./tutorial-playground-part-1 --numbers 1 2 3.14
Error: Option '--numbers' expects a signed integer (not '3.14')

As expected, an error is raised: the --numbers option stores its values in an std::vector<int> which obviously does not accept floating point numbers (without loss of precision).

What happens when we don’t feed it any arguments?

$ ./tutorial-playground-part-1 --numbers
Error: Missing value(s) for option '--numbers': expected one or more arguments

That’s very clear. Notice how this differs from invoking the application without any options:

$ ./tutorial-playground-part-1
Error: Option '--numbers' is required

Or when it is invoked with an unknown option:

$ ./tutorial-playground-part-1 --foo
Error: Unknown option '--foo'

As you can see, the application boasts some pretty nice features with minimal effort on our side.

Let us continue extending the application.

Adding a toggle

Argo has different built-in argument handlers:

  • Options: named arguments which can hold values

  • Toggles: named arguments which can be called with an optional value

  • Flags: named arguments which cannot hold values

  • Positional arguments: unnamed arguments which are values

We’ve already added the --numbers option to our application. Let’s add a handler::Toggle --reverse which will reverse the order of the printed numbers. For those fond of one-liners:

args.add(handler::Toggle{"--reverse", options.reverse}.help("Reverses the order of the numbers"));

Note that this implies addeding a boolean named reverse to the Options struct. This - as well as the code for reversing the numbers - will be shown shortly. But first, we introduce Actions as a powerful tool in Argo.

Adding actions

Actions are a fundamental feature in Argo and central to Argo’s architecture: when parsing command line arguments, the Arguments parser checks which of its argument handlers recognizes the currently processed argument. Every handler that recognizes the current argument is run and can consume any potentially following values: the parsing is effectively delegated to the handler. After consuming any or all values, the handler will trigger its attached actions. These actions are run during the parsing process. This is in contrast to other frameworks: arguments are not collected into a variant type object, instead, the user can immediately decide on the action that is to take place.

Let’s make this more tangible by adding an action that will populate another variable with the square of processed numbers. We extend the Options struct yet again, to have the following:

struct Options
{
   std::vector<int> numbers;
   bool reverse = false;
   std::vector<int> squared;
};

and add an action to the numbers option:

auto square = action::run<int>([&options](const int number) {
   options.squared.push_back(number * number);
   return true;
});
numbers.action(square);

The action is trivial: options.squared is populated with the squared numbers. However, as trivial as it may be, the code shows how to provide a callback function to the action::run action. The latter accepts different types of callbacks. For instance, let’s have the parser refuse odd numbers:

auto validate = action::run<int>([&options](core::Context &context,
                                           const int number) {
   if (number % 2 == 0)
       return true;
   context.error() << "Odd numbers such as " << number << " are not allowed";
   return false;
});
numbers.action(validate);

Note that the callback accepts two arguments in this case: a core::Context object, and the processed number. The former is a class which holds the parser state and influences the parsing process. By calling error on it - with an optional message - we signal the process to quit resulting in Result::state to be set to ReturnCode::Error.

It’s time again to compile our application and play around with it. For reference, this is the result of our recent changes:

#include <argo/Argo.hpp>
#include <cassert>
#include <iostream>
#include <string>

using namespace argo;

struct Options
{
    std::vector<int> numbers;
    bool reverse = false;
    std::vector<int> squared;
};

void pp(const std::string &name, const std::vector<int> &numbers)
{
    std::cout << name << ": ";
    for (const auto number : numbers)
        std::cout << number << " ";
    std::cout << "\n";
}

int main(int argc, char **argv)
{
    Options options{};

    Arguments args{};
    {
        handler::Option numbers{ "--numbers", options.numbers };
        numbers.nargs("+");
        numbers.help("Numbers to display");
        numbers.required();
        auto square = action::run<int>([&options](const int number) {
            options.squared.push_back(number * number);
            return true;
        });
        numbers.action(square);
        auto validate = action::run<int>([&options](core::Context &context,
                                                    const int number) {
            if (number % 2 == 0)
                return true;
            context.error() << "Odd numbers such as " << number << " are not allowed";
            return false;
        });
        numbers.action(validate);
        args.add(numbers);
    }
    {
        args.add(handler::Toggle{ "--reverse", options.reverse }.help(
            "Reverses the order of the numbers"));
    }
    const auto result = args.parse(argc, argv);
    switch (result.status)
    {
    case ReturnCode::Error:
        std::cerr << "Error: " << result.message << std::endl;
        return 1;
    case ReturnCode::SuccessAndAbort:
        return 0;
    case ReturnCode::SuccessAndContinue:
        break;
    }
    if (options.reverse)
    {
        std::reverse(std::begin(options.numbers), std::end(options.numbers));
        std::reverse(std::begin(options.squared), std::end(options.squared));
    }
    pp("numbers", options.numbers);
    pp("squared", options.squared);
    return 0;
}

The code is available as tutorial-playground-part2.cpp and can be found under the examples.

Playground #2

We start again by providing some valid input:

$ ./tutorial-playground-part-2 --numbers 2 4 6 8 10e0
numbers: 2 4 6 8 10
squared: 4 16 36 64 100

The result is as expected: squared contains the squared values of numbers.

Let’s test the validation by feeding it an odd number:

$ ./tutorial-playground-part-2 --numbers 2 4 5 6 7
Error: Odd numbers such as 5 are not allowed

Unsurprisingly, an error is raised when the parser hits the first odd number.

This concludes the tutorial as a quick introduction to parsing with Argo! Check out the rest of the documentation for a more in-depth information regarding API usage and behavior.