diff options
author | Yaroslav de la Peña Smirnov <yps@yaroslavps.com> | 2025-09-12 12:33:18 +0300 |
---|---|---|
committer | Yaroslav de la Peña Smirnov <yps@yaroslavps.com> | 2025-09-12 12:33:18 +0300 |
commit | 7cddd3e1ea648b7c478c00559a23cc10417bc5dd (patch) | |
tree | a8db3f4c86f4fe36d84a469ce41e749e8d67755b | |
parent | 3620253a9463a9a256a2a031312d175fd23c73c1 (diff) | |
download | c-wares-7cddd3e1ea648b7c478c00559a23cc10417bc5dd.tar.gz c-wares-7cddd3e1ea648b7c478c00559a23cc10417bc5dd.zip |
cli.h: a simple command-line parser
A simple command-line commands and options parser.
-rw-r--r-- | cli/Makefile | 14 | ||||
-rw-r--r-- | cli/cli-example.c | 110 | ||||
-rw-r--r-- | cli/cli-test.c | 524 | ||||
-rw-r--r-- | cli/cli.h | 441 |
4 files changed, 1089 insertions, 0 deletions
diff --git a/cli/Makefile b/cli/Makefile new file mode 100644 index 0000000..1d10d3f --- /dev/null +++ b/cli/Makefile @@ -0,0 +1,14 @@ +CC?=gcc +CFLAGS+=-Wall -std=gnu23 -O0 -g + +cli-test: cli-test.c cli.h + $(CC) $(CFLAGS) -o cli-test cli-test.c + +cli-example: cli-example.c cli.h + $(CC) $(CFLAGS) -o cli-example cli-example.c + +example: cli-example + +test: cli-test + +.PHONY: test example diff --git a/cli/cli-example.c b/cli/cli-example.c new file mode 100644 index 0000000..8149be0 --- /dev/null +++ b/cli/cli-example.c @@ -0,0 +1,110 @@ +/** + * silly simple example program + */ +#include "cli.h" + +long distance; +unsigned long time; +long speed; + +bool murica; + +int calc_speed(const struct cli_ctx *ctx) +{ + if (time == 0) { + fprintf(stderr, "time cannot be zero\n"); + return CLI_RC_BAD_ARGS; + } + double speed = (double)distance / ((double)time / 3600); + printf("%lf%s\n", speed, murica ? "mph" : "km/h"); + + return CLI_RC_OK; +} + +int calc_distance(const struct cli_ctx *ctx) +{ + double distance = speed * ((double)time / 3600); + printf("%lf%s\n", distance, murica ? "miles" : "km"); + + return CLI_RC_OK; +} + +const struct cli_opt speed_opts[] = { + { + .type = CLI_OT_INT, + .shor = 'd', + .lon = "distance", + .desc = "the distance (km/miles)", + .value.i = &distance, + }, + { + .type = CLI_OT_UINT, + .shor = 't', + .lon = "time", + .desc = "the time (seconds)", + .value.u = &time, + }, + {0}, +}; + +const struct cli_opt distance_opts[] = { + { + .type = CLI_OT_INT, + .shor = 's', + .lon = "speed", + .desc = "the speed ((km/miles)ph)", + .value.i = &speed, + }, + { + .type = CLI_OT_UINT, + .shor = 't', + .lon = "time", + .desc = "the time (seconds)", + .value.u = &time, + }, + {0}, +}; + +const struct cli_cmd my_cmds[] = { + { + .name = "speed", + .desc = "calculate speed", + .help = + "Tell me the distance and the time, and I'll tell you your speed", + .opts = speed_opts, + .func = calc_speed, + }, + { + .name = "distance", + .desc = "calculate distance", + .help = + "Tell me the speed and the time, and I'll tell you your distance", + .opts = distance_opts, + .func = calc_distance, + }, + {0}, +}; + +struct cli_opt global_opts[] = { + { + .type = CLI_OT_FLAG, + .shor = 'M', + .lon = "murica", + .desc = "Use American™ measurements", + .value.f = &murica, + }, + {0}, +}; + +struct cli my_cli = { + .header = "This is an example CLI program", + .footer = "\nCopyright (c) 2025 Yaroslav de la Peña Smirnov " + "<yps@yaroslavps.com>", + .cmds = my_cmds, + .opts = global_opts, +}; + +int main(int argc, char *argv[]) +{ + return cli_run(&my_cli, argc, argv); +} diff --git a/cli/cli-test.c b/cli/cli-test.c new file mode 100644 index 0000000..5af572c --- /dev/null +++ b/cli/cli-test.c @@ -0,0 +1,524 @@ +#include "cli.h" +#include "../utest/utest.h" +#include "../utils.h" + +/** + * MUTSTR - hack to "force" string literals to be mutable. + * @literal: a C string literal. + */ +#define MUTSTR(literal) \ + ({ \ + static char _s[] = literal; \ + _s; \ + }) + +TEST_BEGIN(test_parse_long) +{ + struct vars { + long i; + unsigned long u; + const char *s; + bool f; + } vars; + struct cli_opt options[] = { + { + .type = CLI_OT_INT, + .lon = "signed", + .value.i = &vars.i, + }, + { + .type = CLI_OT_FLAG, + .lon = "flag", + .value.f = &vars.f, + }, + { + .type = CLI_OT_UINT, + .lon = "unsigned", + .value.u = &vars.u, + }, + { + .type = CLI_OT_STRING, + .lon = "string", + .value.s = &vars.s, + }, + {0}, + }; + struct tcase { + int argc; + char *argv[2]; + struct vars expected_vars; + enum cli_rc expected_rc; + } cases[] = { + { + .argc = 2, + .argv = + { + MUTSTR("--signed"), + MUTSTR("-2"), + }, + .expected_vars = + { + .i = -2, + .u = 0, + .s = NULL, + .f = 0, + }, + .expected_rc = CLI_RC_OK, + }, + { + .argc = 2, + .argv = + { + MUTSTR("--flag"), + MUTSTR("--unsigned=42"), + }, + .expected_vars = + { + .i = 0, + .u = 0, + .s = NULL, + .f = true, + }, + .expected_rc = CLI_RC_OK, + }, + { + .argc = 1, + .argv = + { + MUTSTR("--unsigned=42"), + }, + .expected_vars = + { + .i = 0, + .u = 42, + .s = NULL, + .f = false, + }, + .expected_rc = CLI_RC_OK, + }, + { + .argc = 1, + .argv = + { + MUTSTR("--signed=0xB00B"), + }, + .expected_vars = + { + .i = 0xB00B, + .u = 0, + .s = NULL, + .f = false, + }, + .expected_rc = CLI_RC_OK, + }, + { + .argc = 2, + .argv = + { + MUTSTR("--string"), + MUTSTR("wololo"), + }, + .expected_vars = + { + .i = 0, + .u = 0, + .s = "wololo", + .f = false, + }, + .expected_rc = CLI_RC_OK, + }, + { + .argc = 1, + .argv = + { + MUTSTR("--poop"), + }, + .expected_vars = {0}, + .expected_rc = CLI_RC_BAD_ARGS, + }, + }; + for (size_t i = 0; i < ARRAY_SIZE(cases); i++) { + vars = (struct vars){0}; + struct cli_ctx ctx = { + .opts = options, + .argc = cases[i].argc, + .argv = cases[i].argv, + }; + int rc = parse_long(&ctx); + asserteq(rc, cases[i].expected_rc); + asserteq(vars.i, cases[i].expected_vars.i); + asserteq(vars.u, cases[i].expected_vars.u); + if (cases[i].expected_vars.s) { + assertneq(vars.s, NULL); + asserteq(strcmp(vars.s, cases[i].expected_vars.s), 0); + } else { + asserteq(vars.s, NULL); + } + asserteq(vars.f, cases[i].expected_vars.f); + } + TEST_OUT +} +TEST_END + +TEST_BEGIN(test_parse_short) +{ + struct vars { + long i; + unsigned long u; + const char *s; + bool a; + bool b; + bool c; + } vars; + struct cli_opt options[] = { + { + .type = CLI_OT_INT, + .shor = 'i', + .value.i = &vars.i, + }, + { + .type = CLI_OT_FLAG, + .shor = 'a', + .value.f = &vars.a, + }, + { + .type = CLI_OT_FLAG, + .shor = 'b', + .value.f = &vars.b, + }, + { + .type = CLI_OT_FLAG, + .shor = 'c', + .value.f = &vars.c, + }, + { + .type = CLI_OT_UINT, + .shor = 'u', + .value.u = &vars.u, + }, + { + .type = CLI_OT_STRING, + .shor = 's', + .value.s = &vars.s, + }, + {0}, + }; + struct tcase { + int argc; + char *argv[2]; + struct vars expected_vars; + enum cli_rc expected_rc; + } cases[] = { + { + .argc = 2, + .argv = + { + MUTSTR("-i-2"), + MUTSTR("-b"), + }, + .expected_vars = + { + .i = -2, + .u = 0, + .s = NULL, + .a = false, + .b = false, + .c = false, + }, + .expected_rc = CLI_RC_OK, + }, + { + .argc = 1, + .argv = + { + MUTSTR("-cab"), + }, + .expected_vars = + { + .i = 0, + .u = 0, + .s = NULL, + .a = true, + .b = true, + .c = true, + }, + .expected_rc = CLI_RC_OK, + }, + { + .argc = 1, + .argv = + { + MUTSTR("-sab"), + }, + .expected_vars = + { + .i = 0, + .u = 0, + .s = "ab", + .a = false, + .b = false, + .c = false, + }, + .expected_rc = CLI_RC_OK, + }, + { + .argc = 2, + .argv = + { + MUTSTR("-abu"), + MUTSTR("24"), + }, + .expected_vars = + { + .i = 0, + .u = 24, + .s = NULL, + .a = true, + .b = true, + .c = false, + }, + .expected_rc = CLI_RC_OK, + }, + { + .argc = 2, + .argv = + { + MUTSTR("-ebu"), + MUTSTR("24"), + }, + .expected_vars = + { + .i = 0, + .u = 0, + .s = NULL, + .a = false, + .b = false, + .c = false, + }, + .expected_rc = CLI_RC_BAD_ARGS, + }, + }; + for (size_t i = 0; i < ARRAY_SIZE(cases); i++) { + vars = (struct vars){0}; + struct cli_ctx ctx = { + .opts = options, + .argc = cases[i].argc, + .argv = cases[i].argv, + }; + int rc = parse_short(&ctx); + asserteq(rc, cases[i].expected_rc); + asserteq(vars.i, cases[i].expected_vars.i); + asserteq(vars.u, cases[i].expected_vars.u); + if (cases[i].expected_vars.s) { + assertneq(vars.s, NULL); + asserteq(strcmp(vars.s, cases[i].expected_vars.s), 0); + } else { + asserteq(vars.s, NULL); + } + asserteq(vars.a, cases[i].expected_vars.a); + asserteq(vars.b, cases[i].expected_vars.b); + asserteq(vars.c, cases[i].expected_vars.c); + } + TEST_OUT +} +TEST_END + +TEST_BEGIN(test_parse_options) +{ + struct vars { + long i; + unsigned long u; + const char *s; + bool a; + bool b; + bool c; + } vars; + struct cli_opt options[] = { + { + .type = CLI_OT_INT, + .lon = "signed", + .shor = 's', + .value.i = &vars.i, + }, + { + .type = CLI_OT_FLAG, + .lon = "flag-a", + .shor = 'a', + .value.f = &vars.a, + }, + { + .type = CLI_OT_FLAG, + .lon = "flag-b", + .shor = 'b', + .value.f = &vars.b, + }, + { + .type = CLI_OT_FLAG, + .lon = "flag-c", + .shor = 'c', + .value.f = &vars.c, + }, + { + .type = CLI_OT_UINT, + .lon = "unsigned", + .shor = 'u', + .value.u = &vars.u, + }, + { + .type = CLI_OT_STRING, + .lon = "string", + .shor = 'S', + .value.s = &vars.s, + }, + {0}, + }; + struct tcase { + int argc; + char *argv[8]; + struct vars expected_vars; + enum cli_rc expected_rc; + } cases[] = { + { + .argc = 5, + .argv = + { + MUTSTR("-s69"), + MUTSTR("--unsigned"), + MUTSTR("0xBEEF"), + MUTSTR("-Shore"), + MUTSTR("-bac"), + }, + .expected_vars = + { + .i = 69, + .u = 0xBEEF, + .s = "hore", + .a = true, + .b = true, + .c = true, + }, + .expected_rc = CLI_RC_OK, + }, + { + .argc = 6, + .argv = + { + MUTSTR("-c"), + MUTSTR("--unsigned=0777"), + MUTSTR("--signed"), + MUTSTR("-96"), + MUTSTR("--string=bebop"), + MUTSTR("--flag-b"), + }, + .expected_vars = + { + .i = -96, + .u = 0777, + .s = "bebop", + .a = false, + .b = true, + .c = true, + }, + .expected_rc = CLI_RC_OK, + }, + { + .argc = 1, + .argv = + { + MUTSTR("-aStronomical"), + }, + .expected_vars = + { + .i = 0, + .u = 0, + .s = "tronomical", + .a = true, + .b = false, + .c = false, + }, + .expected_rc = CLI_RC_OK, + }, + { + .argc = 4, + .argv = + { + MUTSTR("-cab"), + MUTSTR("--"), + MUTSTR("--string"), + MUTSTR("hello"), + }, + .expected_vars = + { + .i = 0, + .u = 0, + .s = NULL, + .a = true, + .b = true, + .c = true, + }, + .expected_rc = CLI_RC_OK, + }, + { + .argc = 2, + .argv = + { + MUTSTR("-cab"), + MUTSTR("--string"), + }, + .expected_vars = + { + .i = 0, + .u = 0, + .s = NULL, + .a = true, + .b = true, + .c = true, + }, + .expected_rc = CLI_RC_BAD_ARGS, + }, + { + .argc = 3, + .argv = + { + MUTSTR("-s86"), + MUTSTR("--beef"), + MUTSTR("steak"), + }, + .expected_vars = + { + .i = 86, + .u = 0, + .s = NULL, + .a = false, + .b = false, + .c = false, + }, + .expected_rc = CLI_RC_BAD_ARGS, + }, + }; + for (size_t i = 0; i < ARRAY_SIZE(cases); i++) { + vars = (struct vars){0}; + struct cli_ctx ctx = { + .opts = options, + .argc = cases[i].argc, + .argv = cases[i].argv, + }; + int rc = parse_options(&ctx); + asserteq(rc, cases[i].expected_rc); + asserteq(vars.i, cases[i].expected_vars.i); + asserteq(vars.u, cases[i].expected_vars.u); + if (cases[i].expected_vars.s) { + assertneq(vars.s, NULL); + asserteq(strcmp(vars.s, cases[i].expected_vars.s), 0); + } else { + asserteq(vars.s, NULL); + } + asserteq(vars.a, cases[i].expected_vars.a); + asserteq(vars.b, cases[i].expected_vars.b); + asserteq(vars.c, cases[i].expected_vars.c); + } + TEST_OUT +} +TEST_END + +RUN_TESTS(test_parse_long, test_parse_short, test_parse_options) 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 <command> [--key value] [-k value] [--flag] [-f] [--] [non-option-arg]` + * + * Copyright (c) 2025 - Yaroslav de la Peña Smirnov + */ +#include <errno.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.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 { + 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> [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; +} |