From 0a770cbaf0c9509415e6dabf880bf75d25a02606 Mon Sep 17 00:00:00 2001 From: Yaroslav de la Peña Smirnov Date: Sat, 13 Sep 2025 01:18:44 +0300 Subject: cli: header with extra option implementations For now just one, `struct cli_opt_data_size`. --- cli/cli-opts.h | 79 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ cli/cli-test.c | 33 +++++++++++++++++++++++- cli/cli.h | 23 ++++++++++++++--- 3 files changed, 131 insertions(+), 4 deletions(-) create mode 100644 cli/cli-opts.h diff --git a/cli/cli-opts.h b/cli/cli-opts.h new file mode 100644 index 0000000..4a94bcc --- /dev/null +++ b/cli/cli-opts.h @@ -0,0 +1,79 @@ +/* SPDX-License-Identifier: LGPL-2.1 */ +/** + * cli-opts.h - Extra cli_opt implementations for cli.h + * + * Copyright (c) 2025 - Yaroslav de la Peña Smirnov + */ +#ifndef CLI_OPTS_H +#define CLI_OPTS_H +#include "cli.h" +#include + +/** + * struct cli_opt_data_size - an option that represents data size in bytes. + * @opt: parent class. + * @bytes: the amount given by the user in bytes. + * + * This CLI option can parse units in bytes, KiB, MiB, GiB, TiB. By giving the + * number a suffix, it multiplies the number by the equivalent in bytes. E.g. + * `1K` == `1024`. + */ +struct cli_opt_data_size { + struct cli_opt opt; + uint64_t bytes; +}; + +#define CLI_OPT_DATA_SIZE(name, sh, ln, dsc) \ + struct cli_opt_data_size name = { \ + .opt = \ + { \ + .type = CLI_OT_VALUE, \ + .shor = sh, \ + .lon = ln, \ + .desc = dsc, \ + .set = cli_opt_data_size_set, \ + }, \ + } + +int cli_opt_data_size_set(struct cli_opt *self, const char *val) +{ + struct cli_opt_data_size *opt = + container_of(self, struct cli_opt_data_size, opt); + + char *endptr = NULL; + errno = 0; + unsigned long long num = strtoull(val, &endptr, 10); + if (errno != 0) + goto invalid; + + if (endptr && *endptr != '\0') { + switch (toupper(*endptr)) { + case 'K': + opt->bytes = num <<= 10; + break; + case 'M': + opt->bytes = num <<= 20; + break; + case 'G': + opt->bytes = num <<= 30; + break; + case 'T': + opt->bytes = num <<= 40; + break; + default: + goto invalid; + } + } else { + opt->bytes = num; + } + + return CLI_RC_OK; +invalid: + fprintf(stderr, + "invalid value \"%s\", expected a number optionally followed " + "by a multiplicative suffix (K, M, G, T).", + val); + return CLI_RC_ERR; +} + +#endif /* CLI_OPTS_H */ diff --git a/cli/cli-test.c b/cli/cli-test.c index c7a74e9..3a89f53 100644 --- a/cli/cli-test.c +++ b/cli/cli-test.c @@ -1,4 +1,5 @@ #include "cli.h" +#include "cli-opts.h" #include "../utest/utest.h" #include "../utils.h" @@ -48,6 +49,33 @@ static void reset_opts(void) copt.value = 0; } +TEST_BEGIN(test_cli_opt_data_size_set) +{ + CLI_OPT_DATA_SIZE(opt, 0, NULL, NULL); + struct tcase { + const char *input; + uint64_t expected_val; + enum cli_rc expected_rc; + } cases[] = { + {"420", 420, CLI_RC_OK}, + {"4K", 4ull << 10, CLI_RC_OK}, + {"8M", 8ull << 20, CLI_RC_OK}, + {"12G", 12ull << 30, CLI_RC_OK}, + {"1T", 1ull << 40, CLI_RC_OK}, + {"1E", 0, CLI_RC_ERR}, + {"6m", 6ull << 20, CLI_RC_OK}, + {"16k", 16ull << 10, CLI_RC_OK}, + }; + for (size_t i = 0; i < ARRAY_SIZE(cases); i++) { + opt.bytes = 0; + int rc = opt.opt.set(&opt.opt, cases[i].input); + asserteq(rc, cases[i].expected_rc); + asserteq(opt.bytes, cases[i].expected_val); + } + TEST_OUT +} +TEST_END + TEST_BEGIN(test_parse_long) { struct tcase { @@ -441,4 +469,7 @@ TEST_BEGIN(test_parse_options) } TEST_END -RUN_TESTS(test_parse_long, test_parse_short, test_parse_options) +RUN_TESTS(test_cli_opt_data_size_set, + test_parse_long, + test_parse_short, + test_parse_options) diff --git a/cli/cli.h b/cli/cli.h index 07f0ab4..72b89c5 100644 --- a/cli/cli.h +++ b/cli/cli.h @@ -8,6 +8,8 @@ * * Copyright (c) 2025 - Yaroslav de la Peña Smirnov */ +#ifndef CLI_H +#define CLI_H #include #include #include @@ -49,7 +51,7 @@ struct cli_ctx { }; /** - * enum cli_opt_type - The type of option. + * enum cli_opt_type - The type of option key. * @__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. @@ -61,17 +63,22 @@ enum cli_opt_type : uint8_t { }; /** - * struct cli_option - A flag or "option" for a command. + * struct cli_opt - A flag or "option" for a command; "abstract class". * @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. + * @set: setter method; parses and sets the actual value of the option. + * + * This is an abstract class. You can use the implementations below or make your + * own by embedding this struct in your own and setting the @set method. */ struct cli_opt { enum cli_opt_type type; char shor; const char *lon; const char *desc; + [[gnu::nonnull(1)]] int (*set)(struct cli_opt *self, const char *val); }; @@ -206,6 +213,7 @@ struct cli_cmd { const char *usage; const char *help; struct cli_opt **opts; + [[gnu::nonnull]] int (*run)(const struct cli_cmd *self, const struct cli_ctx *ctx); }; @@ -225,11 +233,13 @@ struct cli { struct cli_opt **opts; }; +[[gnu::nonnull]] static inline bool arg_is_help(const char *const arg) { return !strcmp(arg, "--help") || !strcmp(arg, "help") || !strcmp(arg, "-h"); } +[[gnu::nonnull]] static void list_options(struct cli_opt **opts) { for (size_t i = 0; opts[i]; i++) { @@ -248,6 +258,7 @@ static void list_options(struct cli_opt **opts) } } +[[gnu::nonnull]] static void explain_program(const struct cli *const cli) { if (cli->header) @@ -277,6 +288,7 @@ static void explain_program(const struct cli *const cli) printf("%s\n", cli->footer); } +[[gnu::nonnull]] static void explain_command(const struct cli *const cli, const struct cli_cmd *cmd) { @@ -299,6 +311,7 @@ static void explain_command(const struct cli *const cli, printf("%s\n", cli->footer); } +[[gnu::nonnull]] static int parse_long(struct cli_ctx *ctx) { char *key = ctx->argv[0] + 2; @@ -347,6 +360,7 @@ static int parse_long(struct cli_ctx *ctx) return rc; } +[[gnu::nonnull]] static int parse_short(struct cli_ctx *ctx) { for (const char *c = ctx->argv[0] + 1; *c != '\0'; c++) { @@ -403,6 +417,7 @@ finish: return CLI_RC_OK; } +[[gnu::nonnull]] static int parse_options(struct cli_ctx *ctx) { while (ctx->argc) { @@ -433,6 +448,7 @@ static int parse_options(struct cli_ctx *ctx) return CLI_RC_OK; } +[[gnu::nonnull]] static inline int cli_cmd_run(const struct cli *const cli, const struct cli_cmd *cmd, const struct cli_ctx *global_ctx) @@ -473,7 +489,7 @@ out: * * Return: CLI_RC_OK on success, anything else on error. */ -[[maybe_unused]] +[[maybe_unused, gnu::nonnull]] static int cli_run(struct cli *const cli, int argc, char *argv[]) { cli->binary = cli->binary ?: argv[0]; @@ -514,3 +530,4 @@ out: explain_program(cli); return rc; } +#endif /* CLI_H */ -- cgit v1.2.3