feat: initial libcli implementation

This commit is contained in:
lohhiiccc 2026-03-29 01:10:29 +01:00
parent 05a69b48de
commit 08f50151d3
15 changed files with 564 additions and 0 deletions

116
example/main.c Normal file
View file

@ -0,0 +1,116 @@
#include <stdio.h>
#include <stdlib.h>
#include <getopt.h>
#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;
}

42
include/cli.h Normal file
View file

@ -0,0 +1,42 @@
#ifndef CLI_H
#define CLI_H
#include <stddef.h>
#include <getopt.h>
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

View file

@ -0,0 +1,9 @@
#ifndef CLI_PARSE_UTILS_H
#define CLI_PARSE_UTILS_H
#include <stdint.h>
int cli_parse_uint64(const char *s, uint64_t *out);
int cli_parse_float(const char *s, float *out);
#endif

16
include/compiler.h Normal file
View file

@ -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

166
src/parse.c Normal file
View file

@ -0,0 +1,166 @@
#include <stdint.h>
#include <stdio.h>
#include <getopt.h>
#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);
}

View file

View file

@ -0,0 +1,21 @@
#include <errno.h>
#include <stdlib.h>
#include <math.h>
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;
}

View file

@ -0,0 +1,21 @@
#include <errno.h>
#include <inttypes.h>
#include <stddef.h>
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;
}

View file

View file

@ -0,0 +1,68 @@
#include <criterion/criterion.h>
#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);
}

View file

@ -0,0 +1,68 @@
#include <criterion/criterion.h>
#include <stdint.h>
#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);
}

25
tests/test-suite.log Normal file
View file

@ -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

2
tests/test_cli.log Normal file
View file

@ -0,0 +1,2 @@
[====] Synthesis: Tested: 15 | Passing: 15 | Failing: 0 | Crashing: 0
PASS test_cli (exit status: 0)

4
tests/test_cli.trs Normal file
View file

@ -0,0 +1,4 @@
:test-result: PASS
:global-test-result: PASS
:recheck: no
:copy-in-global-log: no

6
tests/test_main.c Normal file
View file

@ -0,0 +1,6 @@
#include <criterion/criterion.h>
Test(dummy, always_pass)
{
cr_assert(1, "Hello, world!");
}