General overview
The library works based on a tree of commands and flags. A command a CLI argument that modifies the behaviour of the application or of a previous command, while a flag modifies the behaviour of the application.
Commands have long and short names, a data type, a callback function and a list of subcommands and flags. Flags also have long and short names, a data type and a callback function but do not have any subflags or subcommands, they are always owned by a command.
This architecture allows you to build complex chains of commands and flags that can satisfy the needs of any complex application with a CLI interface, while also having an elegant declarative API that works across both C and C++ seamlessly.
C++ API
First, create an instance of the UCLI::Parser class
which looks like this:
class MLS_PUBLIC_API Parser
{
public:
Parser() noexcept = default;
Parser& setHelpHeader(const char* header) noexcept;
Parser& setHelpFooter(const char* footer) noexcept;
// Set to true by default
Parser& setUseGeneratedHelp(bool bUseGeneratedHelp) noexcept;
// Set to 2 by default
Parser& setHelpSubcommandIndentationSpaces(size_t indentSpaces) noexcept;
// The default is `-`
Parser& setFlagPrefix(char prefix) noexcept;
// The default is `,`
Parser& setArrayDelimiter(char delimiter) noexcept;
// By default, we use strict mode where the default argument/command is called and directly exits. Lenient mode
// replaces calls to invalid commands/flags with the default argument/flag command instead without exiting.
Parser& useLenientMode(bool bUseLenientMode) noexcept;
// Whether to toggle boolean arguments or to set them to true. The default behaviour is to set them to true
Parser& setBoolToggle(bool bToggle) noexcept;
Parser& pushCommand(const Command& command) noexcept;
Parser& pushFlag(const Flag& flag) noexcept;
Parser& pushDefaultCommand(const Command& command) noexcept;
Parser& pushDefaultFlag(const Flag& flag) noexcept;
Parser& parse(int argc, char** argv) noexcept;
Parser& release() noexcept;
~Parser() noexcept;
}You can now configure the parser, push commands and flags, and when
ready, run the parse function on your main function's
arguments of int argc, char** argv to parse all command
line input.
Caution
In some cases, the parser may allocate memory on the heap that may
need to be cleaned up after it finishes parsing. The destructor of the
class handles cleanup automatically but for cases where you may want to
reuse the same instance, you can clean up that memory by calling the
release() function.
Pushing commands and flags
Once you have created an instance of your parser, you need to push
some commands and flags. To do that, use the
pushCommand(const Command&) and
pushFlag(const Flag&) functions respectively.
Commands
Commands are defined like this:
typedef struct UCLI_Command
{
const char* longName;
char shortName;
const char* description;
const char** defaultValues;
size_t defaultValuesCount;
union
{
struct
{
const char** stringValues;
size_t stringValuesCount;
} stringValues;
bool* boolValue;
};
UCLI_CommandType type;
UCLI_Command* subcommands;
size_t subcommandsCount;
UCLI_Flag* flags;
size_t flagsCount;
UCLI_CommandEvent callback;
void* context;
bool useLiteralFlags;
} UCLI_Command;Each command has a long and short name. To disable the long or short
names for the command respectively, you can set them to
nullptr and 0 respectively.
The description field is used by the built-in help
command to show a helpful description of what the command does.
The defaultValues and defaultValuesCount
array is used to provide default values when the command is a string or
array command and no values are specified. These fields are optional and
they can be set to nullptr and 0.
Tip
In many cases you might want to tell if you're using the default
values. Since we directly copy the pointer and count without any
additional processing, you can set the defaultValuesCount
value to some known value that you can check against later
Tip
Want to show hints for what each positional argument of your command does? Set the hint strings as default arguments of the command and use the tip from above to differentiate between the default and real values.
The boolValue is a boolean pointer that is used when the
command is of type boolean. If the pointer is valid then it is either
enabled or toggled when the command is run, otherwise nothing is done
and only the callback for the command is called.
The stringValues struct containing the
stringValues and stringValuesCount variables
is filled when the command is of type string or array. These fields are
set by the parser, so there is no need to initialize them by
default.
The type field defines the type of the command as a
value of the UCLI::CommandType enum. The enum looks like
this:
typedef enum UCLI_CommandType
{
UCLI_COMMAND_TYPE_VOID = 0,
UCLI_COMMAND_TYPE_BOOL = 0,
UCLI_COMMAND_TYPE_STRING = 1,
UCLI_COMMAND_TYPE_ARRAY = 2
} UCLI_CommandType;The callback field is a function pointer which takes a
const UCLI::Command*(or a const UCLI::Flag*
for flags) and returns a result of the enum type
UCLI::CallbackResult. This callback is called when the
argument is encountered and parsed. The result enum looks like this:
typedef enum UCLI_CallbackResult
{
UCLI_CALLBACK_RESULT_OK = 0,
UCLI_CALLBACK_RESULT_PREMATURE_EXIT = 1,
} UCLI_CallbackResult;If your callback returns
UCLI_CALLBACK_RESULT_PREMATURE_EXIT the library exits
directly and does not call the callbacks for any subsequent commands.
This is intended for use with commands such as --help.
Tip
In many cases you may not want to provide a callback function,
however not providing one will result in an invalid pointer access bug
on our part. To fix this, initialize your callback function with
UCLI_EMPTY_FLAG_CALLBACK and
UCLI_EMPTY_COMMAND_CALLBACK for flags and commands
respectively.
The context field is a void* which can be
set by the user to point to any additional context data that may be
useful to access in the command's callback function.
The useLiteralFlags boolean controls whether flags can
be interpreted literally for string and array commands. Refer to Behaviour
and edge cases for more information on when this is useful.
Commands can have subcommands. They are stored using the
subcommands and subcommandsCount fields.
Commands can own their own layer of command-specific flags. They are
stored using the flags and flagsCount
fields.
Tip
Because the subcommands and flags fields
are of type UCLI::Command* and UCLI::Flag*
respectively, you would normally need to create an array of subcommands
or flags before setting these pointers. This is tedious and can make
your code unreadable.
Fortunately, we provide the UCLI_MAKE_FLAG_ARRAY and
UCLI_MAKE_COMMAND_ARRAY macros, which allow you to
construct an array of commands or flags from an initializer list diretly
in your top level command's initializer list. For example:
parser.pushCommand({
.longName = "build",
...
.subcommands = UCLI_MAKE_COMMAND_ARRAY(
{
...
.flags = UCLI_MAKE_FLAG_ARRAY(
{
...
}
),
.flagsCount = 1,
},
{
...
}
),
.subcommandsCount = 2,
.flags = UCLI_MAKE_FLAG_ARRAY(
{
...
},
{
...
}
),
.flagsCount = 2
});Tip
Similar to the macros in the previous tip, you can also use the
UCLI_MAKE_STRING_ARRAY macro to create an array of
const char* for initializing the defaultValues
array in the same fashion.
Flags
Flags have almost all of the same fields as commands, without the
subcommand and child flag arrays. The only field that is exclusive to
flags is the priority field.
When a command like this is called:
https://madladsquad.com/app hello --flag-1 --flag-2 where the 2 flags belong to
the same command there may be special cases where a flag that is called
should be called before any other flag. The priority field
is a number of type size_t which defines the sorting
priority of each flag's callback.
Flags are sorted in descending order.
Caution
The priority field is divided into 2 number ranges. From
0 to SIZE_MAX / 2 flags sorted in descending
order at the command depth of the given flag. If the priority is between
SIZE_MAX / 2 and SIZE_MAX, then the flag is
moved to the front of the callback queue and is sorted in descending
order based on priority again. This is useful for commands like
--help that need the highest priority when used from
anywhere but it can cause nasty bugs if you're not careful with the
magnitude of your flag's priority.
Tip
For flags like --help you should set the priority to the
maximum value of SIZE_MAX.
Default commands and flags
The default command and default flag are called when an unrecognized
command or flag is encountered. They are of the same
UCLI::Command and UCLI::Flag types, but no
additional parsing is done, thus the only important fields that need to
be filled are the callback function and the
context pointer.
You can set the default command and flag with the
pushDefaultCommand(const Command&) and
pushDefaultFlag(const Flag&) functions
respectively.
Tip
The priority field also affects default flags. It's recommended that
you set the priority to SIZE_MAX for flags like
--help where we the parser should quit directly.
Caution
If default flag or command is set, they will be set as null internally. If you're using the built-in help message function your default command or flag will be the built-in help message.
Customizing the parser
Built-in help message
By default, we enable using the built-in help message. This means
that the parser will insert a help command and a global
--help flag that will automatically print the commands and
flags tree of your application.
You can decide whether you want to use this command using the
setUseGeneratedHelp function.
Subcommands are printend with additional indentation. You can change
the indentation using the
setHelpSubcommandIndentationSpaces(size_t spaces) function.
By default, we set it to 2.
Most applications' help messages have an information header that
describes the application and a footer that informs the user about the
application's license and copyright holder. You can use the
setHelpHeader(const char*) and
setHelpFooter(const char*) functions to set them.
Note
We enable this feature by default.
Caution
When the built-in help message is enabled the parser will replace the
default command and flag with the built-in help command and flag as long
as the default command and flag are not set to nullptr.
Tokenizer settings
By default, we use the - character for flags. You can
change this character using the setFlagPrefix(char)
function.
By default, the array item separator when setting the value of an
argument using the assignment syntax(--flag=a,b,c,d) is set
to ,. You can change this character using the
setArrayDelimiter(char) function.
Strict and lenient mode
By default, the parser runs in strict mode. This means that if an invalid argument is found, we will quit parsing, run the default command/flag and not run anything else.
For applications that don't want to quit directly, you can enable lenient mode, where each invalid command/flag is replaced with a call to the default command/flag and all commands are then executed.
You can enable lenient mode using the
useLenientMode(bool) function.
Boolean behaviour
By default we set boolean arguments to true if encountered, however,
you can change the behaviour to toggle the boolean instead using the
setBoolToggle(bool) function.
Calling the built-in help command from other commands
In many cases you might want to show the help message again in valid commands, for example if your command fails.
You can do this by calling the UCLI::Parser::helpCommand
static member functions or their C equivalent which are postfixed with
F and C for Flag and
Command respectively.
Caution
Make sure you set the context pointer of your command to
be equal to your current parser instance before calling these functions,
since they depend on internal calls.
C API
The C API is the same as the C++ API, except that the
UCLI::Parser class is replaced by the
UCLI_Parser opaque handle. To construct the parser, call
the UCLI_Parser_init() function which will return a handle
to your parser. When you are ready to free the parser's memory call
UCLI_Parser_free(UCLI_Parser*) and provide the parser
handle.
All functions are the same between APIs, except that they accept a
parser handle as their first argument and are all prefixed with
UCLI_Parser_.
Internal fields
The _internal_ctx_
field
Both flags and commands have an _internal_ctx_ field of
type char*. This field is set to nullptr most
of the time, but when the command is the default command, this field is
set to the name of the current command. This is useful for commands like
--help, which allow show an error message when an
unrecognized command is found.
The _internal_parent
field
The _internal_parent field is only present in commands
and is a pointer to the parent command in the command chain. This is
used to navigate the command chain to find flags higher command
depths.
The
stringValues._internal_ field
This field is an anonymous struct that contains the following booleans:
_bFreeStringValues- whether to free the outerstringValuespointer(the array)_bFreeInnerStringValues- whether to free the inner contents of thestringValuesarray