aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cli/Makefile14
-rw-r--r--cli/cli-example.c110
-rw-r--r--cli/cli-test.c524
-rw-r--r--cli/cli.h441
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;
+}