diff options
| author | Lion Kortlepel <[email protected]> | 2026-01-20 23:29:43 +0100 |
|---|---|---|
| committer | Lion Kortlepel <[email protected]> | 2026-01-20 23:29:43 +0100 |
| commit | 4ed5f7e5d99885f445ec70671779e60efa1bcbcc (patch) | |
| tree | 93dd195e52b61adbbc9339d337f4a6edeee4fbeb | |
| download | args-4ed5f7e5d99885f445ec70671779e60efa1bcbcc.tar.zst args-4ed5f7e5d99885f445ec70671779e60efa1bcbcc.zip | |
initial commit
| -rw-r--r-- | .clang-format | 4 | ||||
| -rw-r--r-- | .clangd | 3 | ||||
| -rw-r--r-- | .gitignore | 5 | ||||
| -rw-r--r-- | LICENSE | 21 | ||||
| -rw-r--r-- | Makefile | 21 | ||||
| -rw-r--r-- | ls_args.h | 262 | ||||
| -rw-r--r-- | tests/ls_test.h | 288 | ||||
| -rw-r--r-- | tests/tests.c | 27 |
8 files changed, 631 insertions, 0 deletions
diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..a65181b --- /dev/null +++ b/.clang-format @@ -0,0 +1,4 @@ +BasedOnStyle: WebKit +BreakBeforeBraces: Attach +SpaceAfterTemplateKeyword: false +ColumnLimit: 80 @@ -0,0 +1,3 @@ +CompileFlags: + CompilationDatabase: . + Add: [-x, c] diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..45b6c28 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +tests/tests +.cache/ +*.o +compile_commands.json +coverage/ @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Lion Kortlepel <[email protected]> + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..9381484 --- /dev/null +++ b/Makefile @@ -0,0 +1,21 @@ +# CAUTION: This Makefile builds ONLY the tests. +# To use this library, see ls_test.h or README.md. + +tests/tests: ls_args.o tests/tests.c tests/ls_test.h + $(CC) -o $@ ls_args.o tests/tests.c -Itests -I. -ggdb + +# Usually you wouldn't do this, but for tests we want this compiled with the +# most pedantic settings. +# Dont use this. +ls_args.o: ls_args.h + $(CC) -c -x c -o $@ $^ -Wall -Wextra -Wpedantic -Werror -std=c89 -ggdb \ + -Wno-error=pragma-once-outside-header \ + -I. \ + -DLS_ARGS_IMPLEMENTATION \ + -Wno-pragma-once-outside-header + +.PHONY: clean + +clean: + rm -f tests/tests + rm -f ls_args.o diff --git a/ls_args.h b/ls_args.h new file mode 100644 index 0000000..fc23134 --- /dev/null +++ b/ls_args.h @@ -0,0 +1,262 @@ +#pragma once + +#include <stddef.h> + +#ifndef LS_REALLOC +#include <stdlib.h> +#define LS_REALLOC realloc +#endif +#ifndef LS_FREE +#include <stdlib.h> +#define LS_FREE free +#endif + +/* Yes the naming is a little inconsistent, but "arg optional" reads better than + * "args mode optional" */ +typedef enum ls_args_mode { + LS_ARG_OPTIONAL = 0, + LS_ARG_REQUIRED = 1 +} ls_args_mode; + +typedef enum ls_args_type { LS_ARGS_TYPE_BOOL = 0 } ls_args_type; + +typedef struct ls_args_arg { + const char* short_opt; + const char* long_opt; + const char* help; + ls_args_type type; + void* val_ptr; + ls_args_mode mode; +} ls_args_arg; + +typedef struct ls_args { + ls_args_arg* args; + int args_len; + int args_cap; +} ls_args; + +void ls_args_init(ls_args*); + +void ls_arg_bool(ls_args*, int* val, const char* short_opt, + const char* long_opt, const char* help, ls_args_mode mode); + +int ls_args_parse(ls_args*, int argc, char** argv); + +char* ls_args_help(ls_args*); + +void ls_args_free(ls_args*); + +#ifdef LS_ARGS_IMPLEMENTATION + +#include <assert.h> +#include <limits.h> +#include <string.h> + +/* 0 on failure, 1 on success */ +static int _lsa_add(ls_args* a, ls_args_arg** arg) { + /* a is already checked when this is called */ + assert(arg != NULL); + if (a->args_len + 1 > a->args_cap) { + ls_args_arg* new_args; + int new_cap = a->args_cap + a->args_cap / 2 + 8; + if (new_cap > INT_MAX / (int)sizeof(*a->args)) { + /* int overflow */ + return 0; + } + new_args = LS_REALLOC(a->args, new_cap * sizeof(*new_args)); + if (new_args == NULL) { + /* allocation failure */ + return 0; + } + a->args_cap = new_cap; + a->args = new_args; + } + *arg = &a->args[a->args_len++]; + return 1; +} + +void ls_args_init(ls_args* a) { memset(a, 0, sizeof(*a)); } + +void _lsa_register(ls_args* a, void* val, ls_args_type type, + const char* short_opt, const char* long_opt, const char* help, + ls_args_mode mode) { + ls_args_arg* arg; + assert(a != NULL); + assert(val != NULL); + /* only one can be NULL, not both, but neither have to be NULL */ + assert(short_opt != NULL || long_opt != NULL); + /* if short_opt isn't null, it must be 1 char */ + assert(short_opt == NULL || strlen(short_opt) == 1); + assert(_lsa_add(a, &arg)); + /* TODO: sanity check that there are no dashes in there, because that would + * be a misuse of the API. */ + /* remove preceding dashes for later matching */ + while (*long_opt == '-') + long_opt++; + /* remove preceding dashes for later matching */ + while (*short_opt == '-') + short_opt++; + /* the rest may be NULL */ + arg->type = type; + arg->short_opt = short_opt; + arg->long_opt = long_opt; + arg->help = help; + arg->mode = mode; + arg->val_ptr = val; +} + +typedef enum _lsa_parsed_type { + LS_ARGS_PARSED_ERROR = 0, + LS_ARGS_PARSED_LONG = 1, + LS_ARGS_PARSED_SHORT = 2, + LS_ARGS_PARSED_STOP = 3, + LS_ARGS_PARSED_POSITIONAL = 4 +} _lsa_parsed_type; + +typedef struct _lsa_parsed { + _lsa_parsed_type type; + union { + /* the full argument that caused the error */ + const char* erroneous; + /* the long arg without the `--` */ + const char* long_arg; + /* might be multiple, like for -abc it would be `abc` */ + const char* short_args; + /* an argument provided without `--`, in full */ + const char* positional; + } as; +} _lsa_parsed; + +_lsa_parsed _lsa_parse(const char* s) { + size_t s_len = strlen(s); + _lsa_parsed res; + assert(s != NULL); + /* empty string or `-` */ + if (s_len < 2) { + res.type = LS_ARGS_PARSED_ERROR; + res.as.erroneous = s; + goto end; + } + if (s[0] == '-') { + if (s[1] == '-') { + /* long opt */ + size_t remaining = s_len - 2; + if (remaining == 0) { + /* special case where `--` is provided on its own to signal + * "everything after this is positional" */ + res.type = LS_ARGS_PARSED_STOP; + goto end; + } + res.type = LS_ARGS_PARSED_LONG; + res.as.long_arg = &s[remaining]; + } else { + /* short opt */ + size_t remaining = s_len - 1; + if (remaining == 0) { + /* shouldn't be possible due to earlier checks */ + res.type = LS_ARGS_PARSED_ERROR; + res.as.erroneous = s; + goto end; + } + res.type = LS_ARGS_PARSED_SHORT; + res.as.short_args = &s[remaining]; + } + } else { + res.type = LS_ARGS_PARSED_POSITIONAL; + res.as.positional = s; + } + +end: + return res; +} + +void ls_arg_bool(ls_args* a, int* val, const char* short_opt, + const char* long_opt, const char* help, ls_args_mode mode) { + _lsa_register(a, val, LS_ARGS_TYPE_BOOL, short_opt, long_opt, help, mode); +} + +void _lsa_apply(ls_args_arg* arg, _lsa_parsed* parsed, ls_args_arg** prev_arg) { + (void)parsed; + (void)prev_arg; + switch (arg->type) { + case LS_ARGS_TYPE_BOOL: + *(int*)arg->val_ptr = 1; + break; + } + *prev_arg = arg; +} + +#include <stdio.h> + +int ls_args_parse(ls_args* a, int argc, char** argv) { + int i; + int k; + ls_args_arg* prev_arg = NULL; + assert(a != NULL); + assert(argv != NULL); + for (i = 1; i < argc; ++i) { + _lsa_parsed parsed = _lsa_parse(argv[i]); + if (prev_arg && prev_arg->type != LS_ARGS_TYPE_BOOL) { + if (parsed.type != LS_ARGS_PARSED_POSITIONAL) { + /* argument for the previous param expected, but none given */ + /* TODO: Error properly */ + fprintf(stderr, "Expected argument for '--%s'\n", + prev_arg->long_opt); + return 0; + } + _lsa_apply(prev_arg, &parsed, NULL); + continue; + } + switch (parsed.type) { + case LS_ARGS_PARSED_ERROR: + /* TODO: Return/print/save error */ + fprintf(stderr, "Failed to parse '%s'\n", parsed.as.erroneous); + return 0; + case LS_ARGS_PARSED_LONG: + for (k = 0; k < a->args_len; ++k) { + if (a->args[k].long_opt != NULL + && strcmp(argv[i], a->args[k].long_opt) == 0) { + /* match! */ + _lsa_apply(&a->args[k], &parsed, &prev_arg); + break; + } + } + break; + case LS_ARGS_PARSED_SHORT: { + const char* args = parsed.as.short_args; + while (*args) { + char arg = *args++; + for (k = 0; k < a->args_len; ++k) { + const char* opt = a->args[k].short_opt; + if (opt != NULL && opt[0] == arg) { + /* match! */ + _lsa_apply(&a->args[k], &parsed, &prev_arg); + break; + } + } + } + break; + } + case LS_ARGS_PARSED_STOP: + case LS_ARGS_PARSED_POSITIONAL: + assert(!"NOT IMPLEMENTED"); + break; + } + } + return 1; +} + +char* ls_args_help(ls_args* a) { + (void)a; + return "help!"; +} + +void ls_args_free(ls_args* a) { + if (a) { + LS_FREE(a->args); + a->args = NULL; + a->args_cap = 0; + a->args_len = 0; + } +} +#endif diff --git a/tests/ls_test.h b/tests/ls_test.h new file mode 100644 index 0000000..f9f4fe9 --- /dev/null +++ b/tests/ls_test.h @@ -0,0 +1,288 @@ +/* Lion's Standard (LS) test harness. + * + * Version: 1.0 + * Website: https://libls.org + * Repo: https://github.com/libls/test + * SPDX-License-Identifier: MIT + * + * ==== TABLE OF CONTENTS ==== + * + * 1. DESCRIPTION + * 2. HOW TO USE + * 3. LICENSE + * + * ==== 1. DESCRIPTION ==== + * + * This is a super simple, minimal unit-test harness. It has auto-registering + * tests and some macros for easy usage. + * + * Compiles under ANSI C, the only special part is the extension __typeof__ if + * you use asserts other than `ASSERT` (e.g. ASSERT_EQ), and the constructor + * attribute __attribute__((destructor)) for automatic test registration. + * + * ==== 2. HOW TO USE ==== + * + * 1. Copy this file into your project and include it: + * + * #include "ls_test.h" + * + * 2. In ONE C file, define LS_TEST_IMPLEMENTATION before including: + * + * #define LS_TEST_IMPLEMENTATION + * #include "ls_test.h" + * + * 3. Write tests as functions with no arguments/returns: + * + * TEST_CASE(test_add) { + * ASSERT_EQ(add(1, 2), 3, "%d"); + * return 0; // needed for --failfast + * } + * + * Use unique names, avoid starting with ls_ or lst_. + * + * 4. Add a main: + * + * TEST_MAIN + * + * 5. Compile and run. Use --help for options. + * + * ==== 3. LICENSE ==== + * + * This file is provided under the MIT license. For commercial support and + * maintenance, feel free to use the e-mail below to contact the author(s). + * + * The MIT License (MIT) + * + * Copyright (c) 2026 Lion Kortlepel <[email protected]> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the “Software”), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +#pragma once + +#include <stdio.h> + +#define TEST_MAIN \ + int main(int argc, char** argv) { return ls_test_main(argc, argv); } + +#define TEST_CASE(name) \ + static int LS_CAT(lst_t_, name)(void); \ + static void LS_CAT(lst_init_, name)(void) LS_CONSTRUCTOR; \ + static void LS_CAT(lst_init_, name)(void) { \ + lst_reg(LS_CAT(lst_t_, name)); \ + } \ + static int LS_CAT(lst_t_, name)(void) + +#define ASSERT(cond) \ + do { \ + if (!(cond)) { \ + const char* _func = __func__; \ + if (strncmp(_func, "lst_t_", 6) == 0) \ + _func += 6; \ + fprintf(stderr, "%s: FAILED: %s (%s:%d)\n", _func, #cond, \ + __FILE__, __LINE__); \ + } \ + } while (0) + +/* the following macros require __typeof__ */ + +#define ASSERT_EQ(a, b, fmt) \ + do { \ + __typeof__(a) _a = (a); \ + __typeof__(b) _b = (b); \ + if (_a != _b) { \ + const char* _func = __func__; \ + if (strncmp(_func, "lst_t_", 6) == 0) \ + _func += 6; \ + fprintf(stderr, \ + "%s: FAILED: %s == %s (actual: " fmt " != " fmt ") (%s:%d)\n", \ + _func, #a, #b, _a, _b, __FILE__, __LINE__); \ + ++lst_fail; \ + return 1; \ + } \ + ++lst_ok; \ + } while (0) + +#define ASSERT_NEQ(a, b, fmt) \ + do { \ + __typeof__(a) _a = (a); \ + __typeof__(b) _b = (b); \ + if (_a == _b) { \ + const char* _func = __func__; \ + if (strncmp(_func, "lst_t_", 6) == 0) \ + _func += 6; \ + fprintf(stderr, \ + "%s: FAILED: %s != %s (actual: " fmt " == " fmt ") (%s:%d)\n", \ + _func, #a, #b, _a, _b, __FILE__, __LINE__); \ + ++lst_fail; \ + return 1; \ + } \ + ++lst_ok; \ + } while (0) + +#define ASSERT_LT(a, b, fmt) \ + do { \ + __typeof__(a) _a = (a); \ + __typeof__(b) _b = (b); \ + if (!(_a < _b)) { \ + const char* _func = __func__; \ + if (strncmp(_func, "lst_t_", 6) == 0) \ + _func += 6; \ + fprintf(stderr, \ + "%s: FAILED: %s < %s (actual: " fmt " >= " fmt ") (%s:%d)\n", \ + _func, #a, #b, _a, _b, __FILE__, __LINE__); \ + ++lst_fail; \ + return 1; \ + } \ + ++lst_ok; \ + } while (0) + +#define ASSERT_LE(a, b, fmt) \ + do { \ + __typeof__(a) _a = (a); \ + __typeof__(b) _b = (b); \ + if (!(_a <= _b)) { \ + const char* _func = __func__; \ + if (strncmp(_func, "lst_t_", 6) == 0) \ + _func += 6; \ + fprintf(stderr, \ + "%s: FAILED: %s <= %s (actual: " fmt " > " fmt ") (%s:%d)\n", \ + _func, #a, #b, _a, _b, __FILE__, __LINE__); \ + ++lst_fail; \ + return 1; \ + } \ + ++lst_ok; \ + } while (0) + +#define ASSERT_GT(a, b, fmt) \ + do { \ + __typeof__(a) _a = (a); \ + __typeof__(b) _b = (b); \ + if (!(_a > _b)) { \ + const char* _func = __func__; \ + if (strncmp(_func, "lst_t_", 6) == 0) \ + _func += 6; \ + fprintf(stderr, \ + "%s: FAILED: %s > %s (actual: " fmt " <= " fmt ") (%s:%d)\n", \ + _func, #a, #b, _a, _b, __FILE__, __LINE__); \ + ++lst_fail; \ + return 1; \ + } \ + ++lst_ok; \ + } while (0) + +#define ASSERT_GE(a, b, fmt) \ + do { \ + __typeof__(a) _a = (a); \ + __typeof__(b) _b = (b); \ + if (!(_a >= _b)) { \ + const char* _func = __func__; \ + if (strncmp(_func, "lst_t_", 6) == 0) \ + _func += 6; \ + fprintf(stderr, \ + "%s: FAILED: %s >= %s (actual: " fmt " < " fmt ") (%s:%d)\n", \ + _func, #a, #b, _a, _b, __FILE__, __LINE__); \ + ++lst_fail; \ + return 1; \ + } \ + ++lst_ok; \ + } while (0) +#define LS_CAT2(a, b) a##b +#define LS_CAT(a, b) LS_CAT2(a, b) + +#if defined(__GNUC__) || defined(__clang__) +#define LS_CONSTRUCTOR __attribute__((constructor)) +#else +#error "Requires __attribute__((constructor)) support" +#endif + +typedef int (*lst_func)(void); + +extern lst_func* lst_funcs; +extern int lst_n; +extern int lst_cap; +extern int lst_fail; +extern int lst_ok; + +void lst_reg(lst_func f); + +#ifdef LS_TEST_IMPLEMENTATION +#include <stdlib.h> +#include <string.h> + +lst_func* lst_funcs; +int lst_n; +int lst_cap; +int lst_fail = 0; +int lst_ok = 0; + +void lst_reg(lst_func f) { + if (lst_n == lst_cap) { + if (lst_cap == 0) { + lst_cap = 8; + } else { + lst_cap *= 2; + } + lst_funcs = (lst_func*)realloc(lst_funcs, lst_cap * sizeof(*lst_funcs)); + } + lst_funcs[lst_n++] = f; +} + +#define HELP_STR \ + "Usage: %s [options]\n" \ + "Options:\n" \ + " --failfast Stop after the first failed test\n" \ + " --help Show this help message\n" + +static int ls_test_main(int argc, char** argv) { + (void)argc; + (void)argv; + + int failfast = 0; + int i; + + for (i = 1; i < argc; ++i) { + if (strcmp(argv[i], "--failfast") == 0) { + failfast = 1; + } else if (strcmp(argv[i], "--help") == 0) { + fprintf(stderr, HELP_STR, argv[0]); + return 0; + } else { + fprintf( + stderr, "unknown argument: %s\n\n" HELP_STR, argv[i], argv[0]); + return 1; + } + } + + for (i = 0; i < lst_n; ++i) { + if (lst_funcs[i]() != 0 && failfast) { + goto end; + } + } + +end: + fprintf(stderr, "%d succeeded, %d failed, %d total\n", lst_ok, lst_fail, + lst_ok + lst_fail); + + if (lst_fail > 0) { + return 1; + } + + return 0; +} +#endif diff --git a/tests/tests.c b/tests/tests.c new file mode 100644 index 0000000..0e644e8 --- /dev/null +++ b/tests/tests.c @@ -0,0 +1,27 @@ +#define LS_TEST_IMPLEMENTATION +#include "ls_test.h" + +#include "ls_args.h" + +TEST_CASE(basic_args) { + int help = 0; + int test = 0; + int no = 0; + ls_args args; + char* argv[] = { "./hello", "-h", "--test", NULL }; + int argc = sizeof(argv) / sizeof(*argv) - 1; + + ls_args_init(&args); + ls_arg_bool(&args, &help, "h", "help", "Provides help", 0); + ls_arg_bool(&args, &test, "t", "test", "A test argument", 0); + ls_arg_bool(&args, &no, "n", "nope", "An argument that isn't present", 0); + ASSERT(ls_args_parse(&args, argc, argv)); + ASSERT_EQ(help, 1, "%d"); + ASSERT_EQ(test, 1, "%d"); + ASSERT_EQ(no, 0, "%d"); + ls_args_free(&args); + return 0; +} + + +TEST_MAIN |
