/* SPDX-License-Identifier: LGPL-2.1 */ /** * cli.h - a header-only helper for making Command-Line Interfaces. * * This mini-library allows you to build CLIs of the following type: * * `bin [--key value] [-k value] [--flag] [-f] [--] [non-option-arg]` * * Copyright (c) 2025 - Yaroslav de la Peña Smirnov */ #ifndef CLI_H #define CLI_H #include #include #include #include #include #include #include "../utils.h" /** * enum cli_rc - Return codes for CLI parsing results. * @CLI_RC_OK: everything was fine. * @CLI_RC_ERR: an error unrelated to cli.h happened. * @CLI_RC_BAD_ARGS: there were errors parsing user arguments. * @CLI_RC_BUG: cli.h's API wasn't respected by the program. * * These return codes are meant to be returned by the main function. * @CLI_RC_BAD_ARGS can also be passed by the program's command functions to let * know cli.h that the last argument(s) w(as/ere) not correct so that it * displays the help output. */ enum cli_rc { CLI_RC_OK, CLI_RC_ERR, CLI_RC_BAD_ARGS, CLI_RC_BUG = 69, }; /** * struct cli_ctx - The context for the parsing of options. * @opts: pointer to the array of options originally passed to cli.h. * @argc: the number of arguments passed on to the command. * @argv: the array of command line arguments passed on to the command. */ struct cli_ctx { struct cli_opt **opts; int argc; char *const *argv; }; /** * enum cli_opt_type - The type of option key. * @__CLI_OT_NONE: unused. * @CLI_OT_FLAG: a flag option; has no value. * @CLI_OT_VALUE: a value option; it expects a value after the option key. */ enum cli_opt_type : uint8_t { __CLI_OT_NONE, CLI_OT_FLAG, CLI_OT_VALUE, }; /** * struct cli_opt - A flag or "option" for a command; "abstract class". * @type: the type of the option; see `enum cli_opt_type` for more info. * @shor: short key, e.g. 'k' for "-k". * @lon: long key, e.g. "key" for "--key". * @desc: a short description of the option to be displayed in the help. * @set: setter method; parses and sets the actual value of the option. * * This is an abstract class. You can use the implementations below or make your * own by embedding this struct in your own and setting the @set method. */ struct cli_opt { enum cli_opt_type type; char shor; const char *lon; const char *desc; [[gnu::nonnull(1)]] int (*set)(struct cli_opt *self, const char *val); }; struct cli_opt_long { struct cli_opt opt; long value; }; #define CLI_OPT_LONG(name, sh, ln, dsc) \ struct cli_opt_long name = { \ .opt = \ { \ .type = CLI_OT_VALUE, \ .shor = sh, \ .lon = ln, \ .desc = dsc, \ .set = cli_opt_long_set, \ }, \ } int cli_opt_long_set(struct cli_opt *self, const char *val) { struct cli_opt_long *opt = container_of(self, struct cli_opt_long, opt); errno = 0; opt->value = strtol(val, NULL, 0); if (errno != 0) return CLI_RC_BAD_ARGS; return CLI_RC_OK; } struct cli_opt_ulong { struct cli_opt opt; long value; }; #define CLI_OPT_ULONG(name, sh, ln, dsc) \ struct cli_opt_ulong name = { \ .opt = \ { \ .type = CLI_OT_VALUE, \ .shor = sh, \ .lon = ln, \ .desc = dsc, \ .set = cli_opt_ulong_set, \ }, \ } int cli_opt_ulong_set(struct cli_opt *self, const char *val) { struct cli_opt_ulong *opt = container_of(self, struct cli_opt_ulong, opt); errno = 0; opt->value = strtoul(val, NULL, 0); if (errno != 0) return CLI_RC_BAD_ARGS; return CLI_RC_OK; } struct cli_opt_string { struct cli_opt opt; const char *value; }; #define CLI_OPT_STRING(name, sh, ln, dsc) \ struct cli_opt_string name = { \ .opt = \ { \ .type = CLI_OT_VALUE, \ .shor = sh, \ .lon = ln, \ .desc = dsc, \ .set = cli_opt_string_set, \ }, \ } int cli_opt_string_set(struct cli_opt *self, const char *val) { struct cli_opt_string *opt = container_of(self, struct cli_opt_string, opt); if (!val) return CLI_RC_BAD_ARGS; opt->value = val; return CLI_RC_OK; } struct cli_opt_flag { struct cli_opt opt; bool value; }; #define CLI_OPT_FLAG(name, sh, ln, dsc) \ struct cli_opt_flag name = { \ .opt = \ { \ .type = CLI_OT_FLAG, \ .shor = sh, \ .lon = ln, \ .desc = dsc, \ .set = cli_opt_flag_set, \ }, \ } int cli_opt_flag_set(struct cli_opt *self, const char *) { struct cli_opt_flag *opt = container_of(self, struct cli_opt_flag, opt); opt->value = true; return CLI_RC_OK; } /** * struct cli_cmd - Represents a single command for the CLI. * @name: the command name as it should be read from argv. * @desc: a short description for the command. * @usage: what to print in the usage line on the command help output. * @help: long description to display on command help output. * @opts: the options that the command expects; requires a 0-filled sentinel; * NULL if no options are expected. The options will be filled in as * they are read from the arguments. cli.h will stop parsing once <--> * is encountered, or a value (non-option arg) without key is read. * The remaining args will be passed in cli_cmd_ctx. * @run: the function to execute when the command is invoked; the options * that were passed in this struct will be set according to what is * parsed from the command-line arguments. */ struct cli_cmd { const char *name; const char *desc; const char *usage; const char *help; struct cli_opt **opts; [[gnu::nonnull]] int (*run)(const struct cli_cmd *self, const struct cli_ctx *ctx); }; /** * struct cli - A CLI program. * @header: the header for the help; can be NULL * @footer: the footer for the help; can be NULL * @binary: name of the binary executed; if NULL, will take it from argv[0] * @cmds: array of commands; requires a 0-filled sentinel; can be NULL. * @opts: array of global options; also requires a sentinel; can be NULL. */ struct cli { const char *binary; const char *header; const char *footer; const struct cli_cmd *cmds; struct cli_opt **opts; }; [[gnu::nonnull]] static inline bool arg_is_help(const char *const arg) { return !strcmp(arg, "--help") || !strcmp(arg, "help") || !strcmp(arg, "-h"); } [[gnu::nonnull]] static void list_options(struct cli_opt **opts) { for (size_t i = 0; opts[i]; i++) { const struct cli_opt *opt = opts[i]; if (opt->lon) { if (opt->shor) { printf("\t--%s, -%c\t", opt->lon, opt->shor); } else { printf("\t--%s\t\t", opt->lon); } } else { printf("\t-%c\t\t", opt->shor); } printf("%s\n", opt->desc); } } [[gnu::nonnull]] static void explain_program(const struct cli *const cli) { if (cli->header) printf("%s\n", cli->header); printf("Usage: %s%s [command options]\n\n", cli->opts ? "[global options] " : "", cli->binary); if (cli->cmds) { puts("Commands:"); for (const struct cli_cmd *cmd = cli->cmds; cmd->name; cmd++) { printf("\t%s\t\t%s\n", cmd->name, cmd->desc); } } if (cli->opts) { if (cli->cmds) { puts("Global options:"); } else { puts("Options:"); } list_options(cli->opts); } if (cli->footer) printf("%s\n", cli->footer); } [[gnu::nonnull]] static void explain_command(const struct cli *const cli, const struct cli_cmd *cmd) { if (cli->header) printf("%s\n", cli->header); printf("Usage: %s%s %s %s\n\n", cli->opts ? "[global options] " : "", cli->binary, cmd->name, cmd->usage ?: "[options]"); printf("%s\n\n", cmd->help); if (cmd->opts) { printf("Options:\n"); list_options(cmd->opts); } if (cli->footer) printf("%s\n", cli->footer); } [[gnu::nonnull]] static int parse_long(struct cli_ctx *ctx) { char *key = ctx->argv[0] + 2; const char *value = NULL; for (size_t i = 0; i < strlen(key); i++) { if (key[i] == '=') { /* option of type '--key=Value' */ key[i] = '\0'; value = key + i + 1; } } struct cli_opt *opt = NULL; for (size_t i = 0; ctx->opts[i]; i++) { if (!strcmp(key, ctx->opts[i]->lon)) { opt = ctx->opts[i]; break; } } if (!opt) { fprintf(stderr, "bad long option '--%s'\n", key); return CLI_RC_BAD_ARGS; } if (!value && opt->type != CLI_OT_FLAG) { ctx->argc--; ctx->argv++; if (!ctx->argc) { fprintf(stderr, "option '--%s' requires a value\n", key); return CLI_RC_BAD_ARGS; } value = *ctx->argv; } int rc = opt->set(opt, value); if (rc == CLI_RC_BAD_ARGS) fprintf(stderr, "bad value '%s' for option '--%s'\n", value, key); ctx->argc--; ctx->argv++; return rc; } [[gnu::nonnull]] static int parse_short(struct cli_ctx *ctx) { for (const char *c = ctx->argv[0] + 1; *c != '\0'; c++) { struct cli_opt *opt = NULL; for (size_t i = 0; ctx->opts[i]; i++) { if (*c == ctx->opts[i]->shor) { opt = ctx->opts[i]; break; } } if (!opt) { fprintf(stderr, "bad short option '-%c'\n", *c); return CLI_RC_BAD_ARGS; } if (opt->type != CLI_OT_FLAG) { const char *value; if (c[1] != '\0') { /* option of type '-kValue' */ value = c + 1; } else { ctx->argc--; ctx->argv++; if (!ctx->argc) { fprintf(stderr, "option '-%c' requires a value\n", *c); return CLI_RC_BAD_ARGS; } value = *ctx->argv; } int rc = opt->set(opt, value); if (rc != CLI_RC_OK) { if (rc == CLI_RC_BAD_ARGS) fprintf( stderr, "bad value '%s' for option '-%c'\n", value, *c); return rc; } goto finish; } int rc = opt->set(opt, NULL); if (rc != CLI_RC_OK) return rc; /* we might have several flags strung together, e.g. '-abcd' */ } finish: ctx->argc--; ctx->argv++; return CLI_RC_OK; } [[gnu::nonnull]] static int parse_options(struct cli_ctx *ctx) { while (ctx->argc) { const char *key = ctx->argv[0]; if (key[0] != '-') /* * If it's a non-option arg that doesn't follow an option, we * consider this to be the same as after '--', i.e. everything from * here is a non-option arg. */ return CLI_RC_OK; if (key[1] == '-') { /* Everything after '--' are non-option arguments */ if (key[2] == '\0') { ctx->argc--; ctx->argv++; return CLI_RC_OK; } int rc = parse_long(ctx); if (rc != CLI_RC_OK) return rc; continue; } int rc = parse_short(ctx); if (rc != CLI_RC_OK) return rc; } return CLI_RC_OK; } [[gnu::nonnull]] static inline int cli_cmd_run(const struct cli *const cli, const struct cli_cmd *cmd, const struct cli_ctx *global_ctx) { struct cli_ctx cmd_ctx = { .opts = cmd->opts, .argc = global_ctx->argc, .argv = global_ctx->argv, }; int rc = CLI_RC_OK; if (global_ctx->argc > 0 && arg_is_help(global_ctx->argv[0])) goto help; rc = parse_options(&cmd_ctx); if (rc != CLI_RC_OK) { if (rc == CLI_RC_BAD_ARGS) goto help; goto out; } rc = cmd->run(cmd, &cmd_ctx); if (rc == CLI_RC_BAD_ARGS) goto help; goto out; help: explain_command(cli, cmd); out: return rc; } /** * cli_run() - Call this function to parse the CLI and execute any commands. * @cli: the CLI descriptor. * @argc: argc passed to your main() function. * @argv: argv passed to your main() function. * * Return: CLI_RC_OK on success, anything else on error. */ [[maybe_unused, gnu::nonnull]] static int cli_run(struct cli *const cli, int argc, char *argv[]) { cli->binary = cli->binary ?: argv[0]; if (argc < 2 || arg_is_help(argv[1])) { explain_program(cli); return CLI_RC_OK; } struct cli_ctx ctx = { .opts = cli->opts, .argc = argc - 1, .argv = argv + 1, }; int rc = parse_options(&ctx); if (rc != CLI_RC_OK || !cli->cmds) goto out; for (const struct cli_cmd *cmd = cli->cmds; cmd->name; cmd++) { if (!strcmp(cmd->name, ctx.argv[0])) { if (!cmd->run) { fprintf( stderr, "BUG: command `%s` has no function\n", cmd->name); return CLI_RC_BUG; } ctx.argc--; ctx.argv++; rc = cli_cmd_run(cli, cmd, &ctx); return rc; } } rc = CLI_RC_BAD_ARGS; out: if (rc == CLI_RC_BAD_ARGS) explain_program(cli); return rc; } #endif /* CLI_H */