From bb5e4eeaa960e0e8931212e4f159e8b27be1c449 Mon Sep 17 00:00:00 2001 From: Mikhail Romanko Date: Tue, 22 Sep 2026 19:00:03 +0300 Subject: [PATCH] Initial commit --- .github/workflows/ci.yaml | 89 +++++++++++++ .gitignore | 68 ++++++++++ CMakeLists.txt | 27 ++++ CgeConf.h | 33 +++++ CgeConfQuery.h | 30 +++++ Conf.c | 188 +++++++++++++++++++++++++++ LICENSE | 12 ++ Makefile.lint | 29 +++++ Makefile.mingw | 23 ++++ Makefile.posix | 35 ++++++ Makefile.win32 | 23 ++++ Query.c | 258 ++++++++++++++++++++++++++++++++++++++ README.md | 7 ++ 13 files changed, 822 insertions(+) create mode 100644 .github/workflows/ci.yaml create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 CgeConf.h create mode 100644 CgeConfQuery.h create mode 100644 Conf.c create mode 100644 LICENSE create mode 100644 Makefile.lint create mode 100644 Makefile.mingw create mode 100644 Makefile.posix create mode 100644 Makefile.win32 create mode 100644 Query.c create mode 100644 README.md diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..01e9e41 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,89 @@ +name: CI + +on: + push: + branches: [trunk] + pull_request: + branches: [trunk] + +jobs: + build-and-analyze: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install tools + run: | + sudo apt-get update -qq + sudo apt-get install -y build-essential clang clang-tools clang-tidy cppcheck gcc-mingw-w64-x86-64 cmake + + - name: Build with GCC + run: | + make -f Makefile.posix clean + make -f Makefile.posix CC=gcc CFLAGS="-std=c99 -Wall -Wextra -Wpedantic -Werror -O2" + + - name: Build with Clang + run: | + make -f Makefile.posix clean + make -f Makefile.posix CC=clang CFLAGS="-std=c99 -Wall -Wextra -Wpedantic -Werror -O2" + + - name: Build with MinGW + run: | + make -f Makefile.posix clean + make -f Makefile.mingw CC=x86_64-w64-mingw32-gcc AR=x86_64-w64-mingw32-ar CFLAGS="-std=c99 -Wall -Wextra -Wpedantic -Werror -O2" + + - name: Build with CMake + run: | + cmake -S . -B build + cmake --build build + + - name: Linting checks + run: | + echo "Running all linting checks..." + FAILED=0 + + # cppcheck + echo "=== Check 1/4: cppcheck ===" + if make -f Makefile.lint cppcheck; then + echo "cppcheck: PASSED" + else + echo "cppcheck: FAILED" + FAILED=1 + fi + + # clang-tidy + echo "=== Check 2/4: clang-tidy ===" + if make -f Makefile.lint clang-tidy; then + echo "clang-tidy: PASSED" + else + echo "clang-tidy: FAILED" + FAILED=1 + fi + + # scan-build + echo "=== Check 3/4: scan-build ===" + if make -f Makefile.lint scan-build; then + echo "scan-build: PASSED" + else + echo "scan-build: FAILED" + FAILED=1 + fi + + # security-check + echo "=== Check 4/4: security-check ===" + if make -f Makefile.lint security-check; then + echo "security-check: PASSED" + else + echo "security-check: FAILED" + FAILED=1 + fi + + # Final result + if [ $FAILED -ne 0 ]; then + echo "One or more linting checks failed." + exit 1 + else + echo "All linting checks passed." + fi \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f765426 --- /dev/null +++ b/.gitignore @@ -0,0 +1,68 @@ +# ---> C +# Prerequisites +*.d + +# Object files +*.o +*.ko +*.obj +*.elf + +# Linker output +*.ilk +*.map +*.exp + +# Precompiled Headers +*.gch +*.pch + +# Libraries +*.lib +*.a +*.la +*.lo + +# Shared objects (inc. Windows DLLs) +*.dll +*.so +*.so.* +*.dylib + +# Executables +*.exe +*.out +*.app +*.i*86 +*.x86_64 +*.hex + +# Debug files +*.dSYM/ +*.su +*.idb +*.pdb + +# Kernel Module Compile Results +*.mod* +*.cmd +.tmp_versions/ +modules.order +Module.symvers +Mkfile.old +dkms.conf + +# ---> CMake +CMakeLists.txt.user +CMakeCache.txt +CMakeFiles +CMakeScripts +Testing +Makefile +cmake_install.cmake +install_manifest.txt +compile_commands.json +CTestTestfile.cmake +_deps +CMakeUserPresets.json + diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..5a4be30 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,27 @@ +cmake_minimum_required(VERSION 3.10) +project(CgeConf LANGUAGES C) + +set(CMAKE_C_STANDARD 99) +set(CMAKE_C_STANDARD_REQUIRED ON) + +set(CONF_SOURCES + Conf.c + Query.c +) + +set(CONF_HEADERS + CgeConf.h + CgeConfQuery.h +) + +add_library(CgeConf STATIC ${CONF_SOURCES} ${CONF_HEADERS}) + +target_include_directories(CgeConf PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) + +install(TARGETS CgeConf + ARCHIVE DESTINATION lib + LIBRARY DESTINATION lib + RUNTIME DESTINATION bin +) + +install(FILES ${CONF_HEADERS} DESTINATION include) diff --git a/CgeConf.h b/CgeConf.h new file mode 100644 index 0000000..7cdd843 --- /dev/null +++ b/CgeConf.h @@ -0,0 +1,33 @@ +#ifndef CGE_CONF_H +#define CGE_CONF_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct CgeConfIO { + int (*read)(void *ctx, void *buffer, size_t size, size_t *actual); + void *ctx; +} CgeConfIO; + +enum CgeConfEvent { + CGE_CONF_SECTION, + CGE_CONF_KEYVALUE, +}; + +typedef void (*CgeConfCb)(int type, const char *key, const char *value, + void *user); + +int CgeConfFromString(const char *str, CgeConfCb callback, void *user); +int CgeConfFromMemory(const char *buffer, size_t size, CgeConfCb callback, + void *user); +int CgeConfFromFile(const char *path, CgeConfCb callback, void *user); +int CgeConfFromCb(CgeConfIO *io, CgeConfCb callback, void *user); + +#ifdef __cplusplus +} +#endif + +#endif /* CGE_CONF_H */ diff --git a/CgeConfQuery.h b/CgeConfQuery.h new file mode 100644 index 0000000..af9a91c --- /dev/null +++ b/CgeConfQuery.h @@ -0,0 +1,30 @@ +#ifndef CGE_CONF_QUERY_H +#define CGE_CONF_QUERY_H + +#include "CgeConf.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct CgeConfQuery CgeConfQuery; +typedef void (*CgeConfQueryIterCb)(const char *section, const char *key, + const char *value, void *user); + + +CgeConfQuery *CgeConfQueryFromCb(CgeConfIO *io); +CgeConfQuery *CgeConfQueryFromFile(const char *file); +CgeConfQuery *CgeConfQueryFromMemory(const char *buffer, size_t size); +CgeConfQuery *CgeConfQueryFromString(const char *str); +void CgeConfQueryFree(CgeConfQuery *query); +const char *CgeConfQueryGet(CgeConfQuery *query, const char *section, + const char *key, const char *defaultValue); + +void CgeConfQueryIter(CgeConfQuery *query, CgeConfQueryIterCb iter, + void *user); + +#ifdef __cplusplus +} +#endif + +#endif /* CGE_CONF_QUERY_H */ diff --git a/Conf.c b/Conf.c new file mode 100644 index 0000000..6e5d60c --- /dev/null +++ b/Conf.c @@ -0,0 +1,188 @@ +#include "CgeConf.h" +#include +#include +#include + +#define MAX_LINE 4096 + +typedef struct StrView { + char *data; + size_t size; +} StrView; + +typedef struct MemCtx { + const char *buffer; + size_t size; + size_t pos; +} MemCtx; + +static int memRead(void *ctx, void *buffer, size_t size, size_t *actual) { + MemCtx *mem = (MemCtx *)ctx; + size_t remaining = mem->size - mem->pos; + + if (size > remaining) + size = remaining; + + memcpy(buffer, mem->buffer + mem->pos, size); + mem->pos += size; + *actual = size; + return 1; +} + +static int fileRead(void *ctx, void *buffer, size_t size, size_t *actual) { + *actual = fread(buffer, 1, size, (FILE *)ctx); + return 1; +} + +static void trimLeft(StrView *view) { + for (; view->size && isspace(*view->data); ++view->data, --view->size); +} + +static void trimRight(StrView *view) { + char *last = view->data + view->size; + for (; last > view->data && isspace(*(last - 1)); last--, view->size--); +} + +static void trim(StrView *view) { + trimLeft(view); + trimRight(view); +} + +static size_t firstIndex(StrView *view, char symbol) { + char *current, *end; + current = view->data; + end = view->data + view->size; + + for (; current < end; ++current) { + if (*current == symbol) + return current - view->data; + } + return -1; +} + +static int split(StrView *view, StrView *prefix, char delim) { + size_t position; + + if ((position = firstIndex(view, delim)) == -1) + return 0; + + prefix->data = view->data; + prefix->size = position; + view->data += position; + view->size -= position; + return 1; +} + +static void parseLine(StrView *view, CgeConfCb callback, void *user) { + StrView key; + + trim(view); + + if (!view->size || *view->data == '#' || *view->data == ';') + return; + + if (*view->data == '[' && *(view->data + view->size - 1) == ']') { + view->size -= 2; + view->data += 1; + trim(view); + + view->data[view->size] = 0; + callback(CGE_CONF_SECTION, NULL, view->data, user); + } else if (split(view, &key, '=')) { + trimRight(&key); + view->data++, view->size--; + trimLeft(view); + + if (view->size > 1 && *view->data == '"' && + *(view->data + view->size - 1) == '"') { + view->size -= 2; + view->data += 1; + } + + key.data[key.size] = 0; + view->data[view->size] = 0; + callback(CGE_CONF_KEYVALUE, key.data, view->data, user); + } +} + +static int readSymbol(CgeConfIO *io) { + unsigned char symbol; + size_t actual; + + if (!io->read(io->ctx, &symbol, 1, &actual) || !actual) + return -1; + + return (int)symbol; +} + +static size_t readLine(CgeConfIO *io, char *buffer, size_t size) { + int symbol; + size_t counter = 0; + + while ((symbol = readSymbol(io)) != -1) { + if (counter && symbol == '\n') + break; + else if (symbol == '\n') + continue; + + if (symbol == '\r') + continue; + + if (counter >= size - 1) + return -1; + + buffer[counter++] = symbol; + } + + buffer[counter] = 0; + return counter; +} + +int CgeConfFromString(const char *str, CgeConfCb callback, void *user) { + return CgeConfFromMemory(str, strlen(str), callback, user); +} + +int CgeConfFromMemory(const char *buffer, size_t size, CgeConfCb callback, + void *user) { + CgeConfIO io; + MemCtx mem; + + mem.buffer = buffer; + mem.size = size; + mem.pos = 0; + + io.read = memRead; + io.ctx = &mem; + + return CgeConfFromCb(&io, callback, user); +} + +int CgeConfFromFile(const char *path, CgeConfCb callback, void *user) { + CgeConfIO io; + FILE *file; + int ret; + + file = fopen(path, "rb"); + if (!file) + return 0; + + io.read = fileRead; + io.ctx = file; + + ret = CgeConfFromCb(&io, callback, user); + fclose(file); + return ret; +} + +int CgeConfFromCb(CgeConfIO *io, CgeConfCb callback, void *user) { + char buffer[MAX_LINE]; + StrView view; + + while ((view.size = readLine(io, buffer, MAX_LINE))) { + if (view.size == -1) + return 0; + view.data = buffer; + parseLine(&view, callback, user); + } + return 1; +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3d44ffd --- /dev/null +++ b/LICENSE @@ -0,0 +1,12 @@ +Copyright (C) 2026 by blankhex me@blankhex.com + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. diff --git a/Makefile.lint b/Makefile.lint new file mode 100644 index 0000000..cac9110 --- /dev/null +++ b/Makefile.lint @@ -0,0 +1,29 @@ +# Makefile.lint - Individual linting targets + +.PHONY: all +all: cppcheck clang-tidy scan-build security-check + @echo "All linting scheduled" + +.PHONY: cppcheck +cppcheck: + @echo "Running cppcheck..." + @cppcheck --enable=warning,performance,portability --std=c99 --quiet -Iinclude -I. . + +.PHONY: clang-tidy +clang-tidy: + @echo "Running clang-tidy..." + @for file in $$(find . -name "*.c" -type f -not -path "*/win32/*"); do \ + clang-tidy "$$file" -- -I. -Iinclude -std=c99; \ + done + +.PHONY: scan-build +scan-build: + @echo "Running scan-build..." + @scan-build --status-bugs make -f Makefile.posix clean all CC=clang + +.PHONY: security-check +security-check: + @echo "Scanning for unsafe C functions..." + @grep -rnE '\b(strcpy|strcat|sprintf|gets|scanf|sscanf|realpath|mktemp|tempnam|tmpnam|getwd|getlogin)\b' \ + . --include="*.c" --include="*.h" | grep -v "^Binary file" && \ + exit 1 || exit 0 \ No newline at end of file diff --git a/Makefile.mingw b/Makefile.mingw new file mode 100644 index 0000000..a04de9b --- /dev/null +++ b/Makefile.mingw @@ -0,0 +1,23 @@ +# MinGW Makefile for CgeConf + +CC = gcc +AR = ar +CFLAGS = -std=c99 -O2 -Wall -Wextra +ARFLAGS = rcs +TARGET = libCgeConf.a + +SOURCES = Conf.c Query.c +OBJECTS = $(SOURCES:.c=.o) + +.PHONY: all clean + +all: $(TARGET) + +$(TARGET): $(OBJECTS) + $(AR) $(ARFLAGS) $@ $^ + +%.o: %.c CgeConf.h CgeConfQuery.h + $(CC) $(CFLAGS) -c $< -o $@ + +clean: + del $(OBJECTS) $(TARGET) 2>nul || exit 0 diff --git a/Makefile.posix b/Makefile.posix new file mode 100644 index 0000000..8b34b05 --- /dev/null +++ b/Makefile.posix @@ -0,0 +1,35 @@ +# POSIX Makefile for CgeConf + +CC = gcc +AR = ar +CFLAGS = -std=c99 -O2 -Wall -Wextra -fPIC +ARFLAGS = rcs +TARGET = libCgeConf.a + +SOURCES = Conf.c Query.c +OBJECTS = $(SOURCES:.c=.o) + +.PHONY: all clean install + +all: $(TARGET) + +$(TARGET): $(OBJECTS) + $(AR) $(ARFLAGS) $@ $^ + +%.o: %.c CgeConf.h CgeConfQuery.h + $(CC) $(CFLAGS) -c $< -o $@ + +clean: + rm -f $(OBJECTS) $(TARGET) + +install: $(TARGET) + cp $(TARGET) /usr/local/lib/ + cp CgeConf.h /usr/local/include/ + cp CgeConfQuery.h /usr/local/include/ + ldconfig || echo "Run ldconfig manually if needed" + +uninstall: + rm -f /usr/local/lib/libCgeConf.a + rm -f /usr/local/include/CgeConf.h + rm -f /usr/local/include/CgeConfQuery.h + ldconfig || true diff --git a/Makefile.win32 b/Makefile.win32 new file mode 100644 index 0000000..c7b9ee0 --- /dev/null +++ b/Makefile.win32 @@ -0,0 +1,23 @@ +# Makefile.win32 for MSVC (NMake) — CgeConf +# Usage: Open "x86 Native Tools Command Prompt", then: +# nmake -f Makefile.win32 + +CC = cl +LIB = lib +CFLAGS = /c /nologo /W3 /O2 +LIBFLAGS = /nologo +TARGET = CgeConf.lib + +SOURCES = Conf.c Query.c +OBJECTS = $(SOURCES:.c=.obj) + +$(TARGET): $(OBJECTS) + $(LIB) $(LIBFLAGS) /OUT:$(TARGET) $(OBJECTS) + +{.}.c{}.obj: + $(CC) $(CFLAGS) /Fo$@ $< + +clean: + del $(OBJECTS) $(TARGET) 2>nul + +.PHONY: clean diff --git a/Query.c b/Query.c new file mode 100644 index 0000000..883f0ac --- /dev/null +++ b/Query.c @@ -0,0 +1,258 @@ +#include "CgeConfQuery.h" + +#include +#include + +#define INIT_EXP 4 +#define INIT_CAPACITY ((size_t)1 << INIT_EXP) +#define MAX_SECTION 4096 + +typedef struct { + char *section; + char *key; + char *value; +} Entry; + +struct CgeConfQuery { + Entry *entryData; + size_t size; + size_t capacity; + Entry **slotData; + char currentSection[MAX_SECTION]; + int exp; +}; + +static char *stringDup(const char *str) { + size_t length; + char *dest; + + length = strlen(str); + if ((dest = malloc(length + 1))) + memcpy(dest, str, length + 1); + return dest; +} + +static size_t hashKey(const char *section, const char *key) { + size_t h = 5381; + const char *p; + int c; + + for (p = section; (c = *p); ++p) + h = ((h << 5) + h) + (unsigned char)c; + h = ((h << 5) + h); + for (p = key; (c = *p); ++p) + h = ((h << 5) + h) + (unsigned char)c; + + return h; +} + +static size_t lookup(size_t hash, int exp, size_t index) { + size_t mask = (1 << exp) - 1; + size_t step = (hash >> (sizeof(hash) * 8 - exp)) | 1; + return (index + step) & mask; +} + +static int reserveSpace(CgeConfQuery *query, size_t need) { + CgeConfQuery tmp; + size_t i, hash, index; + + if (need <= (query->capacity - (query->capacity >> 2))) + return 1; + + tmp.exp = query->exp ? query->exp + 1 : INIT_EXP; + tmp.capacity = query->capacity ? query->capacity * 2 : INIT_CAPACITY; + tmp.size = 0; + tmp.entryData = malloc(tmp.capacity * sizeof(*tmp.entryData)); + tmp.slotData = malloc(tmp.capacity * sizeof(*tmp.slotData)); + if (!tmp.entryData || !tmp.slotData) { + free(tmp.entryData); + free(tmp.slotData); + return 0; + } + memset(tmp.slotData, 0, sizeof(*tmp.slotData) * tmp.capacity); + + for (i = 0; i < query->size; ++i) { + Entry *entry = &query->entryData[i]; + + hash = hashKey(entry->section, entry->key); + index = lookup(hash, tmp.exp, 0); + while (tmp.slotData[index] != NULL) + index = lookup(hash, tmp.exp, index); + + tmp.entryData[tmp.size] = *entry; + tmp.slotData[index] = tmp.entryData + tmp.size++; + } + + free(query->entryData); + free(query->slotData); + query->entryData = tmp.entryData; + query->slotData = tmp.slotData; + query->capacity = tmp.capacity; + query->exp = tmp.exp; + return 1; +} + +static int insert(CgeConfQuery *query, const char *section, const char *key, + const char *value) { + size_t hash, index; + char *newValue; + Entry *entry; + + if (!reserveSpace(query, query->size + 1)) + return 0; + + hash = hashKey(section, key); + index = lookup(hash, query->exp, 0); + + while ((entry = query->slotData[index])) { + if (strcmp(entry->section, section) == 0 && + strcmp(entry->key, key) == 0) { + newValue = stringDup(value); + if (!newValue) + return 0; + free(entry->value); + entry->value = newValue; + return 1; + } + index = lookup(hash, query->exp, index); + } + + entry = &query->entryData[query->size]; + entry->section = stringDup(section); + entry->key = stringDup(key); + entry->value = stringDup(value); + if (!entry->section || !entry->key || !entry->value) { + free(entry->section); + free(entry->key); + free(entry->value); + return 0; + } + + query->size++; + query->slotData[index] = entry; + return 1; +} + +static void populateCb(int type, const char *key, const char *value, + void *user) { + CgeConfQuery *query = (CgeConfQuery *)user; + size_t len; + + if (type == CGE_CONF_SECTION) { + len = strlen(value); + if (len >= sizeof(query->currentSection)) + len = sizeof(query->currentSection) - 1; + memcpy(query->currentSection, value, len); + query->currentSection[len] = 0; + } else if (type == CGE_CONF_KEYVALUE) { + insert(query, query->currentSection, key, value); + } +} + +static CgeConfQuery *queryNew(void) { + CgeConfQuery *query; + + query = (CgeConfQuery *)malloc(sizeof(CgeConfQuery)); + if (!query) + return NULL; + + memset(query, 0, sizeof(*query)); + return query; +} + +CgeConfQuery *CgeConfQueryFromCb(CgeConfIO *io) { + CgeConfQuery *query; + + query = queryNew(); + if (!query) + return NULL; + + if (!CgeConfFromCb(io, populateCb, query)) { + CgeConfQueryFree(query); + return NULL; + } + return query; +} + +CgeConfQuery *CgeConfQueryFromFile(const char *file) { + CgeConfQuery *query; + + query = queryNew(); + if (!query) + return NULL; + + if (!CgeConfFromFile(file, populateCb, query)) { + CgeConfQueryFree(query); + return NULL; + } + return query; +} + +CgeConfQuery *CgeConfQueryFromMemory(const char *buffer, size_t size) { + CgeConfQuery *query; + + query = queryNew(); + if (!query) + return NULL; + + if (!CgeConfFromMemory(buffer, size, populateCb, query)) { + CgeConfQueryFree(query); + return NULL; + } + return query; +} + +CgeConfQuery *CgeConfQueryFromString(const char *str) { + return CgeConfQueryFromMemory(str, strlen(str)); +} + +const char *CgeConfQueryGet(CgeConfQuery *query, const char *section, + const char *key, const char *defaultValue) { + size_t hash, index, i; + Entry *entry; + + if (!query || !key) + return defaultValue; + if (!section) + section = ""; + + hash = hashKey(section, key); + index = lookup(hash, query->exp, 0); + + for (i = 0; i < query->capacity; ++i) { + entry = query->slotData[index]; + if (!entry) + break; + if (strcmp(entry->section, section) == 0 && + strcmp(entry->key, key) == 0) + return entry->value; + index = lookup(hash, query->exp, index); + } + return defaultValue; +} + +void CgeConfQueryFree(CgeConfQuery *query) { + size_t i; + + if (!query) + return; + + for (i = 0; i < query->size; ++i) { + free(query->entryData[i].section); + free(query->entryData[i].key); + free(query->entryData[i].value); + } + free(query->entryData); + free(query->slotData); + free(query); +} + +void CgeConfQueryIter(CgeConfQuery *query, CgeConfQueryIterCb iter, + void *user) { + Entry *entry, *end; + + entry = query->entryData; + end = query->entryData + query->size; + for (; entry != end; ++entry) + iter(entry->section, entry->key, entry->value, user); +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..4c925e6 --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +# CgeArgs - CLI argument parsing + +TODO + +## License + +0BSD - a permissive license with no attribution required.