Parsing command line arguments in C++ with Clara

In a previous post, I wrote about the C++ unit-testing framework Catch2. Catch uses another library, called Clara, for parsing command line arguments. Clara is an open-source, single-header, simple, composable and easy to use parser written by the author of Catch2. In this post, I will show how you can use Clara in C++ to parse command line arguments.

Clara is available on Github at https://github.com/catchorg/Clara. Although there is not much documentation at the moment, the library is easy to use. The following sample shows an example of using Clara. We’ll drill down on it in a moment.

#include <iostream>
#include <string>

#include "clara.hpp"

enum class verbosity_level
{
   low, normal, debug
};

int main(int argc, char const * const * argv)
{
   using namespace clara;

   auto source    = std::string{};
   auto dest      = std::string{};
   auto threshold = 0.0;
   auto ignore    = false;
   auto verbosity = verbosity_level::low;
   auto depth     = 0;
   auto showhelp  = false;

   auto parser =
      Opt(threshold, "threshold")["-t"]("Threshold for analysis inclusion") |
      Opt(ignore)["-i"]["--ignore"]("Ignore conflicts") |
      Opt(verbosity, "low|normal|debug")["-v"]["--verbosity"]("The verbosity level") |
      Opt([&depth](int const d) 
      {
         if (d < 0 || d > 10)
         {
            return ParserResult::runtimeError("Depth must be between 1 and 10");
         }
         else
         {
            depth = d;
            return ParserResult::ok(ParseResultType::Matched);
         }
      }, "depth")["-d"]("Depth of analysis (1 to 10)") |
      Arg(source, "source")("The path of the source") |
      Arg(dest, "destination")("The path to the result") |
      Help(showhelp);

   try
   {
      auto result = parser.parse(Args(argc, argv));
      if (!result)
      {
         std::cerr << "Error in command line: " << result.errorMessage() << std::endl;
         return 1;
      }
      else if (showhelp)
      {
         parser.writeToStream(std::cout);
      }
      else
      {
         std::cout <<
            "source:    " << source << std::endl <<
            "dest:      " << dest << std::endl <<
            "threshold: " << threshold << std::endl <<
            "ignore:    " << ignore << std::endl <<
            "verbosity: " << verbosity << std::endl <<
            "dept:      " << depth << std::endl;
      }
   }
   catch (std::exception const & e)
   {
      std::cout << e.what() << std::endl;
   }

   return 0;
}

We can split this program into several parts:

  • Declaration of variables to hold values of command line arguments.
  • Creation of a parser by combining individual parsers clara::Opt, clara::Arg, and clara::Help.
  • Parsing the command line with parse().
  • Interpreting the result and doing something based on the argument value. In this example, if the arguments were correctly parsed they are just printed to the console.

clara::Opt and clara::Arg are individiual parsers that are very similar execept for one important difference: the first specify an argument tied to an option (such as -v normal or --ignore) and the later to arguments that are not tied to an option (and therefore mising the square bracket names). Options are specified with - or -- but on Windows / is also accepted. clara::Help is yet another parser that always binds to a boolean variable setting it to true if any of the standard help options, -h, --help and -?, are found.

To understand the Opt parser’s parts let’s take the following example:

Opt(threshold, "threshold")["-t"]("Threshold for analysis inclusion")

There are three main parts in this construction:

  • (threshold, "threshold") specifies a variable (threshold) that will receive the value of the command line argument tied to the option and a hint ("threshold") for the value.
  • ["-t"] indicates one or more names for the option (for multiple names just chain them like ["-t"]["-threshold"]).
  • ("Threshold for analysis inclusion") indicates a description of the option.

The syntax for Arg and Help is very similar, except that the square bracket names are missing. The description part is optional for all parser types.

If you want to validate the input values for an option or argument, for instance to restrict a numeric value to a certain range, you can specify a lambda instead of the variable. This is shown in the previous example with the parsing of the depth option.

Opt([&depth](int const d) 
{
   if (d < 0 || d > 10)
   {
      return ParserResult::runtimeError("Depth must be between 1 and 10");
   }
   else
   {
      depth = d;
      return ParserResult::ok(ParseResultType::Matched);
   }
}, "depth")["-d"]("Depth of analysis (1 to 10)")

Should the parsing be successful, you must return ParserResult::ok(ParseResultType::Matched). Otherwise, you can return an error, such as ParserResult::runtimeError("Depth must be between 1 and 10").

In the example above, verbosity is variable of type verbosity_level, which is a scoped enum. You can only bind to enums if have overwritten operator >> for them. Therefore, for my example to work, I have also implemented the following:

std::ostream& operator <<(std::ostream& stream, verbosity_level & level)
{
   switch (level)
   {
   case verbosity_level::low:
      stream << "low";
      break;
   case verbosity_level::normal:
      stream << "normal";
      break;
   case verbosity_level::debug:
      stream << "debug";
      break;
   }
   return stream;
}

std::istream& operator >>(std::istream& stream, verbosity_level & level)
{
   std::string token;
   stream >> token;
   if (token == "low") level = verbosity_level::low;
   else if (token == "normal") level = verbosity_level::normal;
   else if (token == "debug") level = verbosity_level::debug;
   else {
      auto parsed = false;
      try {
         auto n = std::stoi(token);
         if (n >= static_cast<int>(verbosity_level::low) &&
             n <= static_cast<int>(verbosity_level::debug))
         {
            level = static_cast<verbosity_level>(n);
            parsed = true;
         }
      }
      catch (std::exception const &) { }

      if(!parsed)
         throw std::runtime_error("Invalid verbosity level value");
   }
   
   return stream;
}

Parsing the actual command line options is done with a call to parse() and passing the arguments through an Args object. The result of the call is a clara::detail::InternalParseResult object. There are various ways to check its value. The explicit operator bool returns true if the type of the result is Ok (the other options being LogicError and RuntimeError).

You can actually print the parser description to an output stream using the writeToStream() method. This is how the result looks for the example shown above:

clara_demo.exe /?
usage:
  clara_demo.exe [<source> <destination>] options

where options are:
  -t <threshold>                        Threshold for analysis inclusion
  -i, --ignore                          Ignore conflicts
  -v, --verbosity <low|normal|debug>    The verbosity level
  -d <depth>                            Depth of analysis (1 to 10)
  -?, -h, --help                        display usage information

Let’s look at some parsing examples for the program above:

  • no arguments provided
    clara_demo.exe
    source:
    dest:
    threshold: 0
    ignore:    0
    verbosity: low
    dept:      0
    
  • only one argument provided (i.e. source)
    clara_demo.exe c:\temp\input.dat
    source:    c:\temp\input.dat
    dest:
    threshold: 0
    ignore:    0
    verbosity: low
    dept:      0
    
  • both arguments provided (source and dest)
    clara_demo.exe c:\temp\input.dat c:\temp\output.txt
    source:    c:\temp\input.dat
    dest:      c:\temp\output.txt
    threshold: 0
    ignore:    0
    verbosity: low
    dept:      0
    
  • additionally option -t provided
    clara_demo.exe c:\temp\input.dat c:\temp\output.txt -t 3.14
    source:    c:\temp\input.dat
    dest:      c:\temp\output.txt
    threshold: 3.14
    ignore:    0
    verbosity: low
    dept:      0
    
  • additionally option -i or --ignore provided
    clara_demo.exe c:\temp\input.dat c:\temp\output.txt -t 3.14 -i
    source:    c:\temp\input.dat
    dest:      c:\temp\output.txt
    threshold: 3.14
    ignore:    1
    verbosity: low
    dept:      0
    
  • additionally option -d provided with valid numerical value in accepted range
    clara_demo.exe c:\temp\input.dat c:\temp\output.txt -t 3.14 -i -d 5
    source:    c:\temp\input.dat
    dest:      c:\temp\output.txt
    threshold: 3.14
    ignore:    1
    verbosity: low
    dept:      5
    
  • additionally option -d provided with invalid numerical value outside the accepted range
    clara_demo.exe c:\temp\input.dat c:\temp\output.txt -t 3.14 -i -d 55
    Error in command line: Depth must be between 1 and 10
    
  • additionally option -v provided with valid numerical value
    clara_demo.exe c:\temp\input.dat c:\temp\output.txt -t 3.14 -i -d 5 -v 1
    source:    c:\temp\input.dat
    dest:      c:\temp\output.txt
    threshold: 3.14
    ignore:    1
    verbosity: normal
    dept:      5
    
  • additionally option -v provided with valid textual value
    clara_demo.exe c:\temp\input.dat c:\temp\output.txt -t 3.14 -i -d 5 -v debug
    source:    c:\temp\input.dat
    dest:      c:\temp\output.txt
    threshold: 3.14
    ignore:    1
    verbosity: debug
    dept:      5
    
  • additionally option -v provided with invalid numerical value
    clara_demo.exe c:\temp\input.dat c:\temp\output.txt -t 3.14 -i -d 5 -v 10
    Invalid verbosity level value
    
  • additionally option -v provided with invalid textual value
    clara_demo.exe c:\temp\input.dat c:\temp\output.txt -t 3.14 -i -d 5 -v high
    Invalid verbosity level value
    

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.