aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLion Kortlepel <[email protected]>2026-01-29 18:30:21 +0000
committerLion Kortlepel <[email protected]>2026-01-29 18:30:21 +0000
commitb4feabc2c1dccb7212421c8e1edf57db6388a7a3 (patch)
tree85bf1b91b4b3ae416b62a9fc3e9b618db735ecfc
parentc781f2c7a4a41fad1a7e5e66ebb258c1fc8a415a (diff)
downloadargs-b4feabc2c1dccb7212421c8e1edf57db6388a7a3.tar.zst
args-b4feabc2c1dccb7212421c8e1edf57db6388a7a3.zip
feat: implement positional arguments
-rw-r--r--ls_args.h144
-rw-r--r--tests/tests.c116
2 files changed, 236 insertions, 24 deletions
diff --git a/ls_args.h b/ls_args.h
index bd57e2a..1c9301a 100644
--- a/ls_args.h
+++ b/ls_args.h
@@ -40,9 +40,11 @@
*
* 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);
* if (!ls_args_parse(&args, argc, argv)) {
- * printf("Error: %s\n%s\n", args.last_error, ls_args_help(&args));
+ * printf("Error: %s\n%s\n", args.last_error,
+ * ls_args_help(&args));
* }
* ls_args_free(&args);
*
@@ -97,8 +99,14 @@ typedef enum ls_args_type {
} ls_args_type;
typedef struct ls_args_arg {
- const char* short_opt;
- const char* long_opt;
+ int is_pos;
+ union {
+ struct _lsa_spec {
+ const char* short_opt;
+ const char* long_opt;
+ } name;
+ unsigned pos;
+ } match;
const char* help;
ls_args_type type;
void* val_ptr;
@@ -159,6 +167,33 @@ 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:
+ *
+ * ./hello -r hello1 -v -x hello2 --other-flag
+ * ^^^^^^ ^^^^^^
+ * n=0 n=1
+ *
+ * assuming that -r is a boolean flag (takes no arguments).
+ *
+ * Example 2:
+ *
+ * ./my-app --flag1 --flag2 -- --help.txt
+ * ^^ ^^^^^^^^^^
+ * | n=0
+ * |
+ * "stop" indicator
+ * 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.
+ */
+int ls_args_pos_string(ls_args*, unsigned n, const char** val, const char* help,
+ 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
@@ -246,8 +281,9 @@ int _lsa_register(ls_args* a, void* val, ls_args_type type,
* be a misuse of the API. */
/* the rest may be NULL */
arg->type = type;
- arg->short_opt = short_opt;
- arg->long_opt = long_opt;
+ arg->match.name.short_opt = short_opt;
+ arg->match.name.long_opt = long_opt;
+ arg->is_pos = 0;
arg->help = help;
arg->mode = mode;
arg->val_ptr = val;
@@ -266,6 +302,29 @@ 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) {
+ ls_args_arg* arg;
+ int ret;
+ assert(a != NULL);
+ assert(val != NULL);
+ ret = _lsa_add(a, &arg);
+ if (ret == 0) {
+ 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->mode = mode;
+ arg->val_ptr = val;
+ arg->is_pos = 1;
+ return 1;
+}
+
typedef enum _lsa_parsed_type {
LS_ARGS_PARSED_ERROR = 0,
LS_ARGS_PARSED_LONG = 1,
@@ -343,8 +402,12 @@ static int _lsa_parse_long(
int found = 0;
size_t k;
for (k = 0; k < a->args_len; ++k) {
- if (a->args[k].long_opt != NULL
- && strcmp(parsed->as.long_arg, a->args[k].long_opt) == 0) {
+ if (a->args[k].is_pos) {
+ continue;
+ }
+ if (a->args[k].match.name.long_opt != NULL
+ && strcmp(parsed->as.long_arg, a->args[k].match.name.long_opt)
+ == 0) {
_lsa_apply(&a->args[k], prev_arg);
found = 1;
break;
@@ -370,18 +433,23 @@ static int _lsa_parse_short(
int found = 0;
size_t k;
if (*prev_arg) {
- const size_t len = 128 + strlen((*prev_arg)->short_opt);
+ 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'",
- (*prev_arg)->short_opt, arg);
+ named.short_opt, arg);
a->last_error = a->_allocated_error;
return 0;
}
for (k = 0; k < a->args_len; ++k) {
- const char* opt = a->args[k].short_opt;
+ const char* opt;
+ if (a->args[k].is_pos) {
+ continue;
+ }
+ opt = a->args[k].match.name.short_opt;
if (opt != NULL && opt[0] == arg) {
_lsa_apply(&a->args[k], prev_arg);
found = 1;
@@ -400,8 +468,28 @@ static int _lsa_parse_short(
return 1;
}
+static int _lsa_parse_positional(
+ ls_args* a, _lsa_parsed* parsed, unsigned pos) {
+ const size_t len = 32;
+ size_t i;
+ for (i = 0; i < a->args_len; ++i) {
+ ls_args_arg* arg = &a->args[i];
+ if (arg->is_pos && arg->match.pos == pos) {
+ *(const char**)arg->val_ptr = parsed->as.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;
+ return 0;
+}
+
int ls_args_parse(ls_args* a, int argc, char** argv) {
int i;
+ unsigned pos_i = 0;
ls_args_arg* prev_arg = NULL;
assert(a != NULL);
assert(argv != NULL);
@@ -415,11 +503,12 @@ int ls_args_parse(ls_args* a, int argc, char** argv) {
if (prev_arg) {
if (parsed.type != LS_ARGS_PARSED_POSITIONAL) {
/* argument for the previous param expected, but none given */
- const size_t len = 64 + strlen(prev_arg->long_opt);
+ 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->long_opt);
+ "Expected argument following '--%s'",
+ prev_arg->match.name.long_opt);
a->last_error = a->_allocated_error;
return 0;
}
@@ -451,32 +540,47 @@ int ls_args_parse(ls_args* a, int argc, char** argv) {
}
break;
}
- case LS_ARGS_PARSED_STOP:
- /* TODO */
+ case LS_ARGS_PARSED_STOP: {
+ i += 1;
+ for (; i < argc; ++i) {
+ _lsa_parsed parsed;
+ parsed.type = LS_ARGS_PARSED_POSITIONAL;
+ parsed.as.positional = argv[i];
+ if (!_lsa_parse_positional(a, &parsed, pos_i)) {
+ return 0;
+ }
+ pos_i += 1;
+ }
+ /* that's all, no more parsing allowed */
+ i = argc;
break;
+ }
case LS_ARGS_PARSED_POSITIONAL:
- assert(!"UNREACHABLE");
+ if (!_lsa_parse_positional(a, &parsed, pos_i)) {
+ return 0;
+ }
+ ++pos_i;
break;
}
}
if (prev_arg) {
/* argument for the previous param expected, but none given */
- const size_t len = 64 + strlen(prev_arg->long_opt);
+ 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->long_opt);
+ prev_arg->match.name.long_opt);
a->last_error = a->_allocated_error;
return 0;
}
for (i = 0; i < (int)a->args_len; ++i) {
if (a->args[i].mode == LS_ARGS_REQUIRED && !a->args[i].found) {
- const size_t len = 64 + strlen(a->args[i].long_opt);
+ const size_t 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].long_opt);
+ a->args[i].match.name.long_opt);
a->last_error = a->_allocated_error;
return 0;
}
diff --git a/tests/tests.c b/tests/tests.c
index 74ddb3e..78c8f24 100644
--- a/tests/tests.c
+++ b/tests/tests.c
@@ -1,5 +1,5 @@
-#include <stdint.h>
#include <limits.h>
+#include <stdint.h>
#define LS_TEST_IMPLEMENTATION
#include "ls_test.h"
@@ -51,7 +51,8 @@ TEST_CASE(basic_args_required) {
ls_args_init(&args);
ls_args_bool(&args, &help, "h", "help", "Provides help", 0);
- ls_args_bool(&args, &test, "t", "test", "A test argument", LS_ARGS_REQUIRED);
+ ls_args_bool(
+ &args, &test, "t", "test", "A test argument", LS_ARGS_REQUIRED);
ASSERT(!ls_args_parse(&args, argc, argv));
ASSERT_STR_EQ(args.last_error, "Required argument '--test' not found");
@@ -64,6 +65,71 @@ TEST_CASE(basic_args_required) {
return 0;
}
+TEST_CASE(basic_args_positional) {
+ int help = 0;
+ int test = 0;
+ int no = 0;
+ const char* input;
+ const char* output;
+ ls_args args;
+ char* argv[] = { "./hello", "-h", "hi", "--test", "world", NULL };
+ int argc = sizeof(argv) / sizeof(*argv) - 1;
+
+ 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_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);
+
+ 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");
+ ASSERT_STR_EQ(input, "hi");
+ ASSERT_STR_EQ(output, "world");
+ ls_args_free(&args);
+ return 0;
+}
+
+TEST_CASE(too_many_positional_after_double_dash) {
+ const char* first = NULL;
+ const char* second = NULL;
+ ls_args args;
+ char* argv[] = { "./hello", "--", "one", "two", "three", NULL };
+ 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);
+
+ ASSERT(!ls_args_parse(&args, argc, argv));
+ ASSERT_STR_EQ(args.last_error, "Unexpected argument 'three'");
+ 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;
+
+ ls_args_init(&args);
+ ls_args_bool(&args, &help, "h", "help", "Provides help", 0);
+
+ ASSERT(!ls_args_parse(&args, argc, argv));
+
+ ASSERT_STR_EQ(args.last_error, "Unexpected argument 'world'");
+ ls_args_free(&args);
+ return 0;
+}
TEST_CASE(basic_args_only_short) {
int help = 0;
@@ -189,7 +255,8 @@ TEST_CASE(error_expected_argument_short_combined) {
ls_args_bool(&args, &help, "h", "help", "Provides help", 0);
ls_args_string(&args, &file, "f", "file", "File to work on", 0);
ASSERT(!ls_args_parse(&args, argc, argv));
- ASSERT_STR_EQ(args.last_error, "Expected argument following '-f', instead got another argument '-h'");
+ ASSERT_STR_EQ(args.last_error,
+ "Expected argument following '-f', instead got another argument '-h'");
ls_args_free(&args);
return 0;
}
@@ -241,7 +308,8 @@ TEST_CASE(strip_dashes) {
/* you can mix them */
ls_args_bool(&args, &test, "t", "--test", "A test argument", 0);
/* have as many as you want */
- ls_args_bool(&args, &no, "-n", "----nope", "An argument that isn't present", 0);
+ ls_args_bool(
+ &args, &no, "-n", "----nope", "An argument that isn't present", 0);
if (!ls_args_parse(&args, argc, argv)) {
printf("Error: %s\n", args.last_error);
ASSERT(!"ls_args_parse failed");
@@ -276,6 +344,27 @@ TEST_CASE(alloc_fail) {
return 0;
}
+TEST_CASE(alloc_fail_pos_string) {
+ const char* input = NULL;
+ ls_args args;
+ char* argv[] = { "./hello", "file.txt", NULL };
+ int argc = sizeof(argv) / sizeof(*argv) - 1;
+ int ret;
+
+ ls_args_init(&args);
+ ASSERT_EQ(args.args_len, 0, "%uz");
+ /* 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);
+ fail_alloc = 0;
+ ASSERT(!ret);
+ ASSERT_STR_EQ(args.last_error, "Allocation failure");
+ ASSERT_EQ(args.args_len, 0, "%uz");
+ ls_args_free(&args);
+ return 0;
+}
+
TEST_CASE(error_parse_fail_empty) {
int help = 0;
ls_args args;
@@ -301,6 +390,25 @@ TEST_CASE(error_parse_ignore_double_dash) {
return 0;
}
+TEST_CASE(parse_stop) {
+ const char *first, *second;
+ ls_args args;
+ int help = 0;
+ char* argv[] = { "./hello", "--help", "--", "-h", "--test", NULL };
+ 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_bool(&args, &help, "h", "help", "Provides help", 0);
+ ASSERT(ls_args_parse(&args, argc, argv));
+ ASSERT_STR_EQ(first, "-h");
+ ASSERT_STR_EQ(second, "--test");
+ ASSERT_EQ(help, 1, "%d");
+ ls_args_free(&args);
+ return 0;
+}
+
TEST_CASE(error_invalid_argument_short) {
int help = 0;
ls_args args;