From 7cddd3e1ea648b7c478c00559a23cc10417bc5dd Mon Sep 17 00:00:00 2001 From: Yaroslav de la Peña Smirnov Date: Fri, 12 Sep 2025 12:33:18 +0300 Subject: cli.h: a simple command-line parser A simple command-line commands and options parser. --- cli/cli.h | 441 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 441 insertions(+) create mode 100644 cli/cli.h (limited to 'cli/cli.h') diff --git a/cli/cli.h b/cli/cli.h new file mode 100644 index 0000000..f5ce8f3 --- /dev/null +++ b/cli/cli.h @@ -0,0 +1,441 @@ +/* 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; +} -- cgit v1.2.3