Welcome to little lamb

Code » limb » commit 89724c6

Add command.h: helpers to make a command-based program

author Olivier Brunel
2023-04-15 18:29:18 UTC
committer Olivier Brunel
2023-05-20 18:06:36 UTC
parent 6ac0611ccd5ccb4c42f08d2a5b2378474db61b2a

Add command.h: helpers to make a command-based program

src/doc/command.h.0.md +224 -0
src/doc/command.h/dienocommand.3.md +31 -0
src/doc/command.h/getcommandordie.3.md +37 -0
src/doc/output.h.0.md +10 -0
src/doc/output.h/die.3.md +17 -0
src/liblimb/command.h/dienocommand.c +25 -0
src/liblimb/command.h/getcommandordie.c +29 -0
src/liblimb/include/limb/command.h +31 -0
src/liblimb/include/limb/output.h +5 -0

diff --git a/src/doc/command.h.0.md b/src/doc/command.h.0.md
new file mode 100644
index 0000000..ea3d8ab
--- /dev/null
+++ b/src/doc/command.h.0.md
@@ -0,0 +1,224 @@
+% 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 :
+
+```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 :
+
+```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.
+
+```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);
+}
+```
diff --git a/src/doc/command.h/dienocommand.3.md b/src/doc/command.h/dienocommand.3.md
new file mode 100644
index 0000000..7c488a7
--- /dev/null
+++ b/src/doc/command.h/dienocommand.3.md
@@ -0,0 +1,31 @@
+% limb manual
+% dienocommand(3)
+
+# NAME
+
+dienocommand - terminate a process execution showing the application's help
+screen, or the usage line
+
+# SYNOPSIS
+
+    #include <limb/command.h>
+
+```pre hl
+void dienocommand(int <em>exit</em>, const char *<em>usage</em>, const char *<em>help</em>)
+```
+
+# DESCRIPTION
+
+The `dienocommand`() function will show the application's help screen if `help`
+is not NULL, else a simple usage line.
+
+In the later case, [dieusage](3) is called with the given `exit` and `usage`
+arguments.
+
+In the former case, [errhelp](3) is used to show the given `usage` and `help`,
+after which the list of all available commands - name and description - is
+shown. It then terminate process execution with a return code of 0.
+Said list is established using the `commands` array as described in
+[command.h](0).
+
+This function never returns.
diff --git a/src/doc/command.h/getcommandordie.3.md b/src/doc/command.h/getcommandordie.3.md
new file mode 100644
index 0000000..c81d8e9
--- /dev/null
+++ b/src/doc/command.h/getcommandordie.3.md
@@ -0,0 +1,37 @@
+% limb manual
+% getcommandordie(3)
+
+# NAME
+
+getcommandordie - parse command's name and return the corresponding *struct
+command*
+
+# SYNOPSIS
+
+    #include <limb/command.h>
+
+```pre hl
+struct command *getcommandordie(int <em>exit</em>, const char *<em>usage</em>, const char *<em>arg</em>)
+```
+
+# DESCRIPTION
+
+The `getcommandordie`() function will read the string `arg` to find the matching
+command, using the `commands` array as described in [command.h](0). It needs not
+to be an exact match, partial match are valid so long as they are unique.
+For more, refer to [byte_get_match](3).
+
+If no command matched the given `arg`, or more than one did, a warning is
+emitted (through the [warn](3) family of functions) and [dieusage](3) is called
+with the given `exit` and `usage` arguments to terminate process execution.
+
+If a command matched, its corresponding structure (from the `commands` array) is
+returned.
+
+# RETURN VALUE
+
+The `getcommandordie`() returns a pointer to the matching *struct command* for
+the name in `arg` is there is only one.
+
+Otherwise it does /not/ return but terminates process execution through
+[dieusage](3).
diff --git a/src/doc/output.h.0.md b/src/doc/output.h.0.md
index c19c675..870d34b 100644
--- a/src/doc/output.h.0.md
+++ b/src/doc/output.h.0.md
@@ -39,6 +39,13 @@ The following functions/macros are defined :
 : [die](3)
 :: Same as [errdie](3) but prefixed with "*PROG*: "
 
+: [diecmdusage](3)
+:: Same as [dieusage](3) but adds the command's name and usage.
+
+: [diecmdhelp](3)
+:: Same as [diehelp](3) but adds the command's name, usage and show its help
+:: screen.
+
 : [dief](3)
 :: Same as [die](3) but with an extra "fatal: "
 
@@ -72,6 +79,9 @@ The following functions/macros are defined :
 : [errdie](3)
 :: Same as [err](3) but ends program execution using given exit code
 
+: [errhelp](3)
+:: Same as [diehelp](3)  but does /not/ end program execution.
+
 : [errverb](3)
 :: Same as [err](3) but with *OLVL_VERBOSE*
 
diff --git a/src/doc/output.h/die.3.md b/src/doc/output.h/die.3.md
index 2bf5740..bf3564a 100644
--- a/src/doc/output.h/die.3.md
+++ b/src/doc/output.h/die.3.md
@@ -23,6 +23,11 @@ void diefusys(<em>exit</em>, ...)
 void dieusage(<em>exit</em>, <em>usage</em>)
 void diehelp(<em>exit</em>, <em>usage</em>, <em>help</em>)
 
+void errhelp(<em>exit</em>, <em>usage</em>, <em>help</em>)
+
+void diecmdusage(<em>exit</em>, <em>usage</em>, <em>command</em>)
+void diecmdhelp(<em>exit</em>, <em>usage</em>, <em>command</em>)
+
 void dieversion(<em>version</em>, <em>year1</em>, <em>year2</em>, <em>author</em>, <em>url</em>, <em>license</em>)
 ```
 
@@ -34,6 +39,10 @@ All of those are implemented as macros (to [err_putmsgdie](3) or
 then terminate the calling process via [\_exit](3) using `exit` as return/status
 code.
 
+One exception is `errhelp`() which is the same as `diehelp`() only based on
+[err_putmsg](3), so it will /not/ end program execution. It is mainly intended
+for internal use through [dienocommand](3).
+
 ! INFO: output buffers
 ! All of those are using output buffers, and will therefore write to the output
 ! buffers for *stdout* or *stderr*, as well as all the extra buffers set up,
@@ -61,6 +70,14 @@ and a space as prefix, then the `usage` string.
 The `diehelp`() macro is similar to `dieusage`(), only adding two new lines
 followed by the string `help`.
 
+The `diecmdusage`() macro is similar to `dieusage`() but will add the command's
+name and usage line. The command must be a *struct command* as defined in
+[command.h](0).
+
+The `diecmdhelp`() macro is similar to `diehelp`() bit will add the command's
+name and usage, and show its own help screen. As with `diecmdusage`() the
+command must be a *struct command* as defined in [command.h](0).
+
 The `dieversion`() macro writes the program's name (i.e. value of `PROG`), the
 string " version " then the specified `version` and a new line.
 On a second line is written "Copyright (C) " then `year1`, followed by a dash
diff --git a/src/liblimb/command.h/dienocommand.c b/src/liblimb/command.h/dienocommand.c
new file mode 100644
index 0000000..8918648
--- /dev/null
+++ b/src/liblimb/command.h/dienocommand.c
@@ -0,0 +1,25 @@
+/* This file is part of limb                           https://lila.oss/limb
+ * Copyright (C) 2023 Olivier Brunel                          jjk@jjacky.com */
+/* SPDX-License-Identifier: GPL-2.0-only */
+#include <unistd.h> /* _exit() */
+#include <limb/command.h>
+#include <limb/output.h>
+
+void
+dienocommand(int exit, const char *usage, const char *help)
+{
+    if (help) {
+        errhelp(usage, help);
+        err("Available commands:");
+        char sp[17] = "                 ";
+        for (unsigned i = 0; i < n_commands; ++i) {
+            int n = sizeof(sp) - strlen(commands[i]->name);
+            sp[n] = 0;
+            err("  ", commands[i]->name, sp, commands[i]->desc);
+            sp[n] = ' ';
+        }
+        _exit(0);
+    } else {
+        dieusage(exit, usage);
+    }
+}
diff --git a/src/liblimb/command.h/getcommandordie.c b/src/liblimb/command.h/getcommandordie.c
new file mode 100644
index 0000000..ca13f96
--- /dev/null
+++ b/src/liblimb/command.h/getcommandordie.c
@@ -0,0 +1,29 @@
+/* This file is part of limb                           https://lila.oss/limb
+ * Copyright (C) 2023 Olivier Brunel                          jjk@jjacky.com */
+/* SPDX-License-Identifier: GPL-2.0-only */
+#include <limb/bytestr.h>
+#include <limb/command.h>
+#include <limb/output.h>
+
+struct command *
+getcommandordie(int exit, const char *usage, const char *arg)
+{
+    const char *names[n_commands + 1];
+    for (unsigned i = 0; i < n_commands; ++i)
+        names[i] = commands[i]->name;
+    names[n_commands] = NULL;
+
+    size_t alen = strlen(arg);
+    int r, first = -1;
+    r = byte_get_match(&first, arg, alen, names);
+    if (r < 0) {
+        warn("unknown command: ", arg);
+        /* more than 1 partial match? */
+        if (first >= 0)
+            list_matches(err_putmsg, OLVL_NORMAL, "did you mean ", NULL,
+                         " or ", " ?", arg, alen, first, names);
+        dieusage(exit, usage);
+    }
+
+    return commands[r];
+}
diff --git a/src/liblimb/include/limb/command.h b/src/liblimb/include/limb/command.h
new file mode 100644
index 0000000..5b23b39
--- /dev/null
+++ b/src/liblimb/include/limb/command.h
@@ -0,0 +1,31 @@
+/* This file is part of limb                           https://lila.oss/limb
+ * Copyright (C) 2023 Olivier Brunel                          jjk@jjacky.com */
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef LIMB_COMMAND_H
+#define LIMB_COMMAND_H
+
+typedef int (*main_fn) (int argc, const char *argv[], const char *env[],
+                        const char usage[], void *ctx);
+
+#define COMMAND(n, d, u, h) \
+    int n ## _main (int argc, const char *argv[], const char *env[], \
+                    const char usage[], void *ctx); \
+    struct command command_ ## n = { #n, d, u, h, n ## _main }
+
+struct command {
+    const char *name;
+    const char *desc;
+    const char *usage;
+    const char *help;
+    main_fn main;
+};
+
+extern struct command *commands[];
+extern unsigned n_commands;
+
+#define N_COMMANDS  unsigned n_commands = sizeof(commands) / sizeof(*commands) - 1
+
+extern void dienocommand(int exit, const char *usage, const char *help);
+extern struct command *getcommandordie(int exit, const char *usage, const char *arg);
+
+#endif /* LIMB_COMMAND_H */
diff --git a/src/liblimb/include/limb/output.h b/src/liblimb/include/limb/output.h
index fea3e1d..6f011d7 100644
--- a/src/liblimb/include/limb/output.h
+++ b/src/liblimb/include/limb/output.h
@@ -62,9 +62,14 @@ extern const char *PROG;
 #define diefsys(e, ...)     diesys(e, "fatal: ", __VA_ARGS__)
 #define diefusys(e, ...)    diefsys(e, "unable to ", __VA_ARGS__)
 
+#define errhelp(u, h)       err("usage: ", PROG, " ", u, "\n\n", h)
+
 #define dieusage(e, u)      errdie(e, "usage: ", PROG, " ", u)
 #define diehelp(e, u, h)    errdie(e, "usage: ", PROG, " ", u, "\n\n", h)
 
+#define diecmdusage(e, u, c)errdie(e, "usage: ", PROG, " ", u, (c)->name, " ", (c)->usage)
+#define diecmdhelp(e, u, c) errdie(e, "usage: ", PROG, " ", u, (c)->name, " ", (c)->usage, "\n", (c)->desc, "\n\n", (c)->help)
+
 #define dieversion(v , yb, ye, a, u, l) \
                             outdie(0, PROG, " version ", v, "\n", \
                                    "Copyright (C) ", yb, \