diff --git a/example/main.c b/example/main.c new file mode 100644 index 0000000..ae8a6c8 --- /dev/null +++ b/example/main.c @@ -0,0 +1,116 @@ +#include +#include +#include + +#include "cli.h" +#include "cli_parse_utils.h" +#include "compiler.h" + +/* Config */ + +#define FLAG_VERBOSE (1 << 0) + +struct greet_config { + const char *name; + uint64_t count; + uint8_t flags; +}; + +/* Handlers */ + +static int +handle_help(__unused const char *arg, __unused void *cfg) +{ + printf("Usage: greet [OPTIONS] ...\n\n"); + printf(" -h, --help Display this help\n"); + printf(" -V, --version Display version\n"); + printf(" -v, --verbose Verbose output\n"); + printf(" -n, --name NAME Name to greet (default: world)\n"); + printf(" -c, --count N Number of greetings (default: 1)\n"); + return CLI_EXIT_SUCCESS; +} + +static int +handle_version(__unused const char *arg, __unused void *cfg) +{ + printf("greet 0.1.0 (libcli example)\n"); + return CLI_EXIT_SUCCESS; +} + +static int +handle_verbose(__unused const char *arg, void *cfg) +{ + struct greet_config *config = (struct greet_config *)cfg; + SET_FLAG(config->flags, FLAG_VERBOSE); + return CLI_SUCCESS; +} + +static int +handle_name(const char *arg, void *cfg) +{ + struct greet_config *config = (struct greet_config *)cfg; + config->name = arg; + return CLI_SUCCESS; +} + +static int +handle_count(const char *arg, void *cfg) +{ + struct greet_config *config = (struct greet_config *)cfg; + if (0 != cli_parse_uint64(arg, &config->count)) + return CLI_ERROR; + return CLI_SUCCESS; +} + +/* Option table */ + +#define GREET_OPTIONS_LIST \ + X('h', "help", no_argument, handle_help, OPT_ARG_NONE, "Display this help") \ + X('V', "version", no_argument, handle_version, OPT_ARG_NONE, "Display version") \ + X('v', "verbose", no_argument, handle_verbose, OPT_ARG_NONE, "Verbose output") \ + X('n', "name", required_argument, handle_name, OPT_ARG_STRING, "Name to greet") \ + X('c', "count", required_argument, handle_count, OPT_ARG_UINT, "Number of greetings") + +#define X(s, l, a, h, t, d) + 1 +enum { GREET_NB_OPTS = 0 GREET_OPTIONS_LIST }; +#undef X + +#define X(s, l, a, h, t, d) + (1 + (a != no_argument ? 1 : 0)) +enum { GREET_OPTSTR_LEN = 1 GREET_OPTIONS_LIST }; +#undef X + +#define X(s, l, a, h, t, d) { s, l, a, h, t, d }, +static const struct option_descriptor g_opts[] = { GREET_OPTIONS_LIST }; +#undef X + +/* Main */ + +int +main(int argc, char **argv) +{ + struct greet_config config = { .name = "world", .count = 1 }; + char opt_str[GREET_OPTSTR_LEN + 1]; + struct option long_opts[GREET_NB_OPTS + 1]; + enum cli_code ret; + uint64_t i; + + cli_set_prog_name(argv[0]); + + ret = cli_parse(argc, argv, &config, g_opts, GREET_NB_OPTS, + opt_str, long_opts); + + if (CLI_EXIT_SUCCESS == ret) + return EXIT_SUCCESS; + if (CLI_ERROR == ret) + return EXIT_FAILURE; + + for (i = 0; i < config.count; i++) + { + if (HAS_FLAG(config.flags, FLAG_VERBOSE)) + printf("[%llu] Hello, %s!\n", (unsigned long long)(i + 1), + config.name); + else + printf("Hello, %s!\n", config.name); + } + return EXIT_SUCCESS; +} diff --git a/include/cli.h b/include/cli.h new file mode 100644 index 0000000..c85318a --- /dev/null +++ b/include/cli.h @@ -0,0 +1,42 @@ +#ifndef CLI_H +#define CLI_H + +#include +#include + +enum cli_code { + CLI_EXIT_SUCCESS = -1, + CLI_SUCCESS = 0, + CLI_ERROR = 1 +}; + +typedef int (*t_option_handler)(const char *arg, void *config); + +enum option_arg_type +{ + OPT_ARG_NONE = 0, + OPT_ARG_INT, + OPT_ARG_UINT, + OPT_ARG_SECONDS, + OPT_ARG_BYTES, + OPT_ARG_TTL, + OPT_ARG_STRING +}; + +struct option_descriptor +{ + char short_opt; + const char *long_opt; + int has_arg; + t_option_handler handler; + enum option_arg_type arg_type; + const char *description; +}; + +void cli_set_prog_name(const char *name); + +enum cli_code cli_parse(int argc, char **argv, void *config, + const struct option_descriptor *opts, size_t nb_opts, + char *opt_str, struct option *long_opts); + +#endif diff --git a/include/cli_parse_utils.h b/include/cli_parse_utils.h new file mode 100644 index 0000000..691f259 --- /dev/null +++ b/include/cli_parse_utils.h @@ -0,0 +1,9 @@ +#ifndef CLI_PARSE_UTILS_H +#define CLI_PARSE_UTILS_H + +#include + +int cli_parse_uint64(const char *s, uint64_t *out); +int cli_parse_float(const char *s, float *out); + +#endif diff --git a/include/compiler.h b/include/compiler.h new file mode 100644 index 0000000..17f557a --- /dev/null +++ b/include/compiler.h @@ -0,0 +1,16 @@ +#ifndef COMPILER_H +#define COMPILER_H + +#define __unused __attribute__((unused)) + +#define COUNT_OF(arr) (sizeof(arr) / sizeof((arr)[0])) + +#define STATIC_ARRAY_FOREACH(arr, ptr) \ + for ((ptr) = (arr); (ptr) < (arr) + COUNT_OF(arr); (ptr)++) + +#define HAS_FLAG(flags, flag) ((flags) & (flag)) +#define SET_FLAG(flags, flag) ((flags) |= (flag)) +#define CLEAR_FLAG(flags, flag) ((flags) &= ~(flag)) +#define TOGGLE_FLAG(flags, flag) ((flags) ^= (flag)) + +#endif diff --git a/src/parse.c b/src/parse.c new file mode 100644 index 0000000..2b89227 --- /dev/null +++ b/src/parse.c @@ -0,0 +1,166 @@ +#include +#include +#include + +#include "cli.h" + +/* Internal bitmask helpers */ +#define HAS_FLAG(flags, flag) ((flags) & (flag)) +#define SET_FLAG(flags, flag) ((flags) |= (flag)) + +/* Map -? to -h to support help shortcut */ +#define HANDLE_QUESTION_MARK(opt) \ + do { \ + if (opt == '?' && (optopt) == '?') \ + opt = 'h'; \ + } while (0) + +/* Forward declarations */ +static void build_long_options(struct option *long_opts, size_t nb_opts, + const struct option_descriptor *src); +static void build_optstr(char *dest, const struct option_descriptor *opts, + size_t nb_opts); +static const struct option_descriptor *find_option_handler(int opt, + const struct option_descriptor *opts, size_t nb_opts); +static int handle_one_option(int opt, char **argv, void *config, + uint64_t *opt_tracker, + const struct option_descriptor *opts, size_t nb_opts); +static enum cli_code error_unknown_opt(const char *current_opt); +static enum cli_code error_invalid_opt(const char *current_opt); +static enum cli_code error_duplicate_opt(const char *current_opt); +static void print_suggest_help(void); +/* ------------------- */ + +static const char *s_prog_name = ""; + +void +cli_set_prog_name(const char *name) +{ + if (NULL != name) + s_prog_name = name; +} + +enum cli_code +cli_parse(int argc, char **argv, void *config, + const struct option_descriptor *opts, size_t nb_opts, + char *opt_str, struct option *long_opts) +{ + uint64_t tracker = 0; + int opt; + int res; + + build_optstr(opt_str, opts, nb_opts); + build_long_options(long_opts, nb_opts, opts); + while (-1 != (opt = getopt_long(argc, argv, opt_str, long_opts, NULL))) + { + HANDLE_QUESTION_MARK(opt); + res = handle_one_option(opt, argv, config, &tracker, opts, nb_opts); + if (CLI_SUCCESS != res) + return (enum cli_code)res; + } + return CLI_SUCCESS; +} + +static void +build_optstr(char *dest, const struct option_descriptor *opts, size_t nb_opts) +{ + size_t str_i = 1; + + /* Mute default error messages */ + dest[0] = ':'; + + for (size_t opt_i = 0; opt_i < nb_opts; ++opt_i) + { + dest[str_i++] = opts[opt_i].short_opt; + switch (opts[opt_i].has_arg) + { + case optional_argument: dest[str_i++] = ':'; /* fall through */ + case required_argument: dest[str_i++] = ':'; break; + default: break; + } + } + dest[str_i] = '\0'; +} + +static int +handle_one_option(int opt, char **argv, void *config, + uint64_t *opt_tracker, + const struct option_descriptor *opts, size_t nb_opts) +{ + const char *current_opt = argv[optind - 1]; + const struct option_descriptor *desc; + size_t bitmask_index; + int res; + + if ('?' == opt) + return error_unknown_opt(current_opt); + else if (':' == opt) + return error_invalid_opt(current_opt); + + desc = find_option_handler(opt, opts, nb_opts); + bitmask_index = (size_t)(desc - opts); + if (HAS_FLAG(*opt_tracker, (1ULL << bitmask_index))) + return error_duplicate_opt(current_opt); + + SET_FLAG(*opt_tracker, (1ULL << bitmask_index)); + res = desc->handler(optarg, config); + if (CLI_ERROR == res) + return error_invalid_opt(current_opt); + return res; +} + +static void +build_long_options(struct option *long_opts, size_t nb_opts, + const struct option_descriptor *src) +{ + for (size_t i = 0; i < nb_opts; ++i) + { + long_opts[i].name = src[i].long_opt; + long_opts[i].has_arg = src[i].has_arg; + long_opts[i].flag = NULL; + long_opts[i].val = src[i].short_opt; + } + long_opts[nb_opts] = (struct option){0}; +} + +static const struct option_descriptor * +find_option_handler(int opt, const struct option_descriptor *opts, + size_t nb_opts) +{ + for (size_t i = 0; i < nb_opts; ++i) + { + if (opts[i].short_opt == opt) + return &opts[i]; + } + return NULL; +} + +static enum cli_code +error_unknown_opt(const char *current_opt) +{ + fprintf(stderr, "%s: unknown option -- '%s'\n", s_prog_name, current_opt); + print_suggest_help(); + return CLI_ERROR; +} + +static enum cli_code +error_invalid_opt(const char *current_opt) +{ + fprintf(stderr, "%s: invalid option '%s'\n", s_prog_name, current_opt); + print_suggest_help(); + return CLI_ERROR; +} + +static enum cli_code +error_duplicate_opt(const char *current_opt) +{ + fprintf(stderr, "%s: duplicate option '%s'\n", s_prog_name, current_opt); + print_suggest_help(); + return CLI_ERROR; +} + +static void +print_suggest_help(void) +{ + fprintf(stderr, "Try '%s --help' for more information.\n", s_prog_name); +} diff --git a/src/parse_utils/.dirstamp b/src/parse_utils/.dirstamp new file mode 100644 index 0000000..e69de29 diff --git a/src/parse_utils/parse_float.c b/src/parse_utils/parse_float.c new file mode 100644 index 0000000..85de590 --- /dev/null +++ b/src/parse_utils/parse_float.c @@ -0,0 +1,21 @@ +#include +#include +#include + +int +cli_parse_float(const char *s, float *out) +{ + char *end; + + if (NULL == s || '\0' == *s || '-' == *s) + return 1; + + errno = 0; + float v = strtof(s, &end); + + if (s == end || ERANGE == errno || '\0' != *end || 0 == isfinite(v)) + return 1; + + *out = v; + return 0; +} diff --git a/src/parse_utils/parse_int.c b/src/parse_utils/parse_int.c new file mode 100644 index 0000000..9a3cdeb --- /dev/null +++ b/src/parse_utils/parse_int.c @@ -0,0 +1,21 @@ +#include +#include +#include + +int +cli_parse_uint64(const char *s, uint64_t *out) +{ + char *end; + + if ( NULL == s || '\0' == *s || '-' == *s) + return 1; + + errno = 0; + const uintmax_t v = (uintmax_t)strtoumax(s, &end, 10); + + if (s == end || ERANGE == errno || '\0' != *end || v > UINT64_MAX) + return 1; + + *out = (uint64_t)v; + return 0; +} diff --git a/tests/parse_utils/.dirstamp b/tests/parse_utils/.dirstamp new file mode 100644 index 0000000..e69de29 diff --git a/tests/parse_utils/test_parse_float.c b/tests/parse_utils/test_parse_float.c new file mode 100644 index 0000000..72c75b7 --- /dev/null +++ b/tests/parse_utils/test_parse_float.c @@ -0,0 +1,68 @@ +#include +#include "cli_parse_utils.h" + +/* Test 1: Valid simple number */ +Test(parse_float, valid_number) +{ + float result; + int ret = cli_parse_float("42.42", &result); + + cr_assert_eq(ret, 0); + cr_assert_float_eq(result, 42.42, 0.0001); +} + +/* Test 2: Zero */ +Test(parse_float, zero) +{ + float result; + int ret = cli_parse_float("0", &result); + + cr_assert_eq(ret, 0); + cr_assert_float_eq(result, 0, 0.0001); +} + +/* Test 3: NULL pointer */ +Test(parse_float, null_pointer) +{ + float result; + int ret = cli_parse_float(NULL, &result); + + cr_assert_eq(ret, 1); +} + +/* Test 4: Empty string */ +Test(parse_float, empty_string) +{ + float result; + int ret = cli_parse_float("", &result); + + cr_assert_eq(ret, 1); +} + +/* Test 5: Negative number */ +Test(parse_float, negative_number) +{ + float result; + int ret = cli_parse_float("-42", &result); + + cr_assert_eq(ret, 1); +} + +/* Test 6: Invalid characters */ +Test(parse_float, invalid_characters) +{ + float result; + int ret = cli_parse_float("42abc", &result); + + cr_assert_eq(ret, 1); +} + +/* Test 7: Dot + number */ +Test(parse_float, dot) +{ + float result; + int ret = cli_parse_float(".42", &result); + + cr_assert_eq(ret, 0); + cr_assert_float_eq(result, 0.42, 0.0001); +} diff --git a/tests/parse_utils/test_parse_int.c b/tests/parse_utils/test_parse_int.c new file mode 100644 index 0000000..dadffb5 --- /dev/null +++ b/tests/parse_utils/test_parse_int.c @@ -0,0 +1,68 @@ +#include +#include +#include "cli_parse_utils.h" + +/* Test 1: Valid simple number */ +Test(parse_uint64, valid_number) +{ + uint64_t result; + int ret = cli_parse_uint64("42", &result); + + cr_assert_eq(ret, 0); + cr_assert_eq(result, 42); +} + +/* Test 2: Zero */ +Test(parse_uint64, zero) +{ + uint64_t result; + int ret = cli_parse_uint64("0", &result); + + cr_assert_eq(ret, 0); + cr_assert_eq(result, 0); +} + +/* Test 3: NULL pointer */ +Test(parse_uint64, null_pointer) +{ + uint64_t result; + int ret = cli_parse_uint64(NULL, &result); + + cr_assert_eq(ret, 1); +} + +/* Test 4: Empty string */ +Test(parse_uint64, empty_string) +{ + uint64_t result; + int ret = cli_parse_uint64("", &result); + + cr_assert_eq(ret, 1); +} + +/* Test 5: Negative number */ +Test(parse_uint64, negative_number) +{ + uint64_t result; + int ret = cli_parse_uint64("-42", &result); + + cr_assert_eq(ret, 1); +} + +/* Test 6: Invalid characters */ +Test(parse_uint64, invalid_characters) +{ + uint64_t result; + int ret = cli_parse_uint64("42abc", &result); + + cr_assert_eq(ret, 1); +} + +/* Test 7: Overflow */ +Test(parse_uint64, overflow) +{ + uint64_t result; + int ret = cli_parse_uint64("18446744073709551616", &result); + + cr_assert_eq(ret, 1); +} diff --git a/tests/test-suite.log b/tests/test-suite.log new file mode 100644 index 0000000..9c25473 --- /dev/null +++ b/tests/test-suite.log @@ -0,0 +1,25 @@ +======================================== + libcli 0.1.0: tests/test-suite.log +======================================== + +# TOTAL: 1 +# PASS: 1 +# SKIP: 0 +# XFAIL: 0 +# FAIL: 0 +# XPASS: 0 +# ERROR: 0 + +System information (uname -a): Linux 6.12.58-gentoo-v2 #3 SMP PREEMPT_DYNAMIC Wed Jan 14 19:57:01 CET 2026 x86_64 AMD Ryzen 7 2700X Eight-Core Processor AuthenticAMD +Distribution information (/etc/os-release): +NAME=Gentoo +ID=gentoo +PRETTY_NAME="Gentoo Linux" +ANSI_COLOR="1;32" +HOME_URL="https://www.gentoo.org/" +SUPPORT_URL="https://www.gentoo.org/support/" +BUG_REPORT_URL="https://bugs.gentoo.org/" +VERSION_ID="2.18" + +.. contents:: :depth: 2 + diff --git a/tests/test_cli.log b/tests/test_cli.log new file mode 100644 index 0000000..d0ea1d4 --- /dev/null +++ b/tests/test_cli.log @@ -0,0 +1,2 @@ +[====] Synthesis: Tested: 15 | Passing: 15 | Failing: 0 | Crashing: 0 +PASS test_cli (exit status: 0) diff --git a/tests/test_cli.trs b/tests/test_cli.trs new file mode 100644 index 0000000..3f0f396 --- /dev/null +++ b/tests/test_cli.trs @@ -0,0 +1,4 @@ +:test-result: PASS +:global-test-result: PASS +:recheck: no +:copy-in-global-log: no diff --git a/tests/test_main.c b/tests/test_main.c new file mode 100644 index 0000000..a68cf99 --- /dev/null +++ b/tests/test_main.c @@ -0,0 +1,6 @@ +#include + +Test(dummy, always_pass) +{ + cr_assert(1, "Hello, world!"); +}