diff options
| author | Lion Kortlepel <[email protected]> | 2026-02-01 12:34:45 +0000 |
|---|---|---|
| committer | Lion Kortlepel <[email protected]> | 2026-02-01 12:34:45 +0000 |
| commit | 1f51309907acb1bed6e588961ccb4a6a792b85c0 (patch) | |
| tree | db8b5a6168bc73dbbe034d2b5d449c27c8c7c86d | |
| parent | acf522e7bab50b24ec702b405b4e450b65f835cd (diff) | |
| download | args-1f51309907acb1bed6e588961ccb4a6a792b85c0.tar.zst args-1f51309907acb1bed6e588961ccb4a6a792b85c0.zip | |
feat!: break api by removing `n` from positionals, implement help string
| -rw-r--r-- | examples/basic_example.c | 23 | ||||
| -rw-r--r-- | ls_args.h | 155 | ||||
| -rw-r--r-- | tests/tests.c | 105 |
3 files changed, 239 insertions, 44 deletions
diff --git a/examples/basic_example.c b/examples/basic_example.c index 293d5c5..2f77b6c 100644 --- a/examples/basic_example.c +++ b/examples/basic_example.c @@ -5,13 +5,32 @@ int main(int argc, char** argv) { ls_args args; int help = 0; const char* outfile = "out.txt"; + const char* infile; + const char* testfile; ls_args_init(&args); ls_args_bool(&args, &help, "h", "help", "Prints help", 0); - ls_args_string(&args, &outfile, "o", "out", "Specify the outfile, default 'out.txt'", 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); + ls_args_pos_string(&args, &testfile, "Test file", 0); if (!ls_args_parse(&args, argc, argv)) { - printf("Error: %s\n%s\n", args.last_error, ls_args_help(&args)); + printf("Error: %s\n", args.last_error); + puts(ls_args_help(&args)); + ls_args_free(&args); + return 1; } + + if (help) { + puts(ls_args_help(&args)); + ls_args_free(&args); + return 0; + } + + printf("Got input file: %s\n", infile); + printf("Got output file: %s\n", outfile); + printf("Got test file: %s\n", testfile); + ls_args_free(&args); return 0; } @@ -125,10 +125,16 @@ typedef struct ls_args { size_t args_len; size_t args_cap; + /* program name -- this is set on ls_args_parse, but you can change it here + * if you want to, for whatever reason. */ + const char* program_name; + /* some bookkeeping -- these are used to free dynamically allocated memory * for help or errors cleanly on `ls_args_free`. */ void* _allocated_error; void* _allocated_help; + + size_t _next_pos; } ls_args; /* Zero-initializes the arguments, does not allocate */ @@ -167,11 +173,7 @@ int ls_args_bool(ls_args*, int* val, const char* short_opt, * failure. */ int ls_args_string(ls_args*, const char** val, const char* short_opt, const char* long_opt, const char* help, ls_args_mode mode); -/* A positional argument, specifically the `n`th argument which doesn't start - * with `-`, or the `n`th argument after a `--` stop indicator. Since that - * sounds a bit convoluted, here some concrete examples: - * - * Example 1: +/* A positional argument * * ./hello -r hello1 -v -x hello2 --other-flag * ^^^^^^ ^^^^^^ @@ -189,11 +191,11 @@ int ls_args_string(ls_args*, const char** val, const char* short_opt, * everything after the `--` is a positional argument, for example a filename * which starts with a dash (like in this example). * - * This also means that providing n=1 or n=2 etc. without providing n=0 is not - * allowed. + * The first call to this function declares the argument for n=0, the next for + * n=1, and so on. */ -int ls_args_pos_string(ls_args*, unsigned n, const char** val, const char* help, - ls_args_mode mode); +int ls_args_pos_string( + ls_args*, const char** val, const char* name, ls_args_mode mode); /* Does all the heavy lifting. Assumes that `argv` has `argc` elements. NULL * termination of the `argv` array doesn't matter, but null-termination of each @@ -203,12 +205,12 @@ int ls_args_pos_string(ls_args*, unsigned n, const char** val, const char* help, * On failure, the `args.last_error` is set to a human-readable string. */ int ls_args_parse(ls_args* args, int argc, char** argv); -/* Same as args.last_error. */ -char* ls_args_get_error(ls_args*); /* Constructs a help message from the arguments registered on the args struct * via `ls_args_{bool, string, ...} functions. - * The string is statically allocated and only valid until `ls_args_help` is - * called again. */ + * The string is dynamically allocated using LS_REALLOC and is freed + * automatically once ls_args_free() is called. The string may be + * replaced/changed by the next invocation to this function, as the buffer is + * reused. */ char* ls_args_help(ls_args*); /* Frees all memory allocated in the args. */ @@ -302,8 +304,11 @@ int ls_args_string(ls_args* a, const char** val, const char* short_opt, a, val, LS_ARGS_TYPE_STRING, short_opt, long_opt, help, mode); } -int ls_args_pos_string(ls_args* a, unsigned n, const char** val, - const char* help, ls_args_mode mode) { +int ls_args_pos_string( + ls_args* a, const char** val, const char* name, ls_args_mode mode) { + /* TODO: The semantics are unclear when the first arg is not required but + * the second is. Effectively, the first becomes required, too, because the + * second cannot be the second without the first. */ ls_args_arg* arg; int ret; assert(a != NULL); @@ -313,12 +318,9 @@ int ls_args_pos_string(ls_args* a, unsigned n, const char** val, a->last_error = "Allocation failure"; return 0; } - /* TODO: sanity check that there are no dashes in there, because that would - * be a misuse of the API. */ - /* the rest may be NULL */ arg->type = LS_ARGS_TYPE_STRING; - arg->match.pos = n; - arg->help = help; + arg->match.pos = a->_next_pos++; + arg->help = name; arg->mode = mode; arg->val_ptr = val; arg->is_pos = 1; @@ -352,7 +354,7 @@ static _lsa_parsed _lsa_parse(const char* s) { _lsa_parsed res; assert(s != NULL); /* empty string or `-` */ - if (s_len < 2) { + if (s_len == 0 || (s_len == 1 && s[0] == '-')) { res.type = LS_ARGS_PARSED_ERROR; res.as.erroneous = s; goto end; @@ -384,7 +386,7 @@ end: return res; } -void _lsa_apply(ls_args_arg* arg, ls_args_arg** prev_arg) { +static void _lsa_apply(ls_args_arg* arg, ls_args_arg** prev_arg) { arg->found = 1; switch (arg->type) { case LS_ARGS_TYPE_BOOL: @@ -476,6 +478,7 @@ static int _lsa_parse_positional( ls_args_arg* arg = &a->args[i]; if (arg->is_pos && arg->match.pos == pos) { *(const char**)arg->val_ptr = parsed->as.positional; + arg->found = 1; return 1; } } @@ -494,6 +497,7 @@ int ls_args_parse(ls_args* a, int argc, char** argv) { assert(a != NULL); assert(argv != NULL); a->last_error = "Success"; + a->program_name = argv[0]; /* set all args to not found in case this is called multiple times */ for (i = 0; i < (int)a->args_len; ++i) { a->args[i].found = 0; @@ -564,8 +568,13 @@ int ls_args_parse(ls_args* a, int argc, char** argv) { } } if (prev_arg) { + size_t len; /* argument for the previous param expected, but none given */ - const size_t len = 64 + strlen(prev_arg->match.name.long_opt); + /* this can not be a positional argument, because in order to become a + * prev_arg, it must have expected a value earlier. this is only the + * 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'", @@ -581,13 +590,14 @@ int ls_args_parse(ls_args* a, int argc, char** argv) { len = 64; a->_allocated_error = LS_REALLOC(a->_allocated_error, len); memset(a->_allocated_error, 0, len); - sprintf(a->_allocated_error, "Required positional argument not found (argument %d)", - a->args[i].match.pos); + sprintf(a->_allocated_error, + "Required argument '%s' not provided", a->args[i].help); } 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", + sprintf(a->_allocated_error, + "Required argument '--%s' not found", a->args[i].match.name.long_opt); } @@ -599,12 +609,97 @@ int ls_args_parse(ls_args* a, int argc, char** argv) { return 1; } -char* ls_args_help(ls_args* a) { - (void)a; - return "help!"; +typedef struct _lsa_buffer { + char* data; + size_t length; + size_t capacity; +} _lsa_buffer; + +static int _lsa_buffer_reserve(_lsa_buffer* buffer, size_t required_capacity) { + char* new_data; + + if (required_capacity <= buffer->capacity) + return 1; + + new_data = (char*)LS_REALLOC(buffer->data, required_capacity); + if (!new_data) + return 0; + + buffer->data = new_data; + buffer->capacity = required_capacity; + return 1; +} + +static int _lsa_buffer_append_bytes( + _lsa_buffer* buffer, const void* source, size_t byte_count) { + if (!_lsa_buffer_reserve(buffer, buffer->length + byte_count + 1)) { + return 0; + } + + memcpy(buffer->data + buffer->length, source, byte_count); + + buffer->length += byte_count; + buffer->data[buffer->length] = '\0'; + return 1; +} + +static int _lsa_buffer_append_cstr(_lsa_buffer* buffer, const char* string) { + return _lsa_buffer_append_bytes(buffer, string, strlen(string)); } -char* ls_args_get_error(ls_args* a) { return a->last_error; } +char* ls_args_help(ls_args* a) { + _lsa_buffer help; + if (a->_allocated_help != NULL) { + LS_FREE(a->_allocated_help); + a->_allocated_help = NULL; + } + help.data = NULL; + help.length = 0; + help.capacity = 0; + + if (!_lsa_buffer_append_cstr(&help, "Usage: ")) { + goto alloc_fail; + } + if (a->program_name == NULL) { + a->program_name = "<program>"; + } + if (!_lsa_buffer_append_cstr(&help, a->program_name)) { + goto alloc_fail; + } + if (a->args_len > 0) { + size_t i; + for (i = 0; i < a->args_len; ++i) { + if (!a->args[i].is_pos) { + if (!_lsa_buffer_append_cstr(&help, " [OPTION]")) { + goto alloc_fail; + } + break; + } + } + 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; + } + } + } + + a->_allocated_help = help.data; + a->last_error = "Success"; + return a->_allocated_help; +alloc_fail: + a->_allocated_help = help.data; + a->last_error = "Allocation failure"; + return "Not enough memory available to generate help text."; +} void ls_args_free(ls_args* a) { if (a) { diff --git a/tests/tests.c b/tests/tests.c index 0a136b1..1f45ea8 100644 --- a/tests/tests.c +++ b/tests/tests.c @@ -42,6 +42,49 @@ TEST_CASE(basic_args) { return 0; } +TEST_CASE(basic_args_with_unused_positionals) { + int help = 0; + int test = 0; + int no = 0; + const char* unused = NULL; + ls_args args; + char* argv[] = { "./hello", "-h", "--test", NULL }; + int argc = sizeof(argv) / sizeof(*argv) - 1; + + ls_args_init(&args); + ls_args_pos_string(&args, &unused, "Not used", 0); + ls_args_bool(&args, &help, "h", "help", "Provides help", 0); + ls_args_pos_string(&args, &unused, "Not used", 0); + ls_args_bool(&args, &test, "t", "test", "A test argument", 0); + ls_args_pos_string(&args, &unused, "Not used", 0); + ls_args_bool(&args, &no, "n", "nope", "An argument that isn't present", 0); + ls_args_pos_string(&args, &unused, "Not used", 0); + if (!ls_args_parse(&args, argc, argv)) { + printf("Error: %s\n", args.last_error); + ASSERT(!"ls_args_parse failed"); + } + ASSERT_EQ(help, 1, "%d"); + ASSERT_EQ(test, 1, "%d"); + ASSERT_EQ(no, 0, "%d"); + ls_args_free(&args); + 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; + 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); + return 0; +} + TEST_CASE(basic_args_required) { int help = 0; int test = 0; @@ -77,11 +120,11 @@ TEST_CASE(basic_args_positional) { ls_args_init(&args); ls_args_bool(&args, &help, "h", "help", "Provides help", 0); - ls_args_pos_string(&args, 0, &input, "Input file", 0); + ls_args_pos_string(&args, &input, "Input file", 0); ls_args_bool(&args, &test, "t", "test", "A test argument", 0); ls_args_bool(&args, &no, "n", "nope", "An argument that isn't present", 0); - ls_args_pos_string(&args, 1, &output, "Output file", 0); + ls_args_pos_string(&args, &output, "Output file", 0); if (!ls_args_parse(&args, argc, argv)) { printf("Error: %s\n", args.last_error); @@ -104,8 +147,8 @@ TEST_CASE(too_many_positional_after_double_dash) { int argc = sizeof(argv) / sizeof(*argv) - 1; ls_args_init(&args); - ls_args_pos_string(&args, 0, &first, "First positional argument", 0); - ls_args_pos_string(&args, 1, &second, "Second positional argument", 0); + ls_args_pos_string(&args, &first, "First positional argument", 0); + ls_args_pos_string(&args, &second, "Second positional argument", 0); ASSERT(!ls_args_parse(&args, argc, argv)); ASSERT_STR_EQ(args.last_error, "Unexpected argument 'three'"); @@ -114,26 +157,64 @@ TEST_CASE(too_many_positional_after_double_dash) { } TEST_CASE(basic_args_positional_required_error) { + const char* first; + ls_args args; + char* argv[] = { "./hello", NULL }; + int argc = sizeof(argv) / sizeof(*argv) - 1; + + ls_args_init(&args); + ls_args_pos_string(&args, &first, "first file", LS_ARGS_REQUIRED); + + ASSERT(!ls_args_parse(&args, argc, argv)); + + ASSERT_STR_EQ(args.last_error, "Required argument 'first file' not provided"); + ls_args_free(&args); + return 0; +} + +TEST_CASE(positional_value_same_as_flag) { int help = 0; + const char* outfile = "out.txt"; + const char* infile = NULL; + const char* testfile = NULL; + ls_args args; + char* argv[] = { "./basic_example", "hello.txt", "-o", "bruh", "h", NULL }; + int argc = sizeof(argv) / sizeof(*argv) - 1; + + ls_args_init(&args); + ls_args_bool(&args, &help, "h", "help", "Prints 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); + ls_args_pos_string(&args, &testfile, "Test file", 0); + + ASSERT(ls_args_parse(&args, argc, argv)); + ASSERT_STR_EQ(infile, "hello.txt"); + ASSERT_STR_EQ(outfile, "bruh"); + ASSERT_STR_EQ(testfile, "h"); + + ls_args_free(&args); + return 0; +} + +TEST_CASE(basic_args_positional_required) { const char* first; ls_args args; char* argv[] = { "./hello", "world", NULL }; int argc = sizeof(argv) / sizeof(*argv) - 1; ls_args_init(&args); - ls_args_pos_string(&args, 0, &first, "First positional argument", LS_ARGS_REQUIRED); + ls_args_pos_string(&args, &first, "first file", LS_ARGS_REQUIRED); - ASSERT(!ls_args_parse(&args, argc, argv)); + ASSERT(ls_args_parse(&args, argc, argv)); - ASSERT_STR_EQ(args.last_error, "Required positional argument not found (argument 0)"); + ASSERT_STR_EQ(first, "world"); ls_args_free(&args); return 0; } TEST_CASE(basic_args_positional_only_error) { int help = 0; - const char* input; - const char* output; ls_args args; char* argv[] = { "./hello", "world", NULL }; int argc = sizeof(argv) / sizeof(*argv) - 1; @@ -373,7 +454,7 @@ TEST_CASE(alloc_fail_pos_string) { /* deliberately fail the allocation here */ fail_alloc = 1; /* if the allocation fails, this fails */ - ret = ls_args_pos_string(&args, 0, &input, "Input file", 0); + ret = ls_args_pos_string(&args, &input, "Input file", 0); fail_alloc = 0; ASSERT(!ret); ASSERT_STR_EQ(args.last_error, "Allocation failure"); @@ -415,8 +496,8 @@ TEST_CASE(parse_stop) { int argc = sizeof(argv) / sizeof(*argv) - 1; ls_args_init(&args); - ls_args_pos_string(&args, 0, &first, "First positional argument", 0); - ls_args_pos_string(&args, 1, &second, "First positional argument", 0); + ls_args_pos_string(&args, &first, "First positional argument", 0); + ls_args_pos_string(&args, &second, "First positional argument", 0); ls_args_bool(&args, &help, "h", "help", "Provides help", 0); ASSERT(ls_args_parse(&args, argc, argv)); ASSERT_STR_EQ(first, "-h"); |
