aboutsummaryrefslogtreecommitdiff
path: root/cli/cli.h
diff options
context:
space:
mode:
Diffstat (limited to 'cli/cli.h')
-rw-r--r--cli/cli.h441
1 files changed, 441 insertions, 0 deletions
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;
+}