/* 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 */ #include #include #include #include #include /** * 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 { const struct cli_opt *opts; int argc; char *const *argv; }; /** * enum cli_opt_type - The type of option. * @__CLI_OT_NONE: used as sentinel value; shouldn't be used directly. * @CLI_OT_INT: an integer; will be parsed as a C `long`. * @CLI_OT_STRING: a string; the pointer to the string will be passed as is. * @CLI_OT_FLAG: a flag; if the key was given, it wil be set; has no value. */ enum cli_opt_type { __CLI_OT_NONE, CLI_OT_INT, CLI_OT_UINT, CLI_OT_STRING, CLI_OT_FLAG, }; /** * struct cli_option - A flag or "option" for a command. * @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. * @value: pointer to variable to set: * - STRING: @value.s, will be set to point to the argv with the * value. * - INT: @value.i, will be set to the parsed value of the argv. * - UINT: @value.u, same as INT but unsigned * - FLAG: @value.f, will be set to true upon encountering the * option. */ struct cli_opt { enum cli_opt_type type; char shor; const char *lon; const char *desc; union { long *i; unsigned long *u; const char **s; bool *f; } value; }; /** * 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. * @func: the function to execute when the command is invoked; the options * that were passed in this struct will be filled 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; const struct cli_opt *opts; int (*func)(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; const struct cli_opt *opts; }; static inline bool arg_is_help(const char *const arg) { return !strcmp(arg, "--help") || !strcmp(arg, "help") || !strcmp(arg, "-h"); } static void list_options(const struct cli_opt *opts) { for (const struct cli_opt *opt = opts; opt->type; opt++) { 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); } } 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); } 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); } static int parse_value(const struct cli_opt *opt, const char *value) { switch (opt->type) { case CLI_OT_INT: errno = 0; *opt->value.i = strtol(value, NULL, 0); if (errno != 0) return CLI_RC_BAD_ARGS; break; case CLI_OT_UINT: errno = 0; *opt->value.u = strtoul(value, NULL, 0); if (errno != 0) return CLI_RC_BAD_ARGS; break; case CLI_OT_STRING: *opt->value.s = value; break; case CLI_OT_FLAG: *opt->value.f = true; break; default: fprintf(stderr, "BUG: incorrect option type %d\n", opt->type); return CLI_RC_BUG; } return CLI_RC_OK; } 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; } } const struct cli_opt *opt = ctx->opts; if (opt) { for (; opt->type; opt++) { if (!strcmp(key, opt->lon)) break; } } if (!opt || !opt->type) { 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 = parse_value(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; } static int parse_short(struct cli_ctx *ctx) { for (const char *c = ctx->argv[0] + 1; *c != '\0'; c++) { const struct cli_opt *opt = ctx->opts; if (opt) { for (; opt->type; opt++) { if (*c == opt->shor) break; } } if (!opt || !opt->type) { 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 = parse_value(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 = parse_value(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; } 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') 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; } 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->func(&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. */ 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->func) { 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); goto out; } } rc = CLI_RC_BAD_ARGS; out: if (rc == CLI_RC_BAD_ARGS) explain_program(cli); return rc; }