author | Olivier Brunel
<jjk@jjacky.com> 2023-04-15 18:29:18 UTC |
committer | Olivier Brunel
<jjk@jjacky.com> 2023-05-20 18:06:36 UTC |
parent | 6ac0611ccd5ccb4c42f08d2a5b2378474db61b2a |
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, \