1 Introduction
Command Line Interface (CLI) definition language is a domain-specific language (DSL) for defining command line interfaces of C++ programs. CLI definitions are automatically translated to C++ classes using the CLI compiler. These classes implement parsing of the command line arguments and provide a convenient and type-safe interface for accessing the extracted data.
Beyond this guide, you may also find the following sources of information useful:
- CLI Compiler Command Line Manual
- The
INSTALL
file in the CLI distribution provides build instructions for various platforms. - The
examples/
directory in the CLI distribution contains a collection of examples and a README file with an overview of each example. - The cli-users mailing list is the place to ask technical questions about the CLI language and compiler. Furthermore, the cli-users mailing list archives may already have answers to some of your questions.
2 Hello World Example
In this chapter we will examine how to define a very simple command
line interface in CLI, translate this interface to C++, and use the
result in an application. The code presented in this chapter is based
on the hello
example which can be found in the
examples/hello/
directory of the CLI distribution.
2.1 Defining Command Line Interface
Our hello
application is going to print a greeting
line for each name supplied on the command line. It will also
support two command line options, --greeting
and --exclamations
, that can be used to
customize the greeting line. The --greeting
option allows us to specify the greeting phrase instead of the
default "Hello"
. The --exclamations
option is used to specify how many exclamation marks should
be printed at the end of each greeting. We will also support
the --help
option which triggers printing of the
usage information.
We can now write a description of the above command line interface
in the CLI language and save it into hello.cli
:
include <string>; class options { bool --help; std::string --greeting = "Hello"; unsigned int --exclamations = 1; };
While some details in the above code fragment might not be completely
clear (the CLI language is covered in greater detail in the next
chapter), it should be easy to connect declarations in
hello.cli
to the command line interface described in
the preceding paragraphs. The next step is to translate this
interface specification to C++.
2.2 Translating CLI Definitions to C++
Now we are ready to translate hello.cli
to C++.
To do this we invoke the CLI compiler from a terminal (UNIX) or
a command prompt (Windows):
$ cli hello.cli
This invocation of the CLI compiler produces three C++ files:
hello.hxx
hello.ixx
, and
hello.cxx
. You can change the file name extensions
for these files with the compiler command line options. See the
CLI
Compiler Command Line Manual for more information.
The following code fragment is taken from hello.hxx
; it
should give you an idea about what gets generated:
#include <string> class options { public: options (int argc, char** argv); options (int argc, char** argv, int& end); // Option accessors. // public: bool help () const; const std::string& greeting () const; unsigned int exclamations () const; private: .. };
The options
C++ class corresponds to the options
CLI class. For each option in this CLI class an accessor function is
generated inside the C++ class. The options
C++ class also
defines a number of overloaded constructs that we can use to parse the
argc/argv
array. Let's now see how we can use this generated
class to implement option parsing in our hello
application.
2.3 Implementing Application Logic
At this point we have everything we need to implement our application:
#include <iostream> #include "hello.hxx" using namespace std; void usage () { cerr << "usage: driver <options> <names>" << endl << " [--help]" << endl << " [--greeting <string>]" << endl << " [--exclamations <integer>]" << endl; } int main (int argc, char* argv[]) { try { int end; // End of options. options o (argc, argv, end); if (o.help ()) { usage (); return 0; } if (end == argc) { cerr << "no names provided" << endl; usage (); return 1; } // Print the greetings. // for (int i = end; i < argc; i++) { cout << o.greeting () << ", " << argv[i]; for (unsigned int j = 0; j < o.exclamations (); j++) cout << '!'; cout << endl; } } catch (const cli::exception& e) { cerr << e << endl; usage (); return 1; } }
At the beginning of our application we create the options
object which parses the command line. The end
variable
contains the index of the first non-option argument. We then access
the option values as needed during the application execution. We also
catch and print cli::exception
in case something goes
wrong, for example, an unknown option is specified or an option value
is invalid.
2.4 Compiling and Running
After saving our application from the previous section in
driver.cxx
, we are ready to build and run our program.
On UNIX this can be done with the following commands:
$ c++ -o driver driver.cxx hello.cxx $ ./driver world Hello, world! $ ./driver --greeting Hi --exclamations 3 John Jane Hi, John!!! Hi, Jane!!!
We can also test the error handling:
$ ./driver -n 3 Jane unknown option '-n' usage: driver <options> <names> [--help] [--greeting <string>] [--exclamations <integer>] $ ./driver --exclamations abc Jane invalid value 'abc' for option '--exclamations' usage: driver <options> <names> [--help] [--greeting <string>] [--exclamations <integer>]
3 CLI Language
This chapter describes the CLI language and its mapping to C++. A CLI file consists of zero or more Include Directives followed by one or more Namespace Definitions or Option Class Definitions. C and C++-style comments can be used anywhere in the CLI file except in character and string literals.
3.1 Option Class Definition
The central part of the CLI language is option class. An option class contains one or more option definitions, for example:
class options { bool --help; int --compression; };
If we translate the above CLI fragment to C++, we will get a C++ class with the following interface:
class options { public: options (int argc, char** argv, cli::unknown_mode opt_mode = cli::unknown_mode::fail, cli::unknown_mode arg_mode = cli::unknown_mode::stop); options (int start, int argc, char** argv, cli::unknown_mode opt_mode = cli::unknown_mode::fail, cli::unknown_mode arg_mode = cli::unknown_mode::stop); options (int argc, char** argv, int& end, cli::unknown_mode opt_mode = cli::unknown_mode::fail, cli::unknown_mode arg_mode = cli::unknown_mode::stop); options (int start, int argc, char** argv, int& end, cli::unknown_mode opt_mode = cli::unknown_mode::fail, cli::unknown_mode arg_mode = cli::unknown_mode::stop); options (const options&); options& operator= (const options&); public: bool help () const; int compression () const; };
An option class is mapped to a C++ class with the same name. The C++ class defines a set of public overloaded constructors, a public copy constructor and an assignment operator, as well as a set of public accessor functions corresponding to option definitions.
The argc/argv
arguments in the overloaded constructors
are used to pass the command line arguments array, normally as passed
to main()
. The start
argument is used to
specify the position in the arguments array from which the parsing
should start. The constructors that don't have this argument, start
from position 1, skipping the executable name in argv[0]
.
The end
argument is used to return the position in
the arguments array where the parsing of options stopped. This is the
position of the first program argument, if any.
The opt_mode
and arg_mode
arguments
specify the parser behavior when it encounters an unknown option
and argument, respectively. The unknown_mode
type
is part of the generated CLI runtime support code. It has the
following interface:
namespace cli { class unknown_mode { public: enum value { skip, stop, fail }; unknown_mode (value v); operator value () const; }; }
If the mode is skip
, the parser skips an unknown
option or argument and continue parsing. If the mode is
stop
, the parser stops the parsing process. The
position of the unknown entity is stored in the end
argument. If the mode is fail
, the parser throws the
cli::unknown_option
or cli::unknown_argument
exception (described blow) on encountering an unknown option or argument,
respectively.
The parsing constructor (those with the argc/argv
arguments)
can throw the following exceptions: cli::unknown_option
,
cli::unknown_argument
, cli::missing_value
, and
cli::invalid_value
. The first two exceptions are thrown
on encountering unknown options and arguments, respectively, as
described above. The missing_value
exception is thrown when
an option value is missing. The invalid_value
exception is
thrown when an option value is invalid, for example, a non-integer value
is specified for an option of type int
.
All CLI exceptions are derived from the common cli::exception
class which implements the polymorphic std::ostream
insertion.
For example, if you catch the cli::unknown_option
exception as cli::exception
and print it to
std::cerr
, you will get the error message corresponding
to the unknown_option
exception.
The exceptions described above are part of the generated CLI runtime support code and have the following interfaces:
#include <exception> namespace cli { class exception: public std::exception { public: virtual void print (std::ostream&) const = 0; }; inline std::ostream& operator<< (std::ostream& os, const exception& e) { os << e; return os; } class unknown_option: public exception { public: unknown_option (const std::string& option); const std::string& option () const; virtual void print (std::ostream&) const; virtual const char* what () const throw (); }; class unknown_argument: public exception { public: unknown_argument (const std::string& argument); const std::string& argument () const; virtual void print (std::ostream&) const; virtual const char* what () const throw (); }; class missing_value: public exception { public: missing_value (const std::string& option); const std::string& option () const; virtual void print (std::ostream&) const; virtual const char* what () const throw (); }; class invalid_value: public exception { public: invalid_value (const std::string& option, const std::string& value); const std::string& option () const; const std::string& value () const; virtual void print (std::ostream&) const; virtual const char* what () const throw (); }; }
3.2 Option Definition
An option definition consists of there components: type,
name, and default value. An option type can be any
C++ type as long as its string representation can be parsed using
the std::istream
interface. If the option type is
user-defined then you will need to include its declaration using
the Include Directive.
An option of a type other than bool
is expected to
have a value. An option of type bool
is treated as
a flag and does not have a value. That is, a mere presence of such
an option on the command line sets this option's value to
true
.
The name component specifies the option name as it will be entered
in the command line. A name can contain any number of aliases separated
by |
. The C++ accessor function name is derived from the
first name by removing any leading special characters, such as
-
, /
, etc., and replacing special characters
in other places with underscore. For example, the following option
definition:
class options { int --compression-level | --comp | -c; };
Will result in the following accessor function:
class options { int compression_level () const; };
While any option alias can be used on the command line to specify this option's value.
If the option name conflicts with one of the CLI language keywords, it can be specified as a string literal:
class options { bool "int"; };
The final component of the option definition is the optional default
value. If the default value is not specified, then the option is
initialized with the default constructor. In particular, this means
that a bool
option will be initialized to false
,
an int
option will be initialized to 0
, etc.
Similar to C++ variable initialization, the default option value can be specified using two syntactic forms: an assignment initialization and constructor initialization. The two forms are equivalent except that the constructor initialization can be used with multiple arguments, for example:
include <string>; class options { int -i1 = 5; int -i2 (5); std::string -s1 = "John"; std::string -s2 ("Mr John Doe", 8, 3); };
The assignment initialization supports character, string, boolean, and simple integer literals (including negative integers) as well as identifiers. For more complex expressions use the constructor initialization or wrap the expressions in parenthesis, for example:
include "constants.hxx"; // Defines default_value. class options { int -a = default_value; int -b (25 * 4); int -c = (25 / default_value + 3); };
By default, when an option is specified two or more times on the command
line, the last value overrides all the previous ones. However, a number
of standard C++ containers are handled differently to allow collecting
multiple option values or building key-value maps. These
containers are std::vector
, std::set
, and
std::map
.
When std::vector
or std::set
is specified
as an option type, all the values for this option are inserted into the
container in the order they are encountered. As a result,
std::vector
will contain all the values, including
duplicates while std::set
will contain all the unique
values. For example:
include <set>; include <vector>; class options { std::vector<int> --vector | -v; std::set<int> --set | -s; };
If we have a command line like this:
-v 1 -v 2 -v 1 -s 1 -s 2 -s 1
, then the vector returned
by the vector()
accessor function will contain three
elements: 1
, 2
, and 1
while
the set returned by the set()
accessor will contain
two elements: 1
and 2
.
When std::map
is specified as an option type, the option
value is expected to have two parts: the key and the value, separated
by =
. All the option values are then parsed into key/value
pairs and inserted into the map. For example:
include <map>; include <string>; class options { std::map<std::string, std::string> --map | -m; };
The possible option values for this interface are: -m a=A
,
-m =B
(key is an empty string), -m c=
(value
is an empty string), or -m d
(same as -m d=
).
3.3 Include Directive
If you are using user-defined types in your option definitions,
you will need to include their declarations with the include
directive. Include directives can use < >
or
" "
-enclosed paths. The CLI compiler does not
actually open or read these files. Instead, the include directives
are translated to C++ preprocessor #include
directives
in the generated C++ header file. For example, the following CLI
definition:
include <string>; include "types.hxx"; // Defines the name_type class. class options { std::string --string; name_type --name; };
Will result in the following C++ header file:
#include <string> #include "types.hxx" class options { ... const std::string& string () const; const name_type& name () const; ... };
Without the #include
directives the std::string
and name_type
types in the options
class would
be undeclared and result in compilation errors.
3.4 Namespace Definition
Option classes can be placed into namespaces which are translated directly to C++ namespaces. For example:
namespace compiler { namespace lexer { class options { int --warning-level = 0; }; } namespace parser { class options { int --warning-level = 0; }; } namespace generator { class options { int --target-width = 32; }; } }
The above CLI namespace structure would result in the equivalent C++ namespaces structure:
namespace compiler { namespace lexer { class options { int warning_level () const; }; } namespace parser { class options { int warning_level () const; }; } namespace generator { class options { int target_width () const; }; } }