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: