From e933171609b676556f70834cbf48b441c7ed8060 Mon Sep 17 00:00:00 2001 From: Lion Kortlepel Date: Sun, 1 Feb 2026 15:35:37 +0000 Subject: feat: add readme, refactor error handling we now handle the allocation failure case in error branches too --- README.md | 62 +++++++++++++++ ls_args.h | 191 +++++++++++++++++++++++++------------------- tests/tests.c | 252 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 417 insertions(+), 88 deletions(-) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..bff5cc6 --- /dev/null +++ b/README.md @@ -0,0 +1,62 @@ +# LS Args + +Minimal, single-header command-line argument parser for C. + +- ANSI C / C89 +- Header-only +- No macros or code generation +- Extensively unit-tested (90%+ line- and branch coverage) +- Supports short/long options, booleans, strings, and positional arguments +- Supports short options as `-abc` equivalent to `-a -b -c` +- Optional/required argument modes +- Auto-generated help text +- Supports `--` to indicate that all following arguments should be treated as positional, even if they start with `-` + +## Quick Start +1. Copy `ls_args.h` to your project. +2. Define and initialize an argument parser: + ```c + ls_args args; + ls_args_init(&args); + args.help_description = "My program description."; + ``` +3. Register arguments: + ```c + int verbose = 0; + ls_args_bool(&args, &verbose, "v", "verbose", "Enable verbose output", 0); + + const char* output = NULL; + ls_args_string(&args, &output, "o", "output", "Output file", 0); + + const char* input; + ls_args_pos_string(&args, &input, "Input file", LS_ARGS_REQUIRED); + ``` +4. Parse arguments: + ```c + if (!ls_args_parse(&args, argc, argv)) { + fprintf(stderr, "%s\n", args.last_error); + exit(1); + } + ``` +5. Use parsed values. Free resources when done: + ```c + ls_args_free(&args); + ``` +6. Use `ls_args_help()` to generate a help string from provided arguments: + ```c + puts(ls_args_help(&args)); + ``` + Example output: + ``` + Usage: ./my_example [OPTION] + + Options: + -v --verbose Enable verbose output + -o --output Output file + ``` + +See [`ls_args.h`](ls_args.h) for detailed documentation and usage patterns. + +## License + +MIT. diff --git a/ls_args.h b/ls_args.h index f27b491..36e951c 100644 --- a/ls_args.h +++ b/ls_args.h @@ -1,7 +1,7 @@ /* Lion's Standard (LS) ANSI C commandline argument parser with included help * renderer. * - * Version: 2.2 + * Version: 2.3 * Website: https://libls.org * Repo: https://github.com/libls/args * SPDX-License-Identifier: MIT @@ -244,11 +244,36 @@ void ls_args_free(ls_args*); * separately with -DLS_ARGS_IMPLEMENTATION. */ #ifdef LS_ARGS_IMPLEMENTATION +#define _lsa_ALLOC_FAIL_STR "Allocation failure" + #include +#include #include #include /* for sprintf */ #include +static int _lsa_set_error_va( + ls_args* a, size_t len, const char* fmt, va_list ap) { + a->_allocated_error = LS_REALLOC(a->_allocated_error, len); + if (a->_allocated_error == NULL) { + a->last_error = _lsa_ALLOC_FAIL_STR; + return 0; + } + memset(a->_allocated_error, 0, len); + vsprintf(a->_allocated_error, fmt, ap); + a->last_error = a->_allocated_error; + return 1; +} + +static int _lsa_set_error(ls_args* a, size_t len, const char* fmt, ...) { + int ret; + va_list ap; + va_start(ap, fmt); + ret = _lsa_set_error_va(a, len, fmt, ap); + va_end(ap); + return ret; +} + /* 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 */ @@ -300,7 +325,7 @@ int _lsa_register(ls_args* a, void* val, ls_args_type type, assert(short_opt == NULL || strlen(short_opt) == 1); ret = _lsa_add(a, &arg); if (ret == 0) { - a->last_error = "Allocation failure"; + a->last_error = _lsa_ALLOC_FAIL_STR; return 0; } /* TODO: sanity check that there are no dashes in there, because that would @@ -339,7 +364,7 @@ int ls_args_pos_string( assert(val != NULL); ret = _lsa_add(a, &arg); if (ret == 0) { - a->last_error = "Allocation failure"; + a->last_error = _lsa_ALLOC_FAIL_STR; return 0; } arg->type = LS_ARGS_TYPE_STRING; @@ -441,11 +466,10 @@ static int _lsa_parse_long( } if (!found) { const size_t len = 32 + strlen(parsed->as.erroneous); - a->_allocated_error = LS_REALLOC(a->_allocated_error, len); - memset(a->_allocated_error, 0, len); - sprintf(a->_allocated_error, "Invalid argument '--%s'", - parsed->as.erroneous); - a->last_error = a->_allocated_error; + if (!_lsa_set_error( + a, len, "Invalid argument '--%s'", parsed->as.erroneous)) { + return 0; + } return 0; } return 1; @@ -461,13 +485,12 @@ static int _lsa_parse_short( if (*prev_arg) { struct _lsa_spec named = (*prev_arg)->match.name; const size_t len = 128 + strlen(named.short_opt); - a->_allocated_error = LS_REALLOC(a->_allocated_error, len); - memset(a->_allocated_error, 0, len); - sprintf(a->_allocated_error, - "Expected argument following '-%s', instead got another " - "argument '-%c'", - named.short_opt, arg); - a->last_error = a->_allocated_error; + if (!_lsa_set_error(a, len, + "Expected argument following '-%s', instead got another " + "argument '-%c'", + named.short_opt, arg)) { + return 0; + } return 0; } for (k = 0; k < a->args_len; ++k) { @@ -484,10 +507,9 @@ static int _lsa_parse_short( } if (!found) { const size_t len = 32; - a->_allocated_error = LS_REALLOC(a->_allocated_error, len); - memset(a->_allocated_error, 0, len); - sprintf(a->_allocated_error, "Invalid argument '-%c'", arg); - a->last_error = a->_allocated_error; + if (!_lsa_set_error(a, len, "Invalid argument '-%c'", arg)) { + return 0; + } return 0; } } @@ -506,11 +528,10 @@ static int _lsa_parse_positional( return 1; } } - a->_allocated_error = LS_REALLOC(a->_allocated_error, len); - memset(a->_allocated_error, 0, len); - sprintf( - a->_allocated_error, "Unexpected argument '%s'", parsed->as.positional); - a->last_error = a->_allocated_error; + if (!_lsa_set_error( + a, len, "Unexpected argument '%s'", parsed->as.positional)) { + return 0; + } return 0; } @@ -532,12 +553,11 @@ int ls_args_parse(ls_args* a, int argc, char** argv) { if (parsed.type != LS_ARGS_PARSED_POSITIONAL) { /* argument for the previous param expected, but none given */ const size_t len = 64 + strlen(prev_arg->match.name.long_opt); - a->_allocated_error = LS_REALLOC(a->_allocated_error, len); - memset(a->_allocated_error, 0, len); - sprintf(a->_allocated_error, - "Expected argument following '--%s'", - prev_arg->match.name.long_opt); - a->last_error = a->_allocated_error; + if (!_lsa_set_error(a, len, + "Expected argument following '--%s'", + prev_arg->match.name.long_opt)) { + return 0; + } return 0; } if (prev_arg->type == LS_ARGS_TYPE_STRING) { @@ -549,11 +569,10 @@ int ls_args_parse(ls_args* a, int argc, char** argv) { switch (parsed.type) { case LS_ARGS_PARSED_ERROR: { const size_t len = 32 + strlen(parsed.as.erroneous); - a->_allocated_error = LS_REALLOC(a->_allocated_error, len); - memset(a->_allocated_error, 0, len); - sprintf(a->_allocated_error, "Invalid argument '%s'", - parsed.as.erroneous); - a->last_error = a->_allocated_error; + if (!_lsa_set_error( + a, len, "Invalid argument '%s'", parsed.as.erroneous)) { + return 0; + } return 0; } case LS_ARGS_PARSED_LONG: { @@ -599,11 +618,10 @@ int ls_args_parse(ls_args* a, int argc, char** argv) { * case with -/--... arguments */ assert(!prev_arg->is_pos); len = 64 + strlen(prev_arg->match.name.long_opt); - a->_allocated_error = LS_REALLOC(a->_allocated_error, len); - memset(a->_allocated_error, 0, len); - sprintf(a->_allocated_error, "Expected argument following '--%s'", - prev_arg->match.name.long_opt); - a->last_error = a->_allocated_error; + if (!_lsa_set_error(a, len, "Expected argument following '--%s'", + prev_arg->match.name.long_opt)) { + return 0; + } return 0; } @@ -612,20 +630,19 @@ int ls_args_parse(ls_args* a, int argc, char** argv) { size_t len; if (a->args[i].is_pos) { len = 64; - a->_allocated_error = LS_REALLOC(a->_allocated_error, len); - memset(a->_allocated_error, 0, len); - sprintf(a->_allocated_error, - "Required argument '%s' not provided", a->args[i].help); + if (!_lsa_set_error(a, len, + "Required argument '%s' not provided", + a->args[i].help)) { + return 0; + } } else { len = 64 + strlen(a->args[i].match.name.long_opt); - a->_allocated_error = LS_REALLOC(a->_allocated_error, len); - memset(a->_allocated_error, 0, len); - sprintf(a->_allocated_error, - "Required argument '--%s' not found", - a->args[i].match.name.long_opt); + if (!_lsa_set_error(a, len, + "Required argument '--%s' not found", + a->args[i].match.name.long_opt)) { + return 0; + } } - - a->last_error = a->_allocated_error; return 0; } } @@ -701,18 +718,19 @@ char* ls_args_help(ls_args* a) { } } for (i = 0; i < a->args_len; ++i) { - if (a->args[i].is_pos) { - const char* open - = a->args[i].mode == LS_ARGS_REQUIRED ? " <" : " ["; - const char* close - = a->args[i].mode == LS_ARGS_REQUIRED ? ">" : "]"; - if (!_lsa_buffer_append_cstr(&help, open)) - goto alloc_fail; - if (!_lsa_buffer_append_cstr(&help, a->args[i].help)) - goto alloc_fail; - if (!_lsa_buffer_append_cstr(&help, close)) - goto alloc_fail; + const char* open, *close; + if (!a->args[i].is_pos) { + continue; } + + open = a->args[i].mode == LS_ARGS_REQUIRED ? " <" : " ["; + close = a->args[i].mode == LS_ARGS_REQUIRED ? ">" : "]"; + if (!_lsa_buffer_append_cstr(&help, open)) + goto alloc_fail; + if (!_lsa_buffer_append_cstr(&help, a->args[i].help)) + goto alloc_fail; + if (!_lsa_buffer_append_cstr(&help, close)) + goto alloc_fail; } if (a->help_description) { if (!_lsa_buffer_append_cstr(&help, "\n\n")) @@ -720,24 +738,37 @@ char* ls_args_help(ls_args* a) { if (!_lsa_buffer_append_cstr(&help, a->help_description)) goto alloc_fail; } - if (!_lsa_buffer_append_cstr(&help, "\n\nOptions:")) - goto alloc_fail; - for (i = 0; i < a->args_len; ++i) { - if (!a->args[i].is_pos) { - if (!_lsa_buffer_append_cstr(&help, "\n -")) - goto alloc_fail; - if (!_lsa_buffer_append_cstr( - &help, a->args[i].match.name.short_opt)) - goto alloc_fail; - if (!_lsa_buffer_append_cstr(&help, " \t--")) - goto alloc_fail; - if (!_lsa_buffer_append_cstr( - &help, a->args[i].match.name.long_opt)) - goto alloc_fail; - if (!_lsa_buffer_append_cstr(&help, " \t\t")) - goto alloc_fail; - if (!_lsa_buffer_append_cstr(&help, a->args[i].help)) + + /* Only print "Options:" if there are non-positional options */ + { + int has_nonpositional = 0; + for (i = 0; i < a->args_len; ++i) { + if (!a->args[i].is_pos) { + has_nonpositional = 1; + break; + } + } + if (has_nonpositional) { + if (!_lsa_buffer_append_cstr(&help, "\n\nOptions:")) goto alloc_fail; + for (i = 0; i < a->args_len; ++i) { + if (!a->args[i].is_pos) { + if (!_lsa_buffer_append_cstr(&help, "\n -")) + goto alloc_fail; + if (!_lsa_buffer_append_cstr( + &help, a->args[i].match.name.short_opt)) + goto alloc_fail; + if (!_lsa_buffer_append_cstr(&help, " \t--")) + goto alloc_fail; + if (!_lsa_buffer_append_cstr( + &help, a->args[i].match.name.long_opt)) + goto alloc_fail; + if (!_lsa_buffer_append_cstr(&help, " \t\t")) + goto alloc_fail; + if (!_lsa_buffer_append_cstr(&help, a->args[i].help)) + goto alloc_fail; + } + } } } } @@ -747,7 +778,7 @@ char* ls_args_help(ls_args* a) { return a->_allocated_help; alloc_fail: a->_allocated_help = help.data; - a->last_error = "Allocation failure"; + a->last_error = _lsa_ALLOC_FAIL_STR; return "Not enough memory available to generate help text."; } diff --git a/tests/tests.c b/tests/tests.c index 6a7b4b7..9b9007e 100644 --- a/tests/tests.c +++ b/tests/tests.c @@ -3,10 +3,15 @@ #define LS_TEST_IMPLEMENTATION #include "ls_test.h" -int fail_alloc = 0; +int fail_alloc_once = 0; +int alloc_limit = -1; void* test_realloc(void* p, size_t size) { - if (fail_alloc) { + if (fail_alloc_once) { + fail_alloc_once = 0; + return NULL; + } + if (alloc_limit != -1 && (int)size > alloc_limit) { return NULL; } return realloc(p, size); @@ -70,15 +75,246 @@ TEST_CASE(basic_args_with_unused_positionals) { return 0; } +TEST_CASE(help_output_no_options) { + ls_args args; + const char* infile = NULL; + char* help_str; + + ls_args_init(&args); + ls_args_pos_string(&args, &infile, "Input file", 0); + + help_str = ls_args_help(&args); + ASSERT(help_str != NULL); + ASSERT_STR_EQ(args.last_error, "Success"); + + /* [OPTION] should NOT be present */ + ASSERT(!strstr(help_str, "[OPTION]")); + + /* "Options:" should NOT be present */ + ASSERT(!strstr(help_str, "Options:")); + + /* The positional argument should be present */ + ASSERT(strstr(help_str, "[Input file]") || strstr(help_str, "")); + + ls_args_free(&args); + return 0; +} + +TEST_CASE(huge_description) { + int help = 0; + ls_args args; + int i; + char* help_str; + /* Create a very large description string */ + enum { DESC_SIZE = 8192 }; + char* huge_desc = (char*)LS_REALLOC(NULL, DESC_SIZE + 1); + ASSERT(huge_desc != NULL); + for (i = 0; i < DESC_SIZE; ++i) { + huge_desc[i] = 'A' + (i % 26); + } + huge_desc[DESC_SIZE] = '\0'; + + ls_args_init(&args); + ls_args_bool(&args, &help, "h", "help", huge_desc, 0); + + help_str = ls_args_help(&args); + ASSERT(help_str != NULL); + ASSERT_STR_EQ(args.last_error, "Success"); + /* The huge description should appear in the help output */ + ASSERT(strstr(help_str, huge_desc)); + + LS_FREE(huge_desc); + ls_args_free(&args); + return 0; +} + +TEST_CASE(help_output_basic) { + int help = 0; + const char* infile = NULL; + const char* outfile = "out.txt"; + ls_args args; + char* help_str; + + ls_args_init(&args); + ls_args_bool(&args, &help, "h", "help", "Provides help", 0); + ls_args_string(&args, &outfile, "o", "out", + "Specify the outfile, default 'out.txt'", 0); + ls_args_pos_string(&args, &infile, "Input file", 0); + + help_str = ls_args_help(&args); + ASSERT(help_str != NULL); + ASSERT_STR_EQ(args.last_error, "Success"); + + ASSERT(strstr(help_str, "-h")); + ASSERT(strstr(help_str, "--help")); + ASSERT(strstr(help_str, "-o")); + ASSERT(strstr(help_str, "--out")); + + ASSERT(strstr(help_str, "Provides help")); + ASSERT(strstr(help_str, "Specify the outfile")); + + ASSERT(strstr(help_str, "[OPTION]")); + ASSERT(strstr(help_str, "[Input file]")); + + ASSERT(strstr(help_str, "default 'out.txt'")); + + ASSERT(strstr(help_str, "Input file")); + + ASSERT(strstr(help_str, "-h")); + ASSERT(strstr(help_str, "--help")); + ASSERT(strstr(help_str, "-o")); + ASSERT(strstr(help_str, "--out")); + + ls_args_free(&args); + return 0; +} + +TEST_CASE(help_output_basic_required_pos) { + int help = 0; + const char* infile = NULL; + const char* outfile = "out.txt"; + ls_args args; + char* help_str; + + ls_args_init(&args); + ls_args_bool(&args, &help, "h", "help", "Provides help", 0); + ls_args_string(&args, &outfile, "o", "out", + "Specify the outfile, default 'out.txt'", 0); + ls_args_pos_string(&args, &infile, "Input file", LS_ARGS_REQUIRED); + + help_str = ls_args_help(&args); + ASSERT(help_str != NULL); + ASSERT_STR_EQ(args.last_error, "Success"); + + ASSERT(strstr(help_str, "-h")); + ASSERT(strstr(help_str, "--help")); + ASSERT(strstr(help_str, "-o")); + ASSERT(strstr(help_str, "--out")); + + ASSERT(strstr(help_str, "Provides help")); + ASSERT(strstr(help_str, "Specify the outfile")); + + ASSERT(strstr(help_str, "[OPTION]")); + ASSERT(strstr(help_str, "")); + + ASSERT(strstr(help_str, "default 'out.txt'")); + + ASSERT(strstr(help_str, "Input file")); + + ASSERT(strstr(help_str, "-h")); + ASSERT(strstr(help_str, "--help")); + ASSERT(strstr(help_str, "-o")); + ASSERT(strstr(help_str, "--out")); + + ls_args_free(&args); + return 0; +} + +TEST_CASE(help_alloc_limit_sweep) { + int help = 0; + int limit; + const char* infile = NULL; + const char* outfile = "out.txt"; + ls_args args; + char* help_str = NULL; + int succeeded = 0; + + ls_args_init(&args); + args.help_description = "My description!"; + ls_args_bool(&args, &help, "h", "help", "Provides help", 0); + ls_args_string(&args, &outfile, "o", "out", + "Specify the outfile, default 'out.txt'", 0); + ls_args_pos_string(&args, &infile, "Input file", 0); + + /* Sweep alloc_limit from very small sizes upward to ensure all + * allocation attempts inside ls_args_help are exercised. + * For each limit, call ls_args_help multiple times to verify repeated failures. */ + for (limit = 0; limit <= 8192 && !succeeded; ++limit) { + + /* First attempt */ + alloc_limit = limit; + help_str = ls_args_help(&args); + alloc_limit = -1; + if (help_str == NULL || strcmp(args.last_error, "Success") != 0) { + /* Expect allocation-related failure while we are below the needed size */ + ASSERT_STR_EQ(args.last_error, "Allocation failure"); + ASSERT(help_str != NULL); + ASSERT_STR_EQ(help_str, "Not enough memory available to generate help text."); + + /* Second attempt (repeated failure path) */ + alloc_limit = limit; + help_str = ls_args_help(&args); + alloc_limit = -1; + ASSERT_STR_EQ(args.last_error, "Allocation failure"); + ASSERT(help_str != NULL); + ASSERT_STR_EQ(help_str, "Not enough memory available to generate help text."); + } else { + /* Success achieved at this alloc_limit; verify content */ + succeeded = 1; + ASSERT(help_str != NULL); + ASSERT_STR_EQ(args.last_error, "Success"); + ASSERT(strstr(help_str, "Provides help")); + ASSERT(strstr(help_str, "Specify the outfile")); + ASSERT(strstr(help_str, "Input file")); + } + } + + /* Ensure we eventually succeeded in generating help text */ + ASSERT(succeeded); + + ls_args_free(&args); + return 0; +} + +TEST_CASE(help_output_empty_description) { + int help = 0; + ls_args args; + char* help_str; + + ls_args_init(&args); + ls_args_bool(&args, &help, "h", "help", "", 0); + + help_str = ls_args_help(&args); + ASSERT(help_str != NULL); + ASSERT_STR_EQ(args.last_error, "Success"); + + /* Should show the option, but not crash or print garbage for description */ + ASSERT(strstr(help_str, "-h")); + ASSERT(strstr(help_str, "--help")); + /* Should not print any description after the option */ + /* Accept either a blank line or just the option itself */ + /* There should not be any non-space character after the option on its line */ + { + const char* opt_line = strstr(help_str, "-h \t--help"); + ASSERT(opt_line != NULL); + /* Find the end of the line */ + const char* end = strchr(opt_line, '\n'); + if (end) { + /* Check that between the end of the option and the newline, only spaces/tabs appear */ + const char* after = opt_line + strlen("-h \t--help"); + while (after < end && (*after == ' ' || *after == '\t')) ++after; + ASSERT(after == end); + } + } + + ls_args_free(&args); + return 0; +} + +TEST_CASE(free_null) { + /* don't crash */ + ls_args_free(NULL); + return 0; +} + TEST_CASE(help_alloc_fail) { int help = 0; ls_args args; char* help_str; ls_args_init(&args); ls_args_bool(&args, &help, "h", "help", "Provides help", 0); - fail_alloc = 1; + fail_alloc_once = 1; help_str = ls_args_help(&args); - fail_alloc = 0; ASSERT_STR_EQ(args.last_error, "Allocation failure"); ASSERT_STR_EQ(help_str, "Not enough memory available to generate help text."); ls_args_free(&args); @@ -447,10 +683,10 @@ TEST_CASE(alloc_fail) { ls_args_init(&args); ASSERT_EQ(args.args_len, 0, "%uz"); /* deliberately fail the allocation here */ - fail_alloc = 1; + fail_alloc_once = 1; /* if the allocation fails, this fails */ ret = ls_args_bool(&args, &help, "h", "help", "Provides help", 0); - fail_alloc = 0; + ASSERT(!ret); ASSERT_STR_EQ(args.last_error, "Allocation failure"); /* there is no documented error state for this; we simply fail to add the @@ -470,10 +706,10 @@ TEST_CASE(alloc_fail_pos_string) { ls_args_init(&args); ASSERT_EQ(args.args_len, 0, "%uz"); /* deliberately fail the allocation here */ - fail_alloc = 1; + fail_alloc_once = 1; /* if the allocation fails, this fails */ ret = ls_args_pos_string(&args, &input, "Input file", 0); - fail_alloc = 0; + ASSERT(!ret); ASSERT_STR_EQ(args.last_error, "Allocation failure"); ASSERT_EQ(args.args_len, 0, "%uz"); -- cgit