From 0a8ee7a194e598d9c277b019503f7119b32bd17f Mon Sep 17 00:00:00 2001 From: Yaroslav de la Peña Smirnov Date: Fri, 12 Sep 2025 23:43:49 +0300 Subject: cli: make it possible to extend option types Make it possible to extend options with custom value parsers by embeding `struct cli_opt` into a custom struct and setting the `set()` "method". --- cli/cli.h | 251 ++++++++++++++++++++++++++++++++++++++++---------------------- 1 file changed, 163 insertions(+), 88 deletions(-) (limited to 'cli/cli.h') diff --git a/cli/cli.h b/cli/cli.h index f5ce8f3..07f0ab4 100644 --- a/cli/cli.h +++ b/cli/cli.h @@ -13,6 +13,9 @@ #include #include #include +#include + +#include "../utils.h" /** * enum cli_rc - Return codes for CLI parsing results. @@ -40,24 +43,21 @@ enum cli_rc { * @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; + 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. + * @__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 { +enum cli_opt_type : uint8_t { __CLI_OT_NONE, - CLI_OT_INT, - CLI_OT_UINT, - CLI_OT_STRING, CLI_OT_FLAG, + CLI_OT_VALUE, }; /** @@ -66,27 +66,125 @@ enum cli_opt_type { * @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; + 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. @@ -98,17 +196,17 @@ struct cli_opt { * 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 + * @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; - const struct cli_opt *opts; - int (*func)(const struct cli_ctx *ctx); + const char *name; + const char *desc; + const char *usage; + const char *help; + struct cli_opt **opts; + int (*run)(const struct cli_cmd *self, const struct cli_ctx *ctx); }; /** @@ -124,7 +222,7 @@ struct cli { const char *header; const char *footer; const struct cli_cmd *cmds; - const struct cli_opt *opts; + struct cli_opt **opts; }; static inline bool arg_is_help(const char *const arg) @@ -132,9 +230,10 @@ 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) +static void list_options(struct cli_opt **opts) { - for (const struct cli_opt *opt = opts; opt->type; opt++) { + 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); @@ -155,7 +254,8 @@ static void explain_program(const struct cli *const cli) printf("%s\n", cli->header); printf("Usage: %s%s [command options]\n\n", - cli->opts ? "[global options] " : "", cli->binary); + cli->opts ? "[global options] " : "", + cli->binary); if (cli->cmds) { puts("Commands:"); @@ -183,8 +283,11 @@ static void explain_command(const struct cli *const cli, 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("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) { @@ -196,35 +299,6 @@ static void explain_command(const struct cli *const cli, 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; @@ -238,15 +312,15 @@ static int parse_long(struct cli_ctx *ctx) } } - const struct cli_opt *opt = ctx->opts; - if (opt) { - for (; opt->type; opt++) { - if (!strcmp(key, opt->lon)) - break; + 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 || !opt->type) { + if (!opt) { fprintf(stderr, "bad long option '--%s'\n", key); return CLI_RC_BAD_ARGS; } @@ -263,7 +337,7 @@ static int parse_long(struct cli_ctx *ctx) value = *ctx->argv; } - int rc = parse_value(opt, value); + int rc = opt->set(opt, value); if (rc == CLI_RC_BAD_ARGS) fprintf(stderr, "bad value '%s' for option '--%s'\n", value, key); @@ -276,15 +350,15 @@ static int parse_long(struct cli_ctx *ctx) 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; + 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 || !opt->type) { + if (!opt) { fprintf(stderr, "bad short option '-%c'\n", *c); return CLI_RC_BAD_ARGS; } @@ -307,18 +381,18 @@ static int parse_short(struct cli_ctx *ctx) value = *ctx->argv; } - int rc = parse_value(opt, value); + 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); + fprintf( + stderr, "bad value '%s' for option '-%c'\n", value, *c); return rc; } goto finish; } - int rc = parse_value(opt, NULL); + int rc = opt->set(opt, NULL); if (rc != CLI_RC_OK) return rc; /* we might have several flags strung together, e.g. '-abcd' */ @@ -380,7 +454,7 @@ static inline int cli_cmd_run(const struct cli *const cli, goto out; } - rc = cmd->func(&cmd_ctx); + rc = cmd->run(cmd, &cmd_ctx); if (rc == CLI_RC_BAD_ARGS) goto help; @@ -399,6 +473,7 @@ out: * * Return: CLI_RC_OK on success, anything else on error. */ +[[maybe_unused]] static int cli_run(struct cli *const cli, int argc, char *argv[]) { cli->binary = cli->binary ?: argv[0]; @@ -419,9 +494,9 @@ static int cli_run(struct cli *const cli, int argc, char *argv[]) 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); + if (!cmd->run) { + fprintf( + stderr, "BUG: command `%s` has no function\n", cmd->name); return CLI_RC_BUG; } -- cgit v1.2.3