limb 0.2.0

2024-01-09

command.h(0)
limb manual
command.h(0)

NAME

command.h - helpers for implementing command-based application

SYNOPSIS

#include <limb/command.h>

DESCRIPTION

The header defines helpers to implement a command-based application.

Types

The following types are defined :

main_fn

Prototype for the command's main function.

Structures

The following structures are defined :

struct command

A structure to define a command and help showing its help screen.

Functions

The following functions are defined :

dirnocommand(3)

To terminate a process execution showing the application's help screen, or the usage line.

getcommandordie(3)

To parse the command's name and return the corresponding struct command.

Macros

The following macros are defined :

COMMAND(name, desc, usage, help)

To define a new command.

N_COMMANDS

To be used after defining the commands, to define n_commands with the number of commands.

HOW DOES IT WORK?

The idea behind those helpers is to create an application that will be command-based, that is its first argument is to be the name of the command to run. Think git(1) for example.

Writing a new command

To help with the basics of such an interface, notably handling the help screens, one can use this header. Then, to create a new command, say test, one could write a file test.c like so :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
c#include <limb/command.h>
#include <limb/exitcode.h>

COMMAND(test, "Short description of the test command",
        "[OPTION..] [<name>]",
" -n, --dry-run                         Dry mode, i.e. don't actually run the test\n"
);


int
test_main(int argc, const char *argv[], const char *env[], const char usage[], void *ctx_)
{
    if (argc > 1)
        diecmdusage(EX_USAGE, usage, &command_test);
    return 0;
}

The macro COMMAND() defines a new structure named command_test which holds pointers to the different strings to be a short description, usage line and help screen of the command, respectively.

It also declares the command's main function which must follow the following prototype :

int (*main_fn) (int argc, const char *argv[], const char *env[], const char usage[], void *ctx)

Much like a regular's main function, including the environment, and adding as extra argument the usage prefix to be shown if needed (for e.g; global options), and a pointer intended to allow a context to be passed from the main program, i.e. the actual main() function.

Though we don't do anything here, the first thing to do would likely be parsing the command line, which would only contain options passed to this command; i.e. specified after the command's name on the command line.

Options specified before would have been parsed earlier on, and might have resulted in things set up in the given ctx.

And in case of usage error, diecmdusage(3) is used to handle printing the usage line for this command.

We now need to "register" this command, like all the others.

Registering commands

To know which commands are available, a simple array of struct command needs to be constructed. Those were defined via the COMMAND() macro seen earlier.

A simple commands.c file could look like this :

1
2
3
4
5
6
7
8
9
10
c#include <limb/command.h>

extern struct command command_test;

struct command *commands[] = {
    &command_test,
    0
};

N_COMMANDS;

Here comes the declaration of out structure/command, which is then added to the array listing all commands. They will be listed in the same order in the application's help screen, so one might want to keep them grouped by purposes or alphabetically ordered.

The N_COMMANDS simply defines the unsigned n_commands variable, holding the number of commands n the commands array.

At this point, we're ready to work on our main main().

Main application

Now we can start working on our main file, and the actual entry point of the application.

Here we'll have a function to handle parsing of the command line. This will actually only process options before the command's name. This is intended for the --help option as well as any global options.

It will also use the dienocommand(3) function to show the application's help screen, automatically listing all available commands, with their descriptions, at the end.

Lastly the getcommandordie(3) function is used to parse the command's name, which may have been abbreviated, and return the corresponding structure. If no command matches the given name (or more than one does), an error and usage line is shown and program execution terminated.

After handling the global options, the application can then simply call the command's own main function, or use diecmdhelp(3) to show the command's help screen if needed.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
c#include <limb/command.h>
#include <limb/exitcode.h>
#include <limb/parseopt.h>
#include <limb/output.h>

enum {
    OPT_HELP    = 1 << 0,
    OPT_GLOBAL  = 1 << 1,
};

static struct command *
parse_cmdline(int *argc, const char **argv[], unsigned *ctx)
{
    const char usage[] = "[-h] [-G] command [...]";
    const struct option options[] = {
        OPTION_ARG_REQ ('G', "global",                  0, OPTID_SHORTOPT),
        OPTION_ARG_NONE('h', "help",                    0, OPTID_SHORTOPT),
        OPTION_DONE
    };
    struct parseopt po = { 0 };

    int c;
    while ((c = parseopt(*argc, *argv, options, 0, &po))) switch (c) {
        case 'G':
            *ctx |= OPT_GLOBAL;
            break;
        case 'h':
            *ctx |= OPT_HELP;
            break;
        case -1:
            dieusage(EX_USAGE, usage);
        default:
            die(EX_SOFTWARE, "unexpected return value ", PUTMSG_INT(c), " from parseopt");
    };

    /* no command specified */
    if (po.cur == *argc)
        dienocommand(EX_USAGE, usage, (*ctx & OPT_HELP) ?
" -h, --help                            Show (command's) help screen and exit\n"
: NULL);

    *argc -= po.cur;
    *argv += po.cur;

    return getcommandordie(EX_USAGE, usage, **argv);
}

int
main(int argc, const char *argv[], const char *env[])
{
    const char usage[] = "[-G] ";
    unsigned ctx = 0;

    struct command *command = parse_cmdline(&argc, &argv, &ctx);

    if (ctx & OPT_HELP)
        diecmdhelp(0, usage, command);

    return command->main(argc, argv, env, usage, &ctx);
}
limb 0.1.0
2023-07-24
command.h(0)