diff --git a/.clang-format b/.clang-format index 9a51804..75fbcd5 100644 --- a/.clang-format +++ b/.clang-format @@ -16,3 +16,4 @@ AlignConsecutiveAssignments: Enabled: true AcrossEmptyLines: false AcrossComments: false +SortIncludes: true diff --git a/.clang-tidy b/.clang-tidy index 8f5b23d..78543e4 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -3,9 +3,23 @@ Checks: > bugprone-*, misc-*, readability-*, + -readability-implicit-bool-conversion, -readability-magic-numbers, -readability-identifier-length, -misc-unused-parameters, - -misc-include-cleaner + -misc-include-cleaner, + -clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling -HeaderFilterRegex: 'src/.*' +HeaderFilterRegex: '^(src|include|cmd)/.*' + +CheckOptions: + readability-identifier-naming.VariableCase: lower_case + readability-identifier-naming.ParameterCase: lower_case + readability-identifier-naming.FunctionCase: lower_case + readability-identifier-naming.StructCase: CamelCase + readability-identifier-naming.TypedefCase: CamelCase + readability-identifier-naming.EnumCase: CamelCase + readability-identifier-naming.MacroDefinitionCase: UPPER_CASE + readability-identifier-naming.EnumConstantCase: UPPER_CASE + readability-function-cognitive-complexity.Threshold: 25 + readability-function-cognitive-complexity.IgnoreMacros: true \ No newline at end of file diff --git a/.dockerignore b/.dockerignore index 3412e37..97e7839 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,11 +1,17 @@ .DS_Store -*.o -.so -*.dylib -build +*log*.txt +tmp/ +build/ + +# VSCode .vscode/* !.vscode/c_cpp_properties.json +!.vscode/extensions.json !.vscode/launch.json !.vscode/settings.json !.vscode/tasks.json + +# Misc +docs/ + diff --git a/.gitignore b/.gitignore index 5ad959c..97e7839 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,8 @@ .DS_Store *log*.txt -/tmp -/build +tmp/ +build/ # VSCode .vscode/* @@ -12,3 +12,6 @@ !.vscode/settings.json !.vscode/tasks.json +# Misc +docs/ + diff --git a/.vscode/launch.json b/.vscode/launch.json index c392697..8b42ccf 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -2,10 +2,10 @@ "version": "0.2.0", "configurations": [ { - "name": "Debug tl_app_test", + "name": "Debug tl_app", "type": "lldb", "request": "launch", - "program": "${workspaceFolder}/build/bin/tl_app_test", + "program": "${workspaceFolder}/build/tests/bin/tl_app", "args": [], "cwd": "${workspaceFolder}", "preLaunchTask": "CMake: Build" diff --git a/.vscode/settings.json b/.vscode/settings.json index d66d7a0..052b343 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,12 @@ { + "C_Cpp.formatting": "clangFormat", + "C_Cpp.clang_format_style": "file", + "C_Cpp.codeAnalysis.runAutomatically": true, + "C_Cpp.codeAnalysis.clangTidy.enabled": true, + "C_Cpp.files.exclude": { + "**/build": true, + "**/tmp": true + }, "[c]": { "editor.tabSize": 4, "editor.insertSpaces": true, @@ -9,6 +17,9 @@ "[json]": { "editor.defaultFormatter": "vscode.json-language-features" }, + "[jsonc]": { + "editor.defaultFormatter": "vscode.json-language-features" + }, "[markdown]": { "editor.defaultFormatter": "denoland.vscode-deno", "editor.formatOnSave": true @@ -16,6 +27,15 @@ "files.associations": { "*.jsonl": "json", ".clang-format": "yaml", + "*.cmake.in": "cmake" + }, + "files.watcherExclude": { + "**/build/**": true, + "**/tmp/**": true + }, + "search.exclude": { + "**/build": true, + "**/tmp": true }, "cSpell.words": [ "armv", @@ -34,4 +54,7 @@ "trunc", "xcrun" ], + "chat.tools.terminal.autoApprove": { + "mise": true, + } } diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 0323efe..891ef86 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -7,9 +7,8 @@ "command": "cmake", "args": [ "--build", - "${workspaceFolder}/build", - "--config", - "Debug" + "--preset", + "default" ], "group": { "kind": "build", diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..0aea00a --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,255 @@ +# Project Overview + +tinyclib is a tiny C library for building applications across various platforms, +from server-grade hardware to desktops, mobile, and embedded devices. + +## Folder Structure + +- `build/`: Build directory (not committed to git). +- `cmd/`: Command-line tools. +- `docs/`: Project documentation. +- `examples/`: Example code and usage demonstrations. +- `include/`: Public API headers. +- `scripts/`: Utility scripts which are not part of the main source code. +- `src/`: Source code. +- `tests/`: Test files. +- `third_party/`: Third-party libraries and tools. +- `tmp/`: Temporary files and outputs. + +## Project Guidelines + +- Pass extensible boundary structs by pointer, not by value. Use `const` pointer + parameters for input structs and mutable pointer parameters for output + structs. Passing a struct by value bakes the full layout into the call ABI and + defeats the future usefulness of the leading `size` field. +- Update all references across the codebase and documentation when you rename + command arguments, config parameters or any other identifiers. +- When defining C structs, order fields strictly to minimize memory padding and + optimize cache alignment, never purely for narrative readability. Sort fields + from largest to smallest alignment requirements (e.g., 8-byte + pointers/`size_t` first, then 4-byte integers, down to 1-byte + booleans/`char`). If specific fields are frequently accessed together in a hot + path, group them sequentially within those size constraints to maximize + cache-line efficiency. Only apply logical grouping (e.g., inputs vs. outputs) + when it does not introduce invisible padding bytes. +- Do not use cross-file global variables. Use file-local static state only when + necessary. Use opaque context types across multi-file module boundaries, + including tests. Use passed-in context structs for per-instance, per-request, + async, or otherwise explicit state. +- Pay attention to forward declarations when you are using C opaque pointers. +- Keep source code, comments, public headers, examples, and non-plan + documentation project-generic. External consumer names and runtime-specific + integration details belong in plan files only. +- Do not use magic numbers. Every numeric literal used as a size, offset, index, + flag mask, or field position must be a named constant or carry an inline + comment explaining its meaning. Trivially obvious values (0, 1, `'\0'`, + null-terminator arithmetic) are exempt. +- Bare zero values are acceptable for explicit "none" or "unused" semantics, but + they must carry a comment. Use an inline comment when the meaning is short, + and a standalone explanatory comment when the field or control flow needs more + context. +- Do not use scope prefixes like g_ in C. Use descriptive snake_case names. +- Always use uppercase letters for numeric literal suffixes (e.g., 123U, 10L, + 0Z). Never use lowercase (u, l, uz) to prevent visual ambiguity, especially + the dangerous confusion between the letter l and the number 1. Apply this + uniformly to all macros and literal values. +- When generating C callback functions that require error handling and control + flow, always use an explicitly typed enum (enum : int) as the return type to + guarantee ABI stability without relying on out-parameters. Structure the enum + values strictly into three mathematical ranges: assign negative integers (< 0) + for specific, fatal error states (e.g., -1, -2) so the caller can catch all + failures with a single, fast if (result < 0) branch; assign exactly 0 for the + default success or 'continue' state; and assign positive integers (> 0) for + non-error control flow signals like a graceful halt or skipping an element. + This ensures idiomatic, branch-prediction-friendly error propagation where + negative codes can be immediately returned and bubbled up the call stack. +- Prefer status-returning mutating functions when an operation changes state and + may fail. Return `int` for success or failure, and use pointer parameters for + produced values such as indexes or handles. +- Use C "middle style" naming convention for non-static file-level functions + like `[lib]_[file]_[action]_[property]()`. Use C "prefix style" naming + convention for static functions like `[action]_[property]()`. For + boolean-returning functions, use is_/has_/can_ as the action slot. +- For boolean variables and struct fields, prefer concise state names like done, + ready, enabled, or _done. +- Comments in test files should explain what the code can't; the source of a + constant, the meaning of magic bytes, why an odd-looking assertion holds. Do + not restate what names and assertions already say. In test files, prefer an + inline comment over extracting a named constant for magic numbers; keep the + literal at the use site so each test stays self-contained. When refactoring + tests, do not delete existing test comments just because a new test name is + more descriptive. Preserve existing comments unless the user explicitly asks + for their removal, or update them in place when the code change makes them + inaccurate. +- When generating or modifying code comments, act as a procedural outlier. Your + goal is to allow a developer to understand the high-level execution flow + simply by reading the comments from top to bottom. Actively document each + logical step and structural block using short, direct statements (e.g., + "Resolve mount and relative path", "Determine file type"). It is acceptable to + describe the immediate action being taken, provided it acts as a clear section + header that prevents the reader from needing to parse the underlying C syntax. +- When generating or modifying code comments, you must determine capitalization + based on placement and punctuation based on grammatical structure. If a + comment sits on its own line, it must begin with an uppercase letter. If a + comment is placed inline, directly next to code on the same line, it must + begin with a lowercase letter. You must completely omit terminal periods for + all inline comments, short imperative commands, fragments, and noun phrases + (e.g., `// Initialize the database` or + `let x = 0; // initialize the database`). You may only use a terminal period + if a standalone comment is a true, formal full sentence containing both a + distinct subject and a verb. +- Use Doxygen-style documentation comments (`/** ... */`) with tags like @brief, + @param, @return, etc. for all public functions, important internal functions + and macros. Add a new line after each tag group. Use `//` for inline comments. + Do not use comment dividers (`//---...`), or section headers + (`// === ... ===`). +- Start function `@brief` text with a brief third-person singular present-tense + verb, such as "Returns ...", "Handles ...", "Sets ...", "Adds ...` and so on. +- Place documentation comments where the entity is declared, not duplicated + across files. If a function, struct or macro is declared in a header file, its + Doxygen comment lives in the header and the implementation file carries no + repeated block. If the entity has no header declaration, such as a static + function or a file-local struct, its comment lives in the implementation file + where it is defined. The goal is a single source of truth per entity so + comments do not drift apart over time. +- Plan files must begin with `# `, `## Overview`, `## Scope` and + `## Out of Scope` sections, in that order, so a reader can understand what the + plan is about and what it deliberately excludes without reading the whole + document. If the plan has unresolved design questions, follow those three + sections with an `## Open Questions` section. And add a `## Recommendations` + section before any other content +- Do not suppress formatter, linter and checker warnings without asking first. +- Do not remove tests without asking first. Do not cause regressions in existing + tests without asking first. +- When you are creating CMakeLists.txt files for new folders, follow the + existing patterns, structure and naming conventions. +- Do not create common or utility files without asking first. +- Do not add `_internal`, `_private`, `_impl`, or similar suffixes to files, + types, or functions - the directory already carries that signal. Prefer + specific, descriptive filenames (e.g. `client.c`, `request.c`) over generic + ones (`async.c`, `common.c`). +- Stay consistent with existing similar files. Before adding or modifying a + file, look at its siblings and peers to mirror their directory layout, file + organization, section ordering, include/header grouping, naming conventions, + comment style, struct and function shapes, error handling patterns and test + scaffolding. Prefer matching the established pattern over introducing a new + one, even when the new approach seems marginally better. If the existing + pattern is genuinely wrong or no longer fits, ask before diverging so the + inconsistency is intentional and applied across the affected files together. +- Place unit and internal module tests in `tests/unit/`, public API tests in + `tests/api/` and integration tests in `tests/integration/`. Name unit and + internal test files after the module they test using `test_<module>.c`, where + `<module>` matches the source or private header name without the project + prefix. If an internal test covers multiple private modules, use + `test_core_<feature>.c`. For public API tests, use `test_api_<header>.c`, + where `<header>` is the public header name without the project prefix and + without the `.h` suffix. Do not use generic names like `test_utils.c` or + `test_common.c`. Do not mix public API behavior tests and private/internal + module tests in the same file. +- All C tests must use the Unity framework (`unity.h`, `setUp`, `tearDown`, + `UNITY_BEGIN`, `RUN_TEST`, and `UNITY_END`) and be wired through the existing + CMake test target list patterns. +- When writing C test function names, use established pattern structures such as + `test_<function>_<scenario>` for simple utilities, + `test_<function>_<scenario>_<outcome>` for complex algorithms, or + `test_<state>_<action>_<outcome>` for state machines. Within your test file, + always group by target function by clustering all tests for a single function + together to easily spot coverage gaps. Additionally, avoid generic scenario + names like `_normal_case` or `_works` by specifying exactly what makes the + input valid, and remember to test static functions indirectly by evaluating + the public API functions that trigger their logic rather than calling them + directly. +- Known Issues and Bugs: There is a line between "known issues" and "bugs". + Known issues are things that are not ideal but are acceptable for the current + scope and requirements. Bugs are on the other hand unacceptable regardless of + scope. Always add comments and documentation for known issues. Always create a + markdown file in the `docs/bugs` directory for each bug with a description of + the issue. Do not ignore or silently move on from bugs. +- Avoid regressions in existing functionality without asking first.Do not remove + or change a test just to make it pass without asking first. +- **IMPORTANT!** Avoid implicit behavior, "magic" and silently ignoring errors. + Always prefer to report errors, warnings and unsupported use cases. +- **IMPORTANT!** Always ask before making any significant changes related to + performance and public API. +- **IMPORTANT** Do not pollute the project root with temporary files. Use the + project `tmp/` folder for all temporary files and outputs. +- **IMPORTANT!** This is a C project. Do not add any other languages or + dependencies without asking first. +- **IMPORTANT** Do not clean build folder without asking first! Using + `mise exec -- make configure` and `mise exec -- make build` is much faster for + iterative development. Use `mise exec -- make clean` only when you know what + you are doing and need to clean all the generated files. +- Keep tinyclib changes small. Prefer additive public API changes when possible + and call out any breaking public API change before implementing it. + +## Quick Reference + +- `make help`: Show available make targets +- `make configure`: Configure cmake +- `make build`: Build the project +- `make clean`: Remove temporary directories +- `make test`: Run tests +- `make format`: Check code formatting +- `make lint`: Check code linting +- `make check`: Check types +- `make check-all`: Run all checks +- `make fix`: Fix code formatting and linting issues +- `mise exec -- [COMMAND]`: Run a command with the project environment. This + ensures the correct toolchain (cmake, clang, etc.) is used. For example + `mise exec -- cmake`, `mise exec -- make test`. +- `eval "$(mise activate [SHELL_TYPE])"`: Initializes mise in the current shell + session. For example `eval "$(mise activate bash)"`. + +## References + +- [Unity](https://github.com/ThrowTheSwitch/Unity.git) See `tmp/unity` folder + for a local copy of the source and documentation. + +## Design Notes + +### Reviewability Boundaries + +For reviewability, keep capability boundaries small, boring and contractual. +Prefer files that own one stable concept over large files that gather unrelated +operations only because they share a handle or backend. A good boundary lets +stable code stay unchanged while related new work is added in nearby focused +files. + +- Split public headers and source files by consumer-facing capability. +- Keep high-churn features behind narrow headers, narrow source files and + focused tests so reviewers can understand a change without loading unrelated + behavior into memory. +- Add or maintain compile and behavior tests at each boundary so accidental + coupling, broad includes and behavior regressions are caught quickly. +- Avoid catch-all files and directories. Use specific names that describe the + contract they own, and add new files beside stable contracts when a feature + has a distinct consumer or change pattern. +- Treat mature files as contracts. Once a module has clear invariants and good + coverage, prefer extending through adjacent modules rather than repeatedly + reopening the stable core. + +### Abstraction Boundary + +The public API (`include/` and `src/`) uses the project's own type and function +names, not the underlying libraries' names. + +This boundary applies to the public API surface only. Commands (`cmd/`) and +examples (`examples/`) are free to use third-party libraries directly like a +normal C project. + +### Third-Party code patches + +When you patch a third-party code, add `.patch` file for each patch and `.md` +file matching the patch name with a content like (similar to "Common Changelog" +categories): + +```markdown +# <file_name>.patch + +- **Changed** ... +- **Added** ... +- **Removed** ... +- **Fixed** ... +``` + +Keep it simple, do not add unnecessary details. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..43c994c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/Makefile b/Makefile index f4f0ba5..9d35db0 100644 --- a/Makefile +++ b/Makefile @@ -9,6 +9,7 @@ SRC_FILES := src/*.c include/*.h TEST_FILES := tests/unit/*.c CMD_FILES := $(wildcard cmd/*/*.c cmd/*/*.h) ALL_FILES := $(SRC_FILES) $(TEST_FILES) $(CMD_FILES) +LINT_FILES := $(shell find src tests -type f -name '*.c') PRESET ?= default JOBS ?= 4 @@ -55,16 +56,23 @@ format: ## Check code formatting lint: ## Check code linting @test -n "$(CLANG_TIDY)" || { echo "error: clang-tidy not found"; exit 1; } - @test -f "$(PRESET_BUILD_DIR)/compile_commands.json" || cmake --preset default - $(CLANG_TIDY) --config-file=$(PROJECT_DIR)/.clang-tidy -p $(PRESET_BUILD_DIR) $(CLANG_TIDY_EXTRA_ARGS) \ - --header-filter="^$(PROJECT_DIR)/(src|include|tests)/" src/*.c tests/unit/*.c + @test -f "$(PROJECT_DIR)/$(PRESET_BUILD_DIR)/compile_commands.json" || cmake --preset $(PRESET) + @$(CLANG_TIDY) \ + --config-file=$(PROJECT_DIR)/.clang-tidy \ + -p $(PROJECT_DIR)/$(PRESET_BUILD_DIR) \ + $(CLANG_TIDY_EXTRA_ARGS) \ + --warnings-as-errors='*' \ + --quiet \ + --header-filter="^$(PROJECT_DIR)/(src|include|cmd|tests)/" \ + --exclude-header-filter="^$(PROJECT_DIR)/$(BUILD_ROOT)/" \ + $(LINT_FILES) check: ## Static analysis @test -n "$(CPPCHECK)" || { echo "error: cppcheck not found"; exit 1; } - @test -f "$(PRESET_BUILD_DIR)/compile_commands.json" || cmake --preset default + @test -f "$(PROJECT_DIR)/$(PRESET_BUILD_DIR)/compile_commands.json" || cmake --preset $(PRESET) $(CPPCHECK) --enable=warning,style,performance,portability --error-exitcode=1 \ - --check-level=exhaustive --project=$(PRESET_BUILD_DIR)/compile_commands.json \ - --suppress=missingIncludeSystem -i$(PRESET_BUILD_DIR) + --check-level=exhaustive --project=$(PROJECT_DIR)/$(PRESET_BUILD_DIR)/compile_commands.json \ + --suppress=missingIncludeSystem -i$(PROJECT_DIR)/$(PRESET_BUILD_DIR) check-all: test format lint check ## Run all checks diff --git a/include/tl_flag.h b/include/tl_flag.h index e752d0d..4a50f37 100644 --- a/include/tl_flag.h +++ b/include/tl_flag.h @@ -6,12 +6,17 @@ #include <stdbool.h> #include <stddef.h> +/** + * @brief Represents a not-found result for index returning functions. + */ +#define TL_ARG_NOT_FOUND ((size_t)-1) + /** * @brief Represents a parsed flag. * * `name` points into argv (or the tokenizer buffer) at the first '-' of * the flag. `name_len` is the length up to '\0' or '='. For the '=' form - * `name` is NOT a NUL-terminated C string at name_len — name[name_len] is + * `name` is NOT a NUL-terminated C string at name_len: name[name_len] is * '=', so comparisons must use memcmp with name_len, never strcmp. * * argv entry: "--foo=bar" @@ -39,7 +44,89 @@ typedef struct { const char *name; // points at the first '-' of the flag in argv size_t name_len; // length of the flag name up to '\0' or '=' const char *value; // value after first '=', or NULL if none -} tl_flag_t; +} TlFlag; + +/** + * @brief Represents options for argument parsing. + */ +typedef struct { + const char *const *value_flags; + const char *const *bool_flags; +} TlParseOptions; + +/** + * @brief Defines argument parser result codes. + */ +typedef enum { + TL_PARSE_OK = 0, + TL_PARSE_ERROR_INVALID_RANGE = -1, + TL_PARSE_ERROR_UNKNOWN_FLAG = -2, + TL_PARSE_ERROR_MISSING_VALUE = -3, + TL_PARSE_ERROR_CONFLICTING_FLAG = -4, + TL_PARSE_ERROR_MEMORY_ALLOCATION = -5, + TL_PARSE_ERROR_INVALID_INPUT = -6, + TL_PARSE_ERROR_UNTERMINATED_QUOTE = -7, +} TlParseResult; + +/** + * @brief Returns the first argv index matching the given name. + * + * Searches argv from index 0. + * + * @param argc The number of command line arguments. + * @param argv The command line arguments. + * @param name The exact argument name to find. + * + * @return The argv index, or TL_ARG_NOT_FOUND when not found. + */ +size_t tl_arg_index(int argc, char *argv[], const char *name); + +/** + * @brief Returns the first argv index matching the given name after an index. + * + * Searches argv starting after index. + * + * @param argc The number of command line arguments. + * @param argv The command line arguments. + * @param name The exact argument name to find. + * @param index The argv index to search after. + * + * @return The argv index, or TL_ARG_NOT_FOUND when not found. + */ +size_t tl_arg_index_after(int argc, char *argv[], const char *name, size_t index); + +/** + * @brief Parses the given command line arguments with options. + * + * Default parsing is used when options is NULL or both option lists are NULL. + * With default parsing this behaves like tl_parse_args. Strict mode is enabled + * when either option list is non-NULL. + * + * @param argc The number of command line arguments. + * @param argv The command line arguments. + * @param options The parse options, or NULL for default parsing. + * + * @return TL_PARSE_OK on success, or a negative parse error. + */ +TlParseResult tl_parse_args_ex(int argc, char *argv[], const TlParseOptions *options); + +/** + * @brief Parses an explicit argv range. + * + * Parses argv indexes [start_index, end_index). argv[0] has no special meaning + * for this function. The parsed positional indexes are relative to the selected + * range. + * + * @param argc The number of command line arguments. + * @param argv The command line arguments. + * @param start_index The first argv index to parse. + * @param end_index The argv index one past the last token to parse. + * @param options The parse options, or NULL for default parsing. + * + * @return TL_PARSE_OK on success, or a negative parse error. + */ +TlParseResult tl_parse_args_range(int argc, char *argv[], size_t start_index, size_t end_index, + const TlParseOptions *options); /** * @brief Parses the given command line arguments. @@ -54,9 +141,9 @@ typedef struct { * @param argc The number of command line arguments. * @param argv The command line arguments. * - * @return void + * @return TL_PARSE_OK on success, or a negative parse error. */ -void tl_parse_args(int argc, char *argv[]); +TlParseResult tl_parse_args(int argc, char *argv[]); /** * @brief Parses a raw command line string. @@ -67,9 +154,9 @@ void tl_parse_args(int argc, char *argv[]); * * @param line The command line string to parse. * - * @return true on success, false otherwise. + * @return TL_PARSE_OK on success, or a negative parse error. */ -bool tl_parse_line(const char *line); +TlParseResult tl_parse_line(const char *line); /** * @brief Releases memory held by the argument parser. diff --git a/src/tl_flag.c b/src/tl_flag.c index 2cc9038..a75278f 100644 --- a/src/tl_flag.c +++ b/src/tl_flag.c @@ -5,7 +5,7 @@ #include <string.h> // Init vars -static tl_flag_t *flags = NULL; +static TlFlag *flags = NULL; static size_t flag_count = 0; static const char **positionals = NULL; static size_t positional_count = 0; @@ -51,33 +51,170 @@ static bool is_dash_dash(const char *s) { /** * @brief Returns whether a parsed flag matches the given name. */ -static bool flag_matches(const tl_flag_t *f, const char *name, size_t name_len) { +static bool is_flag_match(const TlFlag *f, const char *name, size_t name_len) { if (f->name_len != name_len) { return false; } return memcmp(f->name, name, name_len) == 0; } +/** + * @brief Returns whether the argv token matches a name. + */ +static bool has_token_match(const char *const argv[], size_t index, const char *name) { + return argv[index] && strcmp(argv[index], name) == 0; +} + +/** + * @brief Returns whether the flag name appears in a NULL-terminated list. + */ +static bool has_flag_name(const char *const *names, const char *name, size_t name_len) { + if (!names || !name) { + return false; + } + for (size_t i = 0; names[i]; i++) { + size_t current_len = strlen(names[i]); + if (current_len == name_len && memcmp(names[i], name, name_len) == 0) { + return true; + } + } + return false; +} + +/** + * @brief Returns whether parser options require strict flag matching. + */ +static bool has_strict_flag_lists(const TlParseOptions *options) { + if (!options) { + return false; + } + return options->value_flags || options->bool_flags; +} + +/** + * @brief Returns whether any flag appears in both strict flag lists. + */ +static bool has_conflicting_flags(const TlParseOptions *options) { + if (!options || !options->value_flags || !options->bool_flags) { + return false; + } + for (size_t i = 0; options->value_flags[i]; i++) { + size_t value_len = strlen(options->value_flags[i]); + if (has_flag_name(options->bool_flags, options->value_flags[i], value_len)) { + return true; + } + } + return false; +} + +/** + * @brief Adds a parsed flag entry. + */ +static void add_flag(const char *name, size_t name_len, const char *value) { + flags[flag_count].name = name; + flags[flag_count].name_len = name_len; + flags[flag_count].value = value; + flag_count++; +} + +/** + * @brief Returns the default parsing value for a space-form flag. + */ +static const char *take_default_flag_value(char **tokens, size_t *index, size_t end_index) { + size_t next_index = *index + 1; + if (next_index >= end_index) { + return NULL; + } + if (is_flag(tokens[next_index]) || is_dash_dash(tokens[next_index])) { + return NULL; + } + *index = next_index; + return tokens[next_index]; +} + +/** + * @brief Takes the required strict-mode value for a value flag. + */ +static TlParseResult take_strict_flag_value(char **tokens, size_t *index, size_t end_index, + const char **value) { + size_t next_index = *index + 1; + if (next_index >= end_index) { + return TL_PARSE_ERROR_MISSING_VALUE; + } + if (is_flag(tokens[next_index]) || is_dash_dash(tokens[next_index])) { + return TL_PARSE_ERROR_MISSING_VALUE; + } + *index = next_index; + *value = tokens[next_index]; + return TL_PARSE_OK; +} + +/** + * @brief Parses one flag token into the flag table. + */ +static TlParseResult parse_flag_token(char **tokens, size_t *index, size_t end_index, + const TlParseOptions *options, bool strict) { + const char *tok = tokens[*index]; + const char *eq = strchr(tok, '='); + size_t name_len = eq ? (size_t)(eq - tok) : strlen(tok); + bool value_flag = false; + + if (strict) { + value_flag = has_flag_name(options->value_flags, tok, name_len); + if (!value_flag && !has_flag_name(options->bool_flags, tok, name_len)) { + return TL_PARSE_ERROR_UNKNOWN_FLAG; + } + } + + if (eq) { + add_flag(tok, name_len, eq + 1); + return TL_PARSE_OK; + } + + const char *value = NULL; + if (strict && value_flag) { + TlParseResult result = take_strict_flag_value(tokens, index, end_index, &value); + if (result != TL_PARSE_OK) { + return result; + } + } + if (!strict) { + value = take_default_flag_value(tokens, index, end_index); + } + + add_flag(tok, name_len, value); + return TL_PARSE_OK; +} + /** * @brief Fills the flag and positional tables from a token list. * - * The first token is the program name and is skipped. The rest are - * sorted into flags (anything starting with "-" or "--") and positionals - * (everything else, plus anything after a bare "--"). + * Tokens in [start_index, end_index) are sorted into flags (anything starting + * with "-" or "--") and positionals (everything else, plus anything after a + * bare "--"). */ -static bool parse_tokens(char **tokens, int count) { - if (count <= 1 || !tokens) { - return true; +static TlParseResult parse_token_range(char **tokens, size_t start_index, size_t end_index, + const TlParseOptions *options) { + if (start_index == end_index) { + return TL_PARSE_OK; + } + if (!tokens) { + return TL_PARSE_ERROR_INVALID_RANGE; + } + if (has_conflicting_flags(options)) { + return TL_PARSE_ERROR_CONFLICTING_FLAG; } - flags = (tl_flag_t *)calloc((size_t)count, sizeof(*flags)); - positionals = (const char **)calloc((size_t)count, sizeof(*positionals)); + size_t count = end_index - start_index; + flags = (TlFlag *)calloc(count, sizeof(*flags)); + positionals = (const char **)calloc(count, sizeof(*positionals)); if (!flags || !positionals) { - return false; + return TL_PARSE_ERROR_MEMORY_ALLOCATION; } bool after_dd = false; - for (int i = 1; i < count; i++) { + bool strict = has_strict_flag_lists(options); + for (size_t i = start_index; i < end_index; i++) { char *tok = tokens[i]; if (!tok) { continue; @@ -94,39 +231,24 @@ static bool parse_tokens(char **tokens, int count) { } // Flag if (is_flag(tok)) { - char *eq = strchr(tok, '='); - if (eq) { - flags[flag_count].name = tok; - flags[flag_count].name_len = (size_t)(eq - tok); - flags[flag_count].value = eq + 1; - } else { - const char *value = NULL; - // Consume the next token as the value if it is not another flag - // and not the "--" terminator - if (i + 1 < count && !is_flag(tokens[i + 1]) && !is_dash_dash(tokens[i + 1])) { - value = tokens[i + 1]; - i++; - } - flags[flag_count].name = tok; - flags[flag_count].name_len = strlen(tok); - flags[flag_count].value = value; + TlParseResult result = parse_flag_token(tokens, &i, end_index, options, strict); + if (result != TL_PARSE_OK) { + return result; } - flag_count++; continue; } // Positional positionals[positional_count++] = tok; } - return true; + return TL_PARSE_OK; } /** * @brief Reads one token from `line` starting at `*i` into `line_buf` at `*bi`. * * Stops at unquoted whitespace or end of line. Writes the NUL terminator. - * Returns true on success, false if a quoted string was never closed. */ -static bool read_one_token(const char *line, size_t len, size_t *i, size_t *bi) { +static TlParseResult read_one_token(const char *line, size_t len, size_t *i, size_t *bi) { bool in_quote = false; while (*i < len) { char c = line[*i]; @@ -151,10 +273,10 @@ static bool read_one_token(const char *line, size_t len, size_t *i, size_t *bi) (*i)++; } if (in_quote) { - return false; + return TL_PARSE_ERROR_UNTERMINATED_QUOTE; } line_buf[(*bi)++] = '\0'; - return true; + return TL_PARSE_OK; } /** @@ -162,21 +284,24 @@ static bool read_one_token(const char *line, size_t len, size_t *i, size_t *bi) * * Text inside double quotes is kept as one token, spaces and all. The * quote characters themselves are dropped. A backslash keeps the next - * character as-is (e.g. \" or \\). Returns the token count on success, - * or -1 if malloc fails or a quote is never closed. + * character as-is (e.g. \" or \\). */ -static int tokenize_line(const char *line) { +static TlParseResult tokenize_line(const char *line, size_t *token_count) { + if (!line || !token_count) { + return TL_PARSE_ERROR_INVALID_INPUT; + } + size_t len = strlen(line); line_buf = malloc(len + 1); if (!line_buf) { - return -1; + return TL_PARSE_ERROR_MEMORY_ALLOCATION; } // Upper bound: one token per two bytes, plus a slot for the "argv[0]" entry size_t tok_cap = (len / 2) + 2; line_tokens = (char **)malloc(tok_cap * sizeof(*line_tokens)); if (!line_tokens) { - return -1; + return TL_PARSE_ERROR_MEMORY_ALLOCATION; } size_t n = 0; @@ -190,36 +315,96 @@ static int tokenize_line(const char *line) { if (i >= len) { break; } - line_tokens[n++] = &line_buf[bi]; - if (!read_one_token(line, len, &i, &bi)) { - return -1; + line_tokens[n++] = &line_buf[bi]; + TlParseResult result = read_one_token(line, len, &i, &bi); + if (result != TL_PARSE_OK) { + return result; } } - return (int)n; + *token_count = n; + return TL_PARSE_OK; } -void tl_parse_args(int argc, char *argv[]) { +size_t tl_arg_index(int argc, char *argv[], const char *name) { + if (argc <= 0 || !argv || !name) { + return TL_ARG_NOT_FOUND; + } + for (int i = 0; i < argc; i++) { + if (has_token_match((const char *const *)argv, (size_t)i, name)) { + return (size_t)i; + } + } + return TL_ARG_NOT_FOUND; +} + +size_t tl_arg_index_after(int argc, char *argv[], const char *name, size_t index) { + if (index == TL_ARG_NOT_FOUND || argc <= 0 || !argv || !name) { + return TL_ARG_NOT_FOUND; + } + size_t start = index + 1; + if (start >= (size_t)argc) { + return TL_ARG_NOT_FOUND; + } + for (size_t i = start; i < (size_t)argc; i++) { + if (has_token_match((const char *const *)argv, i, name)) { + return i; + } + } + return TL_ARG_NOT_FOUND; +} + +TlParseResult tl_parse_args_ex(int argc, char *argv[], const TlParseOptions *options) { tl_free_args(); - if (!parse_tokens(argv, argc)) { + if (argc <= 1 || !argv) { + return TL_PARSE_OK; + } + TlParseResult result = parse_token_range(argv, 1, (size_t)argc, options); + if (result != TL_PARSE_OK) { tl_free_args(); } + return result; } -bool tl_parse_line(const char *line) { +TlParseResult tl_parse_args_range(int argc, char *argv[], size_t start_index, size_t end_index, + const TlParseOptions *options) { + tl_free_args(); + if (argc < 0 || start_index == TL_ARG_NOT_FOUND || end_index == TL_ARG_NOT_FOUND || + start_index > end_index || start_index > (size_t)argc || end_index > (size_t)argc) { + return TL_PARSE_ERROR_INVALID_RANGE; + } + if (start_index == end_index) { + return TL_PARSE_OK; + } + TlParseResult result = parse_token_range(argv, start_index, end_index, options); + if (result != TL_PARSE_OK) { + tl_free_args(); + } + return result; +} + +TlParseResult tl_parse_args(int argc, char *argv[]) { + return tl_parse_args_ex(argc, argv, NULL); +} + +TlParseResult tl_parse_line(const char *line) { tl_free_args(); if (!line) { - return false; + return TL_PARSE_ERROR_INVALID_INPUT; } - int n = tokenize_line(line); - if (n < 0) { + size_t n = 0; + TlParseResult result = tokenize_line(line, &n); + if (result != TL_PARSE_OK) { tl_free_args(); - return false; + return result; + } + if (n <= 1) { + return TL_PARSE_OK; } - if (!parse_tokens(line_tokens, n)) { + result = parse_token_range(line_tokens, 1, n, NULL); + if (result != TL_PARSE_OK) { tl_free_args(); - return false; } - return true; + return result; } void tl_free_args(void) { @@ -249,7 +434,7 @@ bool tl_lookup_flag(const char *flag) { } size_t flen = strlen(flag); for (size_t i = 0; i < flag_count; i++) { - if (flag_matches(&flags[i], flag, flen)) { + if (is_flag_match(&flags[i], flag, flen)) { return true; } } @@ -267,7 +452,7 @@ size_t tl_count_flag(const char *flag) { size_t flen = strlen(flag); size_t n = 0; for (size_t i = 0; i < flag_count; i++) { - if (flag_matches(&flags[i], flag, flen)) { + if (is_flag_match(&flags[i], flag, flen)) { n++; } } @@ -281,7 +466,7 @@ const char *tl_get_flag_at(const char *flag, size_t index) { size_t flen = strlen(flag); size_t k = 0; for (size_t i = 0; i < flag_count; i++) { - if (flag_matches(&flags[i], flag, flen)) { + if (is_flag_match(&flags[i], flag, flen)) { if (k == index) { return flags[i].value; } diff --git a/tests/unit/flag_test.c b/tests/unit/flag_test.c index 112f4de..2882b6b 100644 --- a/tests/unit/flag_test.c +++ b/tests/unit/flag_test.c @@ -11,7 +11,7 @@ void tearDown(void) { static void test_tl_parse_args(void) { char *argv[] = {"program", "--test-flag"}; - tl_parse_args(2, argv); + TEST_ASSERT_EQUAL_INT(TL_PARSE_OK, tl_parse_args(2, argv)); TEST_ASSERT_TRUE(tl_lookup_flag("--test-flag")); } @@ -117,17 +117,18 @@ static void test_tl_quoted_value_from_argv(void) { } static void test_tl_parse_line_quoted(void) { - TEST_ASSERT_TRUE(tl_parse_line("program --foo \"bar baz qux\"")); + TEST_ASSERT_EQUAL_INT(TL_PARSE_OK, tl_parse_line("program --foo \"bar baz qux\"")); TEST_ASSERT_EQUAL_STRING("bar baz qux", tl_get_flag("--foo")); } static void test_tl_parse_line_escape(void) { - TEST_ASSERT_TRUE(tl_parse_line("program --foo \"a\\\"b\"")); + TEST_ASSERT_EQUAL_INT(TL_PARSE_OK, tl_parse_line("program --foo \"a\\\"b\"")); TEST_ASSERT_EQUAL_STRING("a\"b", tl_get_flag("--foo")); } static void test_tl_parse_line_full(void) { - TEST_ASSERT_TRUE(tl_parse_line("prog cmd --foo=1 --foo 2 --bar \"x y\" -- --baz")); + TEST_ASSERT_EQUAL_INT(TL_PARSE_OK, + tl_parse_line("prog cmd --foo=1 --foo 2 --bar \"x y\" -- --baz")); TEST_ASSERT_EQUAL_UINT(2, tl_count_flag("--foo")); TEST_ASSERT_EQUAL_STRING("1", tl_get_flag_at("--foo", 0)); TEST_ASSERT_EQUAL_STRING("2", tl_get_flag_at("--foo", 1)); @@ -138,7 +139,12 @@ static void test_tl_parse_line_full(void) { } static void test_tl_parse_line_unterminated(void) { - TEST_ASSERT_FALSE(tl_parse_line("program --foo \"unterminated")); + TEST_ASSERT_EQUAL_INT(TL_PARSE_ERROR_UNTERMINATED_QUOTE, + tl_parse_line("program --foo \"unterminated")); +} + +static void test_tl_parse_line_null(void) { + TEST_ASSERT_EQUAL_INT(TL_PARSE_ERROR_INVALID_INPUT, tl_parse_line(NULL)); } static void test_tl_short_flag(void) { @@ -286,7 +292,8 @@ static void test_tl_positional_dashdash_only(void) { } static void test_tl_parse_line_quoted_positional(void) { - TEST_ASSERT_TRUE(tl_parse_line("prog \"first pos\" --flag v -- \"after dd\" plain")); + TEST_ASSERT_EQUAL_INT(TL_PARSE_OK, + tl_parse_line("prog \"first pos\" --flag v -- \"after dd\" plain")); TEST_ASSERT_EQUAL_STRING("v", tl_get_flag("--flag")); TEST_ASSERT_EQUAL_UINT(3, tl_count_positional()); TEST_ASSERT_EQUAL_STRING("first pos", tl_get_positional(0)); @@ -352,7 +359,7 @@ static void test_tl_null_flag_argument(void) { static void test_tl_empty_argv(void) { char *argv[] = {"program"}; - tl_parse_args(1, argv); + TEST_ASSERT_EQUAL_INT(TL_PARSE_OK, tl_parse_args(1, argv)); TEST_ASSERT_FALSE(tl_lookup_flag("--anything")); TEST_ASSERT_NULL(tl_get_flag("--anything")); TEST_ASSERT_EQUAL_UINT(0, tl_count_flag("--anything")); @@ -378,7 +385,7 @@ static void test_tl_reparse_line_after_args(void) { tl_parse_args(2, argv); TEST_ASSERT_EQUAL_STRING("1", tl_get_flag("--first")); - TEST_ASSERT_TRUE(tl_parse_line("program --second=2")); + TEST_ASSERT_EQUAL_INT(TL_PARSE_OK, tl_parse_line("program --second=2")); TEST_ASSERT_FALSE(tl_lookup_flag("--first")); TEST_ASSERT_EQUAL_STRING("2", tl_get_flag("--second")); } @@ -402,45 +409,45 @@ static void test_tl_count_flag_absent(void) { } static void test_tl_parse_line_empty(void) { - TEST_ASSERT_TRUE(tl_parse_line("")); + TEST_ASSERT_EQUAL_INT(TL_PARSE_OK, tl_parse_line("")); TEST_ASSERT_EQUAL_UINT(0, tl_count_positional()); } static void test_tl_parse_line_whitespace_only(void) { - TEST_ASSERT_TRUE(tl_parse_line(" \t ")); + TEST_ASSERT_EQUAL_INT(TL_PARSE_OK, tl_parse_line(" \t ")); TEST_ASSERT_EQUAL_UINT(0, tl_count_positional()); } static void test_tl_parse_line_program_only(void) { - TEST_ASSERT_TRUE(tl_parse_line("program")); + TEST_ASSERT_EQUAL_INT(TL_PARSE_OK, tl_parse_line("program")); TEST_ASSERT_EQUAL_UINT(0, tl_count_positional()); TEST_ASSERT_FALSE(tl_lookup_flag("--anything")); } static void test_tl_parse_line_multiple_spaces(void) { - TEST_ASSERT_TRUE(tl_parse_line("prog --foo=1\t\t--bar val")); + TEST_ASSERT_EQUAL_INT(TL_PARSE_OK, tl_parse_line("prog --foo=1\t\t--bar val")); TEST_ASSERT_EQUAL_STRING("1", tl_get_flag("--foo")); TEST_ASSERT_EQUAL_STRING("val", tl_get_flag("--bar")); } static void test_tl_parse_line_empty_quoted_value(void) { - TEST_ASSERT_TRUE(tl_parse_line("program --foo \"\"")); + TEST_ASSERT_EQUAL_INT(TL_PARSE_OK, tl_parse_line("program --foo \"\"")); TEST_ASSERT_TRUE(tl_lookup_flag("--foo")); TEST_ASSERT_EQUAL_STRING("", tl_get_flag("--foo")); } static void test_tl_parse_line_escaped_space(void) { - TEST_ASSERT_TRUE(tl_parse_line("program --foo bar\\ baz")); + TEST_ASSERT_EQUAL_INT(TL_PARSE_OK, tl_parse_line("program --foo bar\\ baz")); TEST_ASSERT_EQUAL_STRING("bar baz", tl_get_flag("--foo")); } static void test_tl_parse_line_trailing_backslash(void) { - TEST_ASSERT_TRUE(tl_parse_line("program --foo bar\\")); + TEST_ASSERT_EQUAL_INT(TL_PARSE_OK, tl_parse_line("program --foo bar\\")); TEST_ASSERT_EQUAL_STRING("bar\\", tl_get_flag("--foo")); } static void test_tl_parse_line_double_backslash(void) { - TEST_ASSERT_TRUE(tl_parse_line("program --foo \"a\\\\b\"")); + TEST_ASSERT_EQUAL_INT(TL_PARSE_OK, tl_parse_line("program --foo \"a\\\\b\"")); TEST_ASSERT_EQUAL_STRING("a\\b", tl_get_flag("--foo")); } @@ -458,12 +465,215 @@ static void test_tl_negative_number_value(void) { TEST_ASSERT_EQUAL_STRING("-5", tl_get_flag("--count")); } +static void test_tl_arg_index_returns_argv_index(void) { + char *argv[] = {"program", "--global", "command", "subcommand"}; + TEST_ASSERT_EQUAL_UINT(2, tl_arg_index(4, argv, "command")); + TEST_ASSERT_EQUAL_UINT(3, tl_arg_index(4, argv, "subcommand")); + TEST_ASSERT_EQUAL_UINT(TL_ARG_NOT_FOUND, tl_arg_index(4, argv, "missing")); +} + +static void test_tl_arg_index_after_starts_after_given_index(void) { + char *argv[] = {"program", "command", "command", "subcommand"}; + size_t command_index = tl_arg_index(4, argv, "command"); + TEST_ASSERT_EQUAL_UINT(2, tl_arg_index_after(4, argv, "command", command_index)); + TEST_ASSERT_EQUAL_UINT(3, tl_arg_index_after(4, argv, "subcommand", command_index)); +} + +static void test_tl_arg_index_after_missing_name_returns_not_found(void) { + char *argv[] = {"program", "command"}; + TEST_ASSERT_EQUAL_UINT(TL_ARG_NOT_FOUND, + tl_arg_index_after(2, argv, "command", TL_ARG_NOT_FOUND)); + TEST_ASSERT_EQUAL_UINT(TL_ARG_NOT_FOUND, tl_arg_index_after(2, argv, "missing", 1)); +} + +static void test_tl_flag_public_type_accepts_tl_flag(void) { + TlFlag flag = {.name = "--flag-bool", .name_len = 11, .value = NULL}; + + TEST_ASSERT_EQUAL_STRING("--flag-bool", flag.name); + TEST_ASSERT_EQUAL_UINT(11, flag.name_len); + TEST_ASSERT_NULL(flag.value); +} + +static void test_tl_parse_args_ex_null_options_uses_default_parsing(void) { + char *argv[] = {"program", "--flag-bool", "./tmp/example"}; + TEST_ASSERT_EQUAL_INT(TL_PARSE_OK, tl_parse_args_ex(3, argv, NULL)); + TEST_ASSERT_EQUAL_STRING("./tmp/example", tl_get_flag("--flag-bool")); + TEST_ASSERT_EQUAL_UINT(0, tl_count_positional()); +} + +static void test_tl_parse_args_ex_empty_options_uses_default_parsing(void) { + char *argv[] = {"program", "--flag-bool", "./tmp/example"}; + TlParseOptions options = {.value_flags = NULL, .bool_flags = NULL}; + + TEST_ASSERT_EQUAL_INT(TL_PARSE_OK, tl_parse_args_ex(3, argv, &options)); + TEST_ASSERT_EQUAL_STRING("./tmp/example", tl_get_flag("--flag-bool")); + TEST_ASSERT_EQUAL_UINT(0, tl_count_positional()); +} + +static void test_tl_parse_args_ex_bool_flag_keeps_positional(void) { + char *argv[] = {"program", "--flag-bool", "./tmp/example"}; + const char *bool_flags[] = {"--flag-bool", NULL}; + TlParseOptions options = {.bool_flags = bool_flags}; + + TEST_ASSERT_EQUAL_INT(TL_PARSE_OK, tl_parse_args_ex(3, argv, &options)); + TEST_ASSERT_TRUE(tl_lookup_flag("--flag-bool")); + TEST_ASSERT_NULL(tl_get_flag("--flag-bool")); + TEST_ASSERT_EQUAL_STRING("./tmp/example", tl_get_positional(0)); +} + +static void test_tl_parse_args_range_bool_before_positional(void) { + char *argv[] = {"program", "command", "subcommand", "--flag-bool", "./tmp/example"}; + const char *bool_flags[] = {"--flag-bool", NULL}; + TlParseOptions options = {.bool_flags = bool_flags}; + size_t command_index = tl_arg_index(5, argv, "command"); + size_t subcommand_index = tl_arg_index_after(5, argv, "subcommand", command_index); + + TEST_ASSERT_EQUAL_INT(TL_PARSE_OK, + tl_parse_args_range(5, argv, subcommand_index + 1, 5, &options)); + TEST_ASSERT_TRUE(tl_lookup_flag("--flag-bool")); + TEST_ASSERT_NULL(tl_get_flag("--flag-bool")); + TEST_ASSERT_EQUAL_STRING("./tmp/example", tl_get_positional(0)); +} + +static void test_tl_parse_args_range_bool_after_positional(void) { + char *argv[] = {"program", "command", "subcommand", "./tmp/example", "--flag-bool"}; + const char *bool_flags[] = {"--flag-bool", NULL}; + TlParseOptions options = {.bool_flags = bool_flags}; + + TEST_ASSERT_EQUAL_INT(TL_PARSE_OK, tl_parse_args_range(5, argv, 3, 5, &options)); + TEST_ASSERT_TRUE(tl_lookup_flag("--flag-bool")); + TEST_ASSERT_EQUAL_STRING("./tmp/example", tl_get_positional(0)); +} + +static void test_tl_parse_args_range_value_flag_space(void) { + char *argv[] = {"program", "command", "subcommand", "--flag-value", "value", "./tmp/example"}; + const char *value_flags[] = {"--flag-value", NULL}; + TlParseOptions options = {.value_flags = value_flags}; + + TEST_ASSERT_EQUAL_INT(TL_PARSE_OK, tl_parse_args_range(6, argv, 3, 6, &options)); + TEST_ASSERT_EQUAL_STRING("value", tl_get_flag("--flag-value")); + TEST_ASSERT_EQUAL_STRING("./tmp/example", tl_get_positional(0)); +} + +static void test_tl_parse_args_range_value_flag_equals(void) { + char *argv[] = {"program", "command", "subcommand", "--flag-value=value", "./tmp/example"}; + const char *value_flags[] = {"--flag-value", NULL}; + TlParseOptions options = {.value_flags = value_flags}; + + TEST_ASSERT_EQUAL_INT(TL_PARSE_OK, tl_parse_args_range(5, argv, 3, 5, &options)); + TEST_ASSERT_EQUAL_STRING("value", tl_get_flag("--flag-value")); + TEST_ASSERT_EQUAL_STRING("./tmp/example", tl_get_positional(0)); +} + +static void test_tl_parse_args_range_value_flag_without_value_returns_error(void) { + char *argv[] = {"program", "command", "subcommand", "--flag-value"}; + const char *value_flags[] = {"--flag-value", NULL}; + TlParseOptions options = {.value_flags = value_flags}; + + TEST_ASSERT_EQUAL_INT(TL_PARSE_ERROR_MISSING_VALUE, + tl_parse_args_range(4, argv, 3, 4, &options)); + TEST_ASSERT_FALSE(tl_lookup_flag("--flag-value")); +} + +static void test_tl_parse_args_range_dash_value_requires_equals(void) { + char *argv[] = {"program", "command", "subcommand", "--flag-value", "-5"}; + const char *value_flags[] = {"--flag-value", NULL}; + TlParseOptions options = {.value_flags = value_flags}; + + TEST_ASSERT_EQUAL_INT(TL_PARSE_ERROR_MISSING_VALUE, + tl_parse_args_range(5, argv, 3, 5, &options)); +} + +static void test_tl_parse_args_range_unknown_flag_returns_error(void) { + char *argv[] = {"program", "command", "subcommand", "--unknown", "./tmp/example"}; + const char *bool_flags[] = {"--flag-bool", NULL}; + TlParseOptions options = {.bool_flags = bool_flags}; + + TEST_ASSERT_EQUAL_INT(TL_PARSE_ERROR_UNKNOWN_FLAG, + tl_parse_args_range(5, argv, 3, 5, &options)); +} + +static void test_tl_parse_args_range_conflicting_flag_returns_error(void) { + char *argv[] = {"program", "command", "subcommand", "--flag-bool"}; + const char *value_flags[] = {"--flag-bool", NULL}; + const char *bool_flags[] = {"--flag-bool", NULL}; + TlParseOptions options = {.value_flags = value_flags, .bool_flags = bool_flags}; + + TEST_ASSERT_EQUAL_INT(TL_PARSE_ERROR_CONFLICTING_FLAG, + tl_parse_args_range(4, argv, 3, 4, &options)); +} + +static void test_tl_parse_args_range_invalid_bounds_returns_error(void) { + char *argv[] = {"program", "command"}; + TEST_ASSERT_EQUAL_INT(TL_PARSE_ERROR_INVALID_RANGE, tl_parse_args_range(2, argv, 3, 3, NULL)); + TEST_ASSERT_EQUAL_INT(TL_PARSE_ERROR_INVALID_RANGE, tl_parse_args_range(2, argv, 1, 3, NULL)); + TEST_ASSERT_EQUAL_INT(TL_PARSE_ERROR_INVALID_RANGE, tl_parse_args_range(2, argv, 2, 1, NULL)); + TEST_ASSERT_EQUAL_INT(TL_PARSE_ERROR_INVALID_RANGE, + tl_parse_args_range(2, argv, TL_ARG_NOT_FOUND, 2, NULL)); + TEST_ASSERT_EQUAL_INT(TL_PARSE_ERROR_INVALID_RANGE, + tl_parse_args_range(2, argv, 1, TL_ARG_NOT_FOUND, NULL)); +} + +static void test_tl_parse_args_range_invalid_range_clears_previous_state(void) { + char *argv1[] = {"program", "--old=1", "oldpos"}; + char *argv2[] = {"program", "command"}; + + tl_parse_args(3, argv1); + TEST_ASSERT_TRUE(tl_lookup_flag("--old")); + TEST_ASSERT_EQUAL_UINT(1, tl_count_positional()); + + TEST_ASSERT_EQUAL_INT(TL_PARSE_ERROR_INVALID_RANGE, tl_parse_args_range(2, argv2, 1, 3, NULL)); + TEST_ASSERT_FALSE(tl_lookup_flag("--old")); + TEST_ASSERT_EQUAL_UINT(0, tl_count_positional()); +} + +static void test_tl_parse_args_range_empty_range_returns_ok(void) { + char *argv[] = {"program", "command"}; + TEST_ASSERT_EQUAL_INT(TL_PARSE_OK, tl_parse_args_range(2, argv, 2, 2, NULL)); + TEST_ASSERT_EQUAL_UINT(0, tl_count_flag("--anything")); + TEST_ASSERT_EQUAL_UINT(0, tl_count_positional()); +} + +static void test_tl_parse_args_range_empty_range_clears_previous_state(void) { + char *argv1[] = {"program", "--old=1", "oldpos"}; + char *argv2[] = {"program", "command"}; + + tl_parse_args(3, argv1); + TEST_ASSERT_TRUE(tl_lookup_flag("--old")); + TEST_ASSERT_EQUAL_UINT(1, tl_count_positional()); + + TEST_ASSERT_EQUAL_INT(TL_PARSE_OK, tl_parse_args_range(2, argv2, 2, 2, NULL)); + TEST_ASSERT_FALSE(tl_lookup_flag("--old")); + TEST_ASSERT_EQUAL_UINT(0, tl_count_positional()); +} + +static void test_tl_parse_args_range_can_include_argv_zero(void) { + char *argv[] = {"program", "command", "--flag-bool"}; + const char *bool_flags[] = {"--flag-bool", NULL}; + TlParseOptions options = {.bool_flags = bool_flags}; + + TEST_ASSERT_EQUAL_INT(TL_PARSE_OK, tl_parse_args_range(3, argv, 0, 3, &options)); + TEST_ASSERT_EQUAL_STRING("program", tl_get_positional(0)); + TEST_ASSERT_EQUAL_STRING("command", tl_get_positional(1)); + TEST_ASSERT_TRUE(tl_lookup_flag("--flag-bool")); +} + +static void test_tl_parse_args_range_terminator_keeps_positionals(void) { + char *argv[] = {"program", "command", "subcommand", "--", "--unknown", "./tmp/example"}; + const char *bool_flags[] = {"--flag-bool", NULL}; + TlParseOptions options = {.bool_flags = bool_flags}; + + TEST_ASSERT_EQUAL_INT(TL_PARSE_OK, tl_parse_args_range(6, argv, 3, 6, &options)); + TEST_ASSERT_EQUAL_STRING("--unknown", tl_get_positional(0)); + TEST_ASSERT_EQUAL_STRING("./tmp/example", tl_get_positional(1)); +} + static void test_tl_parse_args_null_argv(void) { - tl_parse_args(0, NULL); + TEST_ASSERT_EQUAL_INT(TL_PARSE_OK, tl_parse_args(0, NULL)); TEST_ASSERT_EQUAL_UINT(0, tl_count_positional()); TEST_ASSERT_FALSE(tl_lookup_flag("--anything")); - tl_parse_args(5, NULL); + TEST_ASSERT_EQUAL_INT(TL_PARSE_OK, tl_parse_args(5, NULL)); TEST_ASSERT_EQUAL_UINT(0, tl_count_positional()); TEST_ASSERT_FALSE(tl_lookup_flag("--anything")); } @@ -524,6 +734,7 @@ int main(void) { RUN_TEST(test_tl_parse_line_escape); RUN_TEST(test_tl_parse_line_full); RUN_TEST(test_tl_parse_line_unterminated); + RUN_TEST(test_tl_parse_line_null); RUN_TEST(test_tl_short_flag); RUN_TEST(test_tl_short_flag_value); RUN_TEST(test_tl_short_flag_equals); @@ -563,6 +774,27 @@ int main(void) { RUN_TEST(test_tl_parse_line_trailing_backslash); RUN_TEST(test_tl_parse_line_double_backslash); RUN_TEST(test_tl_negative_number_value); + RUN_TEST(test_tl_arg_index_returns_argv_index); + RUN_TEST(test_tl_arg_index_after_starts_after_given_index); + RUN_TEST(test_tl_arg_index_after_missing_name_returns_not_found); + RUN_TEST(test_tl_flag_public_type_accepts_tl_flag); + RUN_TEST(test_tl_parse_args_ex_null_options_uses_default_parsing); + RUN_TEST(test_tl_parse_args_ex_empty_options_uses_default_parsing); + RUN_TEST(test_tl_parse_args_ex_bool_flag_keeps_positional); + RUN_TEST(test_tl_parse_args_range_bool_before_positional); + RUN_TEST(test_tl_parse_args_range_bool_after_positional); + RUN_TEST(test_tl_parse_args_range_value_flag_space); + RUN_TEST(test_tl_parse_args_range_value_flag_equals); + RUN_TEST(test_tl_parse_args_range_value_flag_without_value_returns_error); + RUN_TEST(test_tl_parse_args_range_dash_value_requires_equals); + RUN_TEST(test_tl_parse_args_range_unknown_flag_returns_error); + RUN_TEST(test_tl_parse_args_range_conflicting_flag_returns_error); + RUN_TEST(test_tl_parse_args_range_invalid_bounds_returns_error); + RUN_TEST(test_tl_parse_args_range_invalid_range_clears_previous_state); + RUN_TEST(test_tl_parse_args_range_empty_range_returns_ok); + RUN_TEST(test_tl_parse_args_range_empty_range_clears_previous_state); + RUN_TEST(test_tl_parse_args_range_can_include_argv_zero); + RUN_TEST(test_tl_parse_args_range_terminator_keeps_positionals); RUN_TEST(test_tl_parse_args_null_argv); RUN_TEST(test_tl_lookup_positional); RUN_TEST(test_tl_lookup_positional_multiple);