Compare commits
9 commits
40d27b6a9f
...
8e46db9d2e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e46db9d2e | ||
|
|
a1bb371d3a | ||
|
|
960ed4584f | ||
|
|
f91ed23cb0 | ||
|
|
a7dd6bde3b | ||
|
|
3336ac272c | ||
|
|
4589912088 | ||
|
|
08f50151d3 | ||
|
|
05a69b48de |
21 changed files with 921 additions and 0 deletions
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -8,6 +8,11 @@
|
|||
*.o
|
||||
*.d
|
||||
*.lo
|
||||
.dirstamp
|
||||
|
||||
# Automake test results
|
||||
*.log
|
||||
*.trs
|
||||
|
||||
# Libtool
|
||||
*.la
|
||||
|
|
|
|||
13
Makefile.am
Normal file
13
Makefile.am
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
ACLOCAL_AMFLAGS = -I m4
|
||||
|
||||
SUBDIRS = src example
|
||||
|
||||
if BUILD_TESTS
|
||||
SUBDIRS += tests
|
||||
endif
|
||||
|
||||
pkgincludedir = $(includedir)/cli
|
||||
pkginclude_HEADERS = \
|
||||
include/cli.h \
|
||||
include/cli_parse_utils.h \
|
||||
include/compiler.h
|
||||
192
README.md
Normal file
192
README.md
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
# libcli
|
||||
|
||||
A lightweight C library for parsing command-line arguments. Wraps `getopt_long`
|
||||
with a descriptor-based API, typed parse utilities, duplicate detection, and
|
||||
automatic help formatting.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- GCC (uses `__attribute__((unused))`)
|
||||
- autoconf ≥ 2.69, automake, libtool
|
||||
- [Criterion](https://github.com/Snaipe/Criterion) — optional, required for
|
||||
`--enable-tests`
|
||||
|
||||
## Build
|
||||
|
||||
```sh
|
||||
./autogen.sh
|
||||
./configure
|
||||
make
|
||||
make install
|
||||
```
|
||||
|
||||
To build and run the test suite:
|
||||
|
||||
```sh
|
||||
./configure --enable-tests
|
||||
make check
|
||||
```
|
||||
|
||||
## Quickstart
|
||||
|
||||
```c
|
||||
#include "cli.h"
|
||||
#include "cli_parse_utils.h"
|
||||
#include "compiler.h"
|
||||
|
||||
struct config { const char *name; };
|
||||
|
||||
static int handle_name(const char *arg, void *cfg) {
|
||||
((struct config *)cfg)->name = arg;
|
||||
return CLI_SUCCESS;
|
||||
}
|
||||
|
||||
static int handle_help(const char *arg, void *cfg) {
|
||||
printf("Usage: myprog [OPTIONS]\n\n");
|
||||
cli_print_options(g_opts, NB_OPTS, cli_arg_type_to_str);
|
||||
return CLI_EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
#define OPTIONS_LIST \
|
||||
X('h', "help", no_argument, handle_help, OPT_ARG_NONE, "Display this help") \
|
||||
X('n', "name", required_argument, handle_name, OPT_ARG_STRING, "Your name")
|
||||
|
||||
#define X(s, l, a, h, t, d) + 1
|
||||
enum { NB_OPTS = 0 OPTIONS_LIST };
|
||||
#undef X
|
||||
|
||||
#define X(s, l, a, h, t, d) + (1 + (a != no_argument ? 1 : 0))
|
||||
enum { OPTSTR_LEN = 1 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[] = { OPTIONS_LIST };
|
||||
#undef X
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
struct config cfg = { .name = "world" };
|
||||
char opt_str[OPTSTR_LEN + 1];
|
||||
struct option long_opts[NB_OPTS + 1];
|
||||
|
||||
cli_set_prog_name(argv[0]);
|
||||
if (CLI_SUCCESS != cli_parse(argc, argv, &cfg, g_opts, NB_OPTS, opt_str, long_opts))
|
||||
return 1;
|
||||
|
||||
printf("Hello, %s!\n", cfg.name);
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
See `example/` for a complete working example.
|
||||
|
||||
## API
|
||||
|
||||
### Parsing
|
||||
|
||||
```c
|
||||
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);
|
||||
```
|
||||
|
||||
Parses `argv` against the descriptor table `opts`. Calls each option's handler
|
||||
with the raw argument string and `config`. Returns `CLI_SUCCESS`, `CLI_ERROR`,
|
||||
or `CLI_EXIT_SUCCESS` (handler requested early exit, e.g. `--help`).
|
||||
|
||||
`opt_str` and `long_opts` are caller-allocated scratch buffers — size them with
|
||||
the X-macro pattern shown above.
|
||||
|
||||
```c
|
||||
void cli_set_prog_name(const char *name);
|
||||
```
|
||||
|
||||
Sets the program name used in error messages. Call with `argv[0]` before `cli_parse`.
|
||||
|
||||
### Option descriptor
|
||||
|
||||
```c
|
||||
struct option_descriptor {
|
||||
char short_opt; // e.g. 'h'
|
||||
const char *long_opt; // e.g. "help"
|
||||
int has_arg; // no_argument / required_argument / optional_argument
|
||||
t_option_handler handler; // callback: int f(const char *arg, void *config)
|
||||
int arg_type; // display hint for cli_print_options
|
||||
const char *description; // displayed by cli_print_options
|
||||
};
|
||||
```
|
||||
|
||||
### Help
|
||||
|
||||
```c
|
||||
void cli_print_options(const struct option_descriptor *opts, size_t nb_opts,
|
||||
t_arg_type_to_str to_str);
|
||||
```
|
||||
|
||||
Prints a formatted options table to stdout. `to_str` maps `arg_type` integers
|
||||
to placeholder strings (`"<NUM>"`, `"<STR>"`, etc.).
|
||||
|
||||
```c
|
||||
const char *cli_arg_type_to_str(int type);
|
||||
```
|
||||
|
||||
Default `to_str` implementation covering the built-in types. Pass directly to
|
||||
`cli_print_options` if you don't need custom types.
|
||||
|
||||
### Return codes
|
||||
|
||||
| Value | Meaning |
|
||||
|---|---|
|
||||
| `CLI_SUCCESS` | Continue — option handled successfully |
|
||||
| `CLI_ERROR` | Abort — invalid option or argument |
|
||||
| `CLI_EXIT_SUCCESS` | Abort — clean exit requested (e.g. after printing help) |
|
||||
|
||||
### Parse utilities
|
||||
|
||||
```c
|
||||
int cli_parse_uint64(const char *s, uint64_t *out); // non-negative integers
|
||||
int cli_parse_int64(const char *s, int64_t *out); // signed integers (use --opt=-N syntax)
|
||||
int cli_parse_float(const char *s, float *out); // floats (use --opt=-1.5 syntax)
|
||||
```
|
||||
|
||||
All return 0 on success, 1 on failure. They reject NULL, empty strings,
|
||||
trailing characters, and out-of-range values.
|
||||
|
||||
## Custom arg types
|
||||
|
||||
The built-in `CLI_OPT_ARG_TYPES` X-macro covers `OPT_ARG_NONE`, `OPT_ARG_INT`,
|
||||
`OPT_ARG_UINT`, `OPT_ARG_FLOAT`, `OPT_ARG_STRING`. To define custom types with
|
||||
their own display strings, declare your own list and generate both the enum and
|
||||
the `to_str` function from it:
|
||||
|
||||
```c
|
||||
#define MY_ARG_TYPES \
|
||||
X(OPT_ARG_NONE, "") \
|
||||
X(OPT_ARG_UINT, "<NUM>") \
|
||||
X(OPT_ARG_SECONDS, "<SEC>") \
|
||||
X(OPT_ARG_STRING, "<STR>")
|
||||
|
||||
#define X(name, str) name,
|
||||
enum { MY_ARG_TYPES };
|
||||
#undef X
|
||||
|
||||
static const char *my_arg_type_to_str(int type) {
|
||||
static const char *strs[] = {
|
||||
#define X(name, str) str,
|
||||
MY_ARG_TYPES
|
||||
#undef X
|
||||
};
|
||||
if ((size_t)type < COUNT_OF(strs))
|
||||
return strs[type];
|
||||
return "<ARG>";
|
||||
}
|
||||
```
|
||||
|
||||
Then pass `my_arg_type_to_str` to `cli_print_options`.
|
||||
|
||||
## Limitations
|
||||
|
||||
- Maximum 64 options
|
||||
|
||||
## License
|
||||
|
||||
GPLv3 — see [LICENSE](LICENSE).
|
||||
3
autogen.sh
Executable file
3
autogen.sh
Executable file
|
|
@ -0,0 +1,3 @@
|
|||
#!/bin/sh
|
||||
set -e
|
||||
autoreconf --install --verbose
|
||||
47
configure.ac
Normal file
47
configure.ac
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
AC_PREREQ([2.69])
|
||||
AC_INIT([libcli], [0.1.0], [])
|
||||
AC_CONFIG_AUX_DIR([build-aux])
|
||||
AC_CONFIG_MACRO_DIRS([m4])
|
||||
AM_INIT_AUTOMAKE([foreign -Wall -Werror subdir-objects])
|
||||
AC_PROG_CC
|
||||
AM_PROG_AR
|
||||
|
||||
LT_PREREQ([2.2])
|
||||
LT_INIT
|
||||
|
||||
# --enable-tests
|
||||
AC_ARG_ENABLE([tests],
|
||||
[AS_HELP_STRING([--enable-tests], [Build Criterion unit tests (default: no)])],
|
||||
[enable_tests=$enableval],
|
||||
[enable_tests=no])
|
||||
|
||||
AM_CONDITIONAL([BUILD_TESTS], [test "x$enable_tests" = "xyes"])
|
||||
|
||||
AS_IF([test "x$enable_tests" = "xyes"], [
|
||||
PKG_CHECK_MODULES([CRITERION], [criterion], [], [
|
||||
AC_MSG_NOTICE([pkg-config could not find criterion -- trying manual detection])
|
||||
AC_CHECK_HEADER([criterion/criterion.h], [], [
|
||||
AC_MSG_ERROR([criterion/criterion.h not found])])
|
||||
AC_CHECK_LIB([criterion], [main],
|
||||
[CRITERION_LIBS="-lcriterion"],
|
||||
[AC_MSG_ERROR([libcriterion not found])])
|
||||
])
|
||||
])
|
||||
|
||||
AC_CONFIG_FILES([
|
||||
Makefile
|
||||
src/Makefile
|
||||
tests/Makefile
|
||||
example/Makefile
|
||||
])
|
||||
AC_OUTPUT
|
||||
|
||||
AC_MSG_NOTICE([
|
||||
|
||||
libcli $VERSION
|
||||
---------------
|
||||
prefix : $prefix
|
||||
CC : $CC
|
||||
CFLAGS : $CFLAGS
|
||||
tests : $enable_tests
|
||||
])
|
||||
6
example/Makefile.am
Normal file
6
example/Makefile.am
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
noinst_PROGRAMS = greet
|
||||
|
||||
greet_SOURCES = main.c
|
||||
greet_CPPFLAGS = -I$(top_srcdir)/include
|
||||
greet_CFLAGS = -std=c99 -Wall -Wextra
|
||||
greet_LDADD = $(top_builddir)/src/libcli.la
|
||||
120
example/main.c
Normal file
120
example/main.c
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
#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;
|
||||
};
|
||||
|
||||
/* Forward declarations */
|
||||
static int handle_help(const char *arg, void *cfg);
|
||||
static int handle_version(const char *arg, void *cfg);
|
||||
static int handle_verbose(const char *arg, void *cfg);
|
||||
static int handle_name(const char *arg, void *cfg);
|
||||
static int handle_count(const char *arg, void *cfg);
|
||||
/* ------------------- */
|
||||
|
||||
/* 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
|
||||
|
||||
/* Handlers */
|
||||
|
||||
static int
|
||||
handle_help(__unused const char *arg, __unused void *cfg)
|
||||
{
|
||||
printf("Usage: greet [OPTIONS]\n\n");
|
||||
cli_print_options(g_opts, GREET_NB_OPTS, cli_arg_type_to_str);
|
||||
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;
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
48
include/cli.h
Normal file
48
include/cli.h
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
#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);
|
||||
typedef const char *(*t_arg_type_to_str)(int type);
|
||||
|
||||
struct option_descriptor
|
||||
{
|
||||
char short_opt;
|
||||
const char *long_opt;
|
||||
int has_arg;
|
||||
t_option_handler handler;
|
||||
int arg_type;
|
||||
const char *description;
|
||||
};
|
||||
|
||||
/* Default arg types — clients may define their own */
|
||||
#define CLI_OPT_ARG_TYPES \
|
||||
X(OPT_ARG_NONE, "" ) \
|
||||
X(OPT_ARG_INT, "<NUM>") \
|
||||
X(OPT_ARG_UINT, "<NUM>") \
|
||||
X(OPT_ARG_FLOAT, "<NUM>") \
|
||||
X(OPT_ARG_STRING, "<STR>")
|
||||
|
||||
#define X(name, str) name,
|
||||
enum { CLI_OPT_ARG_TYPES };
|
||||
#undef X
|
||||
|
||||
void cli_set_prog_name(const char *name);
|
||||
void cli_print_options(const struct option_descriptor *opts, size_t nb_opts,
|
||||
t_arg_type_to_str to_str);
|
||||
|
||||
const char *cli_arg_type_to_str(int type);
|
||||
|
||||
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
|
||||
11
include/cli_parse_utils.h
Normal file
11
include/cli_parse_utils.h
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
#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_int64(const char *s, int64_t *out);
|
||||
int cli_parse_float(const char *s, float *out);
|
||||
int cli_parse_ufloat(const char *s, float *out);
|
||||
|
||||
#endif
|
||||
16
include/compiler.h
Normal file
16
include/compiler.h
Normal 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
|
||||
15
src/Makefile.am
Normal file
15
src/Makefile.am
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
lib_LTLIBRARIES = libcli.la
|
||||
|
||||
libcli_la_SOURCES = \
|
||||
parse.c \
|
||||
help.c \
|
||||
parse_utils/parse_uint.c \
|
||||
parse_utils/parse_int.c \
|
||||
parse_utils/parse_float.c \
|
||||
parse_utils/parse_ufloat.c
|
||||
|
||||
libcli_la_CPPFLAGS = -I$(top_srcdir)/include
|
||||
libcli_la_CFLAGS = -std=c99 -Wall -Wextra
|
||||
|
||||
# Libtool versioning: current:revision:age
|
||||
libcli_la_LDFLAGS = -version-info 0:1:0
|
||||
34
src/help.c
Normal file
34
src/help.c
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
#include <stdio.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#include "cli.h"
|
||||
#include "compiler.h"
|
||||
|
||||
const char *
|
||||
cli_arg_type_to_str(int type)
|
||||
{
|
||||
static const char *strs[] = {
|
||||
#define X(name, str) str,
|
||||
CLI_OPT_ARG_TYPES
|
||||
#undef X
|
||||
};
|
||||
if ((size_t)type < COUNT_OF(strs))
|
||||
return strs[type];
|
||||
return "<ARG>";
|
||||
}
|
||||
|
||||
void
|
||||
cli_print_options(const struct option_descriptor *opts, size_t nb_opts,
|
||||
t_arg_type_to_str to_str)
|
||||
{
|
||||
printf("Options:\n");
|
||||
for (size_t i = 0; i < nb_opts; i++)
|
||||
{
|
||||
const char *argstr = to_str(opts[i].arg_type);
|
||||
printf(" -%c, --%-15s %-8s %s\n",
|
||||
opts[i].short_opt,
|
||||
opts[i].long_opt,
|
||||
argstr,
|
||||
opts[i].description);
|
||||
}
|
||||
}
|
||||
168
src/parse.c
Normal file
168
src/parse.c
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
#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);
|
||||
if (NULL == desc)
|
||||
return CLI_ERROR;
|
||||
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);
|
||||
}
|
||||
21
src/parse_utils/parse_float.c
Normal file
21
src/parse_utils/parse_float.c
Normal 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)
|
||||
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;
|
||||
}
|
||||
22
src/parse_utils/parse_int.c
Normal file
22
src/parse_utils/parse_int.c
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
#include <errno.h>
|
||||
#include <inttypes.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
int
|
||||
cli_parse_int64(const char *s, int64_t *out)
|
||||
{
|
||||
char *end;
|
||||
|
||||
if (NULL == s || '\0' == *s)
|
||||
return 1;
|
||||
|
||||
errno = 0;
|
||||
const intmax_t v = strtoimax(s, &end, 10);
|
||||
|
||||
if (s == end || ERANGE == errno || '\0' != *end || v < INT64_MIN || v > INT64_MAX)
|
||||
return 1;
|
||||
|
||||
*out = (int64_t)v;
|
||||
return 0;
|
||||
}
|
||||
21
src/parse_utils/parse_ufloat.c
Normal file
21
src/parse_utils/parse_ufloat.c
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
#include <errno.h>
|
||||
#include <stdlib.h>
|
||||
#include <math.h>
|
||||
|
||||
int
|
||||
cli_parse_ufloat(const char *s, float *out)
|
||||
{
|
||||
char *end;
|
||||
|
||||
if (NULL == s || '\0' == *s)
|
||||
return 1;
|
||||
|
||||
errno = 0;
|
||||
float v = strtof(s, &end);
|
||||
|
||||
if (s == end || ERANGE == errno || '\0' != *end || 0 == isfinite(v) || v < 0.0f)
|
||||
return 1;
|
||||
|
||||
*out = v;
|
||||
return 0;
|
||||
}
|
||||
21
src/parse_utils/parse_uint.c
Normal file
21
src/parse_utils/parse_uint.c
Normal 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;
|
||||
}
|
||||
15
tests/Makefile.am
Normal file
15
tests/Makefile.am
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
AM_CPPFLAGS = -I$(top_srcdir)/include $(CRITERION_CFLAGS)
|
||||
AM_CFLAGS = -std=c99
|
||||
|
||||
check_PROGRAMS = test_cli
|
||||
|
||||
test_cli_SOURCES = \
|
||||
test_main.c \
|
||||
parse_utils/test_parse_int.c \
|
||||
parse_utils/test_parse_float.c
|
||||
|
||||
test_cli_LDADD = \
|
||||
$(top_builddir)/src/libcli.la \
|
||||
$(CRITERION_LIBS)
|
||||
|
||||
TESTS = test_cli
|
||||
69
tests/parse_utils/test_parse_float.c
Normal file
69
tests/parse_utils/test_parse_float.c
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
#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 (cli_parse_float accepts negatives; use cli_parse_ufloat to reject) */
|
||||
Test(parse_float, negative_number)
|
||||
{
|
||||
float result;
|
||||
int ret = cli_parse_float("-42", &result);
|
||||
|
||||
cr_assert_eq(ret, 0);
|
||||
cr_assert_float_eq(result, -42.0f, 0.0001f);
|
||||
}
|
||||
|
||||
/* 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);
|
||||
}
|
||||
68
tests/parse_utils/test_parse_int.c
Normal file
68
tests/parse_utils/test_parse_int.c
Normal 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);
|
||||
}
|
||||
6
tests/test_main.c
Normal file
6
tests/test_main.c
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
#include <criterion/criterion.h>
|
||||
|
||||
Test(dummy, always_pass)
|
||||
{
|
||||
cr_assert(1, "Hello, world!");
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue