|
ChainCLI
A modern C++20 command-line interface library
|
ChainCLI is a modern C++20 command-line interface library designed to make building CLI applications simple, intuitive, and maintainable. With its fluent API and method chaining approach, ChainCLI handles all the complex command-line parsing, help generation, and logging infrastructure so you can focus on implementing your application's core functionality.
The idea behind this library was to provide a very verbose and easy to understand way to create CLI-Applications, where everything that has to do with the command line interaction is done for you (create help documentation/route commands/logging), so you only have to write the actual logic of your application (e.g the interesting part). To achieve this it heavily relies on the name giving method chaining. To use it simply include the single header file chain_cli.hpp in your project.
In general, the procedure is as follows (see the demos for more specific examples):
The command defined above can then be run via <executable_name> add and produces the help documentation:
Either using the predefined macro
which expands to
or with your own way of calling CliApp::run.
To see library internal logs define
CHAIN_CLI_VERBOSEwhen compiling.
Commands are created with a unique identifier (=the name of the command to invoke it from the command line) and each command can have its own subcommands that can be added with the withSubcommand method. That means overall the commands are structured in a tree that can be traversed via the identifiers.
Below is an example of what the structure of a CliApp could look like
This structure would allow commands like:
myapp math add 1,2,3,4myapp math calc sqrt 16myapp file compress zip file1.txt,file2.txtEach command in the tree can have its own arguments, options, and execution logic, while sharing the common CLI infrastructure provided by the library. The root command (fittingly called myapp here, basically corresponds to the executable itself) can be configured through
Each command can have different arguments added to it that can be configured in itself. There are three types of arguments provided that can be added to a command with the corresponding methods
PositionalArguments - Command::withPositionalArgument
Arguments that are parsed based on the order they were passed to the application and require at least a name (-> constructor argument)
Displayed in the help messages like <positional_name>
OptionArguments - Command::withOptionArgument
Arguments that are parsed based on a preceding flag (usually a long one like --type and a short one like -t) and require at least a (long) name and a value name (-> the constructor arguments)
Displayed in the help messages like [--option_name,-short_option_name <value_name>]
FlagArguments - Command::withFlagArgument
Arguments that only check if the specified flag is present or not (can be used like a toggle) and require at least a (long) name and a short name (-> constructor arguments)
The first two of these also immediately parse corresponding input parts to a value of the type that was provided when creating the argument. All of the arguments can be required, repeatable and can have a options comment that is displayed in the OPTIONS section of the help command for the corresponding command. Repeatable arguments can be provided in a ','-separated list and are then parsed as a vector instead of a single instance.
Instead of using the chaining methods to create the arguments you can also provide the values to the constructor directly like it is done in the following example:
In my opinion this is a lot less verbose but it depends on your preferences.
Arguments can be put into groups, specifically into mutually exclusive or mutually inclusive ones. This can be done by using the corresponding chaining methods Command::withExclusiveGroup or Command::withInclusiveGroup and passing the arguments you want to have in the group. Inclusive groups require all the arguments in the group to be present if one of them is, while exclusive groups allow only one of the arguments to be present.
Using this, one can quite easily create uncallable commands, for example creating a mutually exclusive group where two of the arguments in it are required. The library doesn't check for this and so will not provide any warnings or something similar.
To pass the parsed Arguments to the implemented logic the library uses CliContext instances. These provide access to them using the corresponding methods like CliContext::getPositionArg or CliContext::getArg (internally this one searches through flag, positional and option args). Note that you have to provide the argument name that was specified when creating the argument to retrieve its value. As arguments that are not required don't have to be present, there are methods to check if they were provided, namely CliContext::isOptionArgPresent and equivalents for the other argument types.
Additionally the CliContext objects carry a reference to the CliApps Logger instance which can be accessed through CliContext::Logger so you can use the configured Logger in your own logic.
Internally the text that is printed for help messages is called a docstring and commands have both a short and a long docstring. The first one is used in help message for the whole app (printed when the executable is invoked without a valid command to call or with <executable> –help) and the second one in the help message for each single command (printed via <executable> <command_name> –help).
example of an app help message
example of a command help message
The text in the middle is the short or long description of the command (can be specified for each command with the chaining methods). The display in the first line and the textual representation of the arguments is modeled after docopt meaning:
<> and optional positional arguments in []. They display their name in the brackets: <minuend>() and optional positional arguments in []. They display their name followed by a semicolon and the short name (if one was specified) as well as the value_name enclosed in <>: [--bound,-b <lowest>][]. They display their name and short name (if specified) in the same fashion that option arguments do: [--remainder,-r]... after the argument itself : <summand>...() if required and [] otherwise. Additionally exclusive groups separate their arguments by | whereas inclusive groups simply use spaces. Inclusive groups are required as soon as one of their arguments is required and exclusive groups if every one of their arguments is required : [[--value,-v <number>] | [--name,-n <text>]]The order of the arguments in the display is determined by the order the arguments were added to the command!
The CliConfig struct is used to configure the CliApplication and change default presets. You can either pass your own instance when creating the CliApp or later edit the configuration via CliApp::getConfig. Examples of settings that can be changed this way are the optionsWidth the help messages use for the line length in the Options section and the alignment there or the repeatableDelimiter used to split repeatable arguments (default ","), as well as the executable name or similar project specific details.
The library uses a simple logging module that works by creating a single logger instance and attaching handlers with their own formatters to it. Each Handler is responsible for outputting a message that was formatted by its formatter (the default formatters provided are the message only formatter and one that includes timestamp and loglevel) to a different target (the default handlers provided target either the console or a file).
Both the logger itself and all the handlers have a minimum level and ignore all logs that are below it. The one of the logger can be set with Logger::setLevel for the handlers they have to be specified before adding them to the logger, e.g in the constructor (the default console handler has Trace as its level).
Moreover a simple LogStyle struct (basically a map of LogLevel to strings) is used to style each loglevel with ANSI-escape-sequences. By default this is only used to color the output for the different levels, like so
This is a TRACE message
This is a VERBOSE message
This is a DEBUG message
This is an INFO message
This is a WARNING message
This is a SUCCESS message
This is an ERROR message
You can easily write your own handler or formatter by extending the corresponding abstract base class (AbstractHandler or AbstractFormatter). If needed one can also write their own implementation of the AbstractLogger and pass it when creating the CliApp to use instead of the one the library provides.
The streams available with
Logger::info,Logger::debugand so on have to be manually flushed usingstd::flush!
To generate its help messages the library uses one central class the Docwriter which has references to different types of docformatters: One for each argument type (positional/option/flag), one for a single command and one for the application as a whole.
At the start of the program both the short and long docstring for each command is built by first retrieving the docstrings (called ArgumentDocString for the display with the brackets in the first line and OptionsDocString for the line in the OPTIONS section) for the arguments which results in calls to the docwriter and their regarding formatters. These are then used by a the commandFormatter to build and set the long/short docstring per command.
If a help message then needs to be printed (either the one for the app or for a single command) the already built docstrings of the commands are used by the AppFormatter to produce the final output.
All of these steps can be fully customized by replacing the default formatters with your own implementation of the abstract base classes (AbstractCliAppDocFormatter, AbstractCommandFormatter and AbstractArgDocFormatter) like below:
To parse the input string to actual values the library simply uses the >> operator, therefore you simply have to provide an appropriate overload of that operator for the parsing module to use.
Thats it! If some things are not fully clear yet, try having a look at the demo projects or dive deeper into the detailed API-Reference.