diff --git a/CMakeLists.txt b/CMakeLists.txt index a57d625..78dbbd7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -87,6 +87,7 @@ if(TL_BUILD_TESTS) tl_config_test tl_debug_test tl_error_test + tl_flag_test tl_test_test ) diff --git a/include/tl_app.h b/include/tl_app.h index 654e0eb..6c39607 100644 --- a/include/tl_app.h +++ b/include/tl_app.h @@ -3,8 +3,6 @@ #ifndef TL_APP_H #define TL_APP_H -#include - /** * @brief Initializes the running app with the given command line arguments. * @@ -15,32 +13,4 @@ */ void tl_init_app(int argc, char *argv[]); -/** - * @brief Parses the given command line arguments. - * - * @param argc The number of command line arguments. - * @param argv The command line arguments. - * - * @return void - */ -void tl_parse_args(int argc, char *argv[]); - -/** - * @brief Looks up a specific flag. - * - * @param flag The flag to look up. - * - * @return true if the flag is found, false otherwise. - */ -bool tl_lookup_flag(const char *flag); - -/** - * @brief Returns the value of a specific flag. - * - * @param flag The flag to get. - * - * @return The value of the flag, or NULL if not found. - */ -const char *tl_get_flag(const char *flag); - #endif // TL_APP_H diff --git a/include/tl_flag.h b/include/tl_flag.h new file mode 100644 index 0000000..83d07a6 --- /dev/null +++ b/include/tl_flag.h @@ -0,0 +1,143 @@ +// See LICENSE.txt and CONTRIBUTING.md for details. + +#ifndef TL_FLAG_H +#define TL_FLAG_H + +#include +#include + +/** + * @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 + * '=', so comparisons must use memcmp with name_len, never strcmp. + * + * argv entry: "--foo=bar" + * - - f o o = b a r \0 + * ^ ^ + * name value + * name_len = 5 + * value = "bar" + * + * argv entry: "--foo" "bar" + * - - f o o \0 b a r \0 + * ^ ^ + * name value + * name_len = 5 + * value = "bar" + * + * argv entry: "--foo" (boolean, no value) + * - - f o o \0 + * ^ + * name + * name_len = 5 + * value = NULL + */ +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; + +/** + * @brief Parses the given command line arguments. + * + * Parses argv into flags. A flag is anything starting with "--". It can carry + * a value written as --name=value, or as --name value in the next entry. + * A bare "--" ends flag parsing; everything after it is a positional, even if + * it starts with dashes. Any previously parsed state is thrown away first. + * + * @param argc The number of command line arguments. + * @param argv The command line arguments. + * + * @return void + */ +void tl_parse_args(int argc, char *argv[]); + +/** + * @brief Parses a raw command line string. + * + * Splits the line into tokens. Double quotes group text with spaces into + * one token, and a backslash keeps the next character as-is. The first + * token is the program name and is skipped, like argv[0]. + * + * @param line The command line string to parse. + * + * @return true on success, false otherwise. + */ +bool tl_parse_line(const char *line); + +/** + * @brief Releases memory held by the argument parser. + * + * Safe to call when nothing has been parsed. Called implicitly by + * tl_parse_args and tl_parse_line. + * + * @return void + */ +void tl_free_args(void); + +/** + * @brief Looks up a specific flag. + * + * @param flag The flag to look up. + * + * @return true if the flag is found, false otherwise. + */ +bool tl_lookup_flag(const char *flag); + +/** + * @brief Returns the value of a specific flag. + * + * Returns the value of the first occurrence of flag. For repeated flags + * use tl_count_flag and tl_get_flag_at. + * + * @param flag The flag to get. + * + * @return The value of the flag, or NULL if not found or no value. + */ +const char *tl_get_flag(const char *flag); + +/** + * @brief Returns the number of times a flag was given. + * + * @param flag The flag to count. + * + * @return The occurrence count (0 if not given). + */ +size_t tl_count_flag(const char *flag); + +/** + * @brief Returns the value of a repeated flag at a given index. + * + * Occurrences are indexed in the order they appeared on the command line. + * + * @param flag The flag to get. + * @param index The occurrence index (0-based). + * + * @return The value, or NULL if out of range or no value at that index. + */ +const char *tl_get_flag_at(const char *flag, size_t index); + +/** + * @brief Returns the number of positional arguments. + * + * Positionals are bare arguments (not starting with `--`) and everything + * after a bare `--` terminator, in the order they appeared. + * + * @return The positional argument count. + */ +size_t tl_count_positional(void); + +/** + * @brief Returns the positional argument at the given index. + * + * @param index The positional index (0-based). + * + * @return The positional value, or NULL if out of range. + */ +const char *tl_get_positional(size_t index); + +#endif // TL_FLAG_H diff --git a/src/tl_app.c b/src/tl_app.c index 71bb3e7..df57e5a 100644 --- a/src/tl_app.c +++ b/src/tl_app.c @@ -2,13 +2,8 @@ #include "tl_app.h" #include "tl_config.h" -#include +#include "tl_flag.h" #include -#include - -// Init vars -static int arg_count = 0; -static char **args = NULL; void tl_init_app(int argc, char *argv[]) { tl_parse_args(argc, argv); @@ -16,37 +11,3 @@ void tl_init_app(int argc, char *argv[]) { tl_set_debug_level((int)strtol(tl_get_flag("--debug-level"), NULL, 10)); } } - -void tl_parse_args(int argc, char *argv[]) { - arg_count = argc; - args = argv; -} - -bool tl_lookup_flag(const char *flag) { - for (int i = 1; i < arg_count; i++) { - // If the argument starts with the flag and is followed by either '\0' or '=' then - if (strncmp(args[i], flag, strlen(flag)) == 0 && - (args[i][strlen(flag)] == '\0' || args[i][strlen(flag)] == '=')) { - return true; - } - } - return false; -} - -const char *tl_get_flag(const char *flag) { - size_t flag_len = strlen(flag); - for (int i = 1; i < arg_count; i++) { - if (strncmp(args[i], flag, flag_len) != 0) { - continue; - } - // If the argument is followed by '=' then return the value after '=' - if (args[i][flag_len] == '=') { - return args[i] + flag_len + 1; - } - // If the argument is an exact match and the next argument exists then return it - if (args[i][flag_len] == '\0' && i + 1 < arg_count) { - return args[i + 1]; - } - } - return NULL; -} diff --git a/src/tl_flag.c b/src/tl_flag.c new file mode 100644 index 0000000..8d3d2de --- /dev/null +++ b/src/tl_flag.c @@ -0,0 +1,265 @@ +// See LICENSE.txt and CONTRIBUTING.md for details. + +#include "tl_flag.h" +#include +#include + +// Init vars +static tl_flag_t *flags = NULL; +static size_t flag_count = 0; +static const char **positionals = NULL; +static size_t positional_count = 0; +static char *line_buf = NULL; +static char **line_tokens = NULL; + +/** + * @brief Returns whether the token is a long flag (starts with "--" and has content). + */ +static bool is_long_flag(const char *s) { + return s != NULL && s[0] == '-' && s[1] == '-' && s[2] != '\0'; +} + +/** + * @brief Returns whether the token is the bare "--" terminator. + */ +static bool is_dash_dash(const char *s) { + return s != NULL && s[0] == '-' && s[1] == '-' && s[2] == '\0'; +} + +/** + * @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) { + if (f->name_len != name_len) { + return false; + } + return memcmp(f->name, name, name_len) == 0; +} + +/** + * @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 "--") and positionals + * (everything else, plus anything after a bare "--"). + */ +static bool parse_tokens(char **tokens, int count) { + if (count <= 1 || !tokens) { + return true; + } + + flags = (tl_flag_t *)calloc((size_t)count, sizeof(*flags)); + positionals = (const char **)calloc((size_t)count, sizeof(*positionals)); + if (!flags || !positionals) { + return false; + } + + bool after_dd = false; + for (int i = 1; i < count; i++) { + char *tok = tokens[i]; + if (!tok) { + continue; + } + // After "--" everything is positional + if (after_dd) { + positionals[positional_count++] = tok; + continue; + } + // Bare "--" terminator + if (is_dash_dash(tok)) { + after_dd = true; + continue; + } + // Long flag + if (is_long_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_long_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; + } + flag_count++; + continue; + } + // Positional + positionals[positional_count++] = tok; + } + return true; +} + +/** + * @brief Splits a command line string into tokens stored in line_tokens. + * + * 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. + */ +static int tokenize_line(const char *line) { + size_t len = strlen(line); + line_buf = malloc(len + 1); + if (!line_buf) { + return -1; + } + + // 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; + } + + size_t n = 0; + size_t bi = 0; + size_t i = 0; + while (i < len) { + // Skip leading whitespace + while (i < len && (line[i] == ' ' || line[i] == '\t')) { + i++; + } + if (i >= len) { + break; + } + // Start a new token at the current buffer position + line_tokens[n++] = &line_buf[bi]; + bool in_quote = false; + while (i < len) { + char c = line[i]; + if (!in_quote && (c == ' ' || c == '\t')) { + break; + } + if (c == '"') { + in_quote = in_quote ? false : true; + i++; + continue; + } + if (c == '\\' && i + 1 < len) { + line_buf[bi++] = line[i + 1]; + i += 2; + continue; + } + line_buf[bi++] = c; + i++; + } + if (in_quote) { + return -1; // unterminated quoted string + } + line_buf[bi++] = '\0'; + } + return (int)n; +} + +void tl_parse_args(int argc, char *argv[]) { + tl_free_args(); + if (!parse_tokens(argv, argc)) { + tl_free_args(); + } +} + +bool tl_parse_line(const char *line) { + tl_free_args(); + if (!line) { + return false; + } + int n = tokenize_line(line); + if (n < 0) { + tl_free_args(); + return false; + } + if (!parse_tokens(line_tokens, n)) { + tl_free_args(); + return false; + } + return true; +} + +void tl_free_args(void) { + if (flags) { + free(flags); + flags = NULL; + } + flag_count = 0; + if (positionals) { + free((void *)positionals); + positionals = NULL; + } + positional_count = 0; + if (line_tokens) { + free((void *)line_tokens); + line_tokens = NULL; + } + if (line_buf) { + free(line_buf); + line_buf = NULL; + } +} + +bool tl_lookup_flag(const char *flag) { + if (!flag) { + return false; + } + size_t flen = strlen(flag); + for (size_t i = 0; i < flag_count; i++) { + if (flag_matches(&flags[i], flag, flen)) { + return true; + } + } + return false; +} + +const char *tl_get_flag(const char *flag) { + return tl_get_flag_at(flag, 0); +} + +size_t tl_count_flag(const char *flag) { + if (!flag) { + return 0; + } + 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)) { + n++; + } + } + return n; +} + +const char *tl_get_flag_at(const char *flag, size_t index) { + if (!flag) { + return NULL; + } + 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 (k == index) { + return flags[i].value; + } + k++; + } + } + return NULL; +} + +size_t tl_count_positional(void) { + return positional_count; +} + +const char *tl_get_positional(size_t index) { + if (index >= positional_count) { + return NULL; + } + return positionals[index]; +} diff --git a/tests/unit/tl_app_test.c b/tests/unit/tl_app_test.c index 750f557..97163f0 100644 --- a/tests/unit/tl_app_test.c +++ b/tests/unit/tl_app_test.c @@ -1,4 +1,5 @@ #include "tl_app.h" +#include "tl_flag.h" #include "unity.h" void setUp(void) { @@ -6,7 +7,7 @@ void setUp(void) { } void tearDown(void) { - // Teardown code if needed + tl_free_args(); } static void test_tl_init_app(void) { @@ -16,32 +17,6 @@ static void test_tl_init_app(void) { TEST_ASSERT_EQUAL_STRING("2", tl_get_flag("--debug-level")); } -static void test_tl_parse_args(void) { - char *argv[] = {"program", "--test-flag"}; - tl_parse_args(2, argv); - TEST_ASSERT_TRUE(tl_lookup_flag("--test-flag")); -} - -static void test_tl_lookup_flag(void) { - char *argv[] = {"program", "--test-flag"}; - tl_init_app(2, argv); - TEST_ASSERT_TRUE(tl_lookup_flag("--test-flag")); - TEST_ASSERT_FALSE(tl_lookup_flag("--nonexistent-flag")); -} - -static void test_tl_get_flag(void) { - char *argv[] = {"program", "--key=value"}; - tl_init_app(2, argv); - TEST_ASSERT_EQUAL_STRING("value", tl_get_flag("--key")); - TEST_ASSERT_NULL(tl_get_flag("--nonexistent-key")); -} - -static void test_tl_get_flag_space(void) { - char *argv[] = {"program", "--key", "value"}; - tl_init_app(3, argv); - TEST_ASSERT_EQUAL_STRING("value", tl_get_flag("--key")); -} - static void test_tl_init_app_space(void) { char *argv[] = {"program", "--debug-level", "3"}; tl_init_app(3, argv); @@ -53,10 +28,6 @@ int main(void) { UNITY_BEGIN(); RUN_TEST(test_tl_init_app); - RUN_TEST(test_tl_parse_args); - RUN_TEST(test_tl_lookup_flag); - RUN_TEST(test_tl_get_flag); - RUN_TEST(test_tl_get_flag_space); RUN_TEST(test_tl_init_app_space); return UNITY_END(); diff --git a/tests/unit/tl_flag_test.c b/tests/unit/tl_flag_test.c new file mode 100644 index 0000000..e13cfe9 --- /dev/null +++ b/tests/unit/tl_flag_test.c @@ -0,0 +1,176 @@ +#include "tl_flag.h" +#include "unity.h" + +void setUp(void) { + // Setup code if needed +} + +void tearDown(void) { + tl_free_args(); +} + +static void test_tl_parse_args(void) { + char *argv[] = {"program", "--test-flag"}; + tl_parse_args(2, argv); + TEST_ASSERT_TRUE(tl_lookup_flag("--test-flag")); +} + +static void test_tl_lookup_flag(void) { + char *argv[] = {"program", "--test-flag"}; + tl_parse_args(2, argv); + TEST_ASSERT_TRUE(tl_lookup_flag("--test-flag")); + TEST_ASSERT_FALSE(tl_lookup_flag("--nonexistent-flag")); +} + +static void test_tl_get_flag(void) { + char *argv[] = {"program", "--key=value"}; + tl_parse_args(2, argv); + TEST_ASSERT_EQUAL_STRING("value", tl_get_flag("--key")); + TEST_ASSERT_NULL(tl_get_flag("--nonexistent-key")); +} + +static void test_tl_get_flag_space(void) { + char *argv[] = {"program", "--key", "value"}; + tl_parse_args(3, argv); + TEST_ASSERT_EQUAL_STRING("value", tl_get_flag("--key")); +} + +static void test_tl_flag_exact_match(void) { + char *argv[] = {"program", "--foobar=1"}; + tl_parse_args(2, argv); + TEST_ASSERT_FALSE(tl_lookup_flag("--foo")); + TEST_ASSERT_NULL(tl_get_flag("--foo")); + TEST_ASSERT_EQUAL_STRING("1", tl_get_flag("--foobar")); +} + +static void test_tl_repeated_flag_equals(void) { + char *argv[] = {"program", "--foo=x", "--foo=y", "--foo=z"}; + tl_parse_args(4, argv); + TEST_ASSERT_EQUAL_UINT(3, tl_count_flag("--foo")); + TEST_ASSERT_EQUAL_STRING("x", tl_get_flag_at("--foo", 0)); + TEST_ASSERT_EQUAL_STRING("y", tl_get_flag_at("--foo", 1)); + TEST_ASSERT_EQUAL_STRING("z", tl_get_flag_at("--foo", 2)); + TEST_ASSERT_NULL(tl_get_flag_at("--foo", 3)); + TEST_ASSERT_EQUAL_STRING("x", tl_get_flag("--foo")); +} + +static void test_tl_repeated_flag_mixed(void) { + char *argv[] = {"program", "--foo=x", "--foo", "y", "--foo=z"}; + tl_parse_args(5, argv); + TEST_ASSERT_EQUAL_UINT(3, tl_count_flag("--foo")); + TEST_ASSERT_EQUAL_STRING("x", tl_get_flag_at("--foo", 0)); + TEST_ASSERT_EQUAL_STRING("y", tl_get_flag_at("--foo", 1)); + TEST_ASSERT_EQUAL_STRING("z", tl_get_flag_at("--foo", 2)); +} + +static void test_tl_repeated_boolean_flag(void) { + char *argv[] = {"program", "--verbose", "--verbose", "--verbose"}; + tl_parse_args(4, argv); + TEST_ASSERT_EQUAL_UINT(3, tl_count_flag("--verbose")); + TEST_ASSERT_NULL(tl_get_flag_at("--verbose", 0)); + TEST_ASSERT_NULL(tl_get_flag_at("--verbose", 1)); + TEST_ASSERT_NULL(tl_get_flag_at("--verbose", 2)); +} + +static void test_tl_adjacent_space_flags(void) { + char *argv[] = {"program", "--foo", "x", "--bar", "y"}; + tl_parse_args(5, argv); + TEST_ASSERT_EQUAL_STRING("x", tl_get_flag("--foo")); + TEST_ASSERT_EQUAL_STRING("y", tl_get_flag("--bar")); +} + +static void test_tl_space_flag_trailing_boolean(void) { + char *argv[] = {"program", "--foo", "x", "--bar"}; + tl_parse_args(4, argv); + TEST_ASSERT_EQUAL_STRING("x", tl_get_flag("--foo")); + TEST_ASSERT_TRUE(tl_lookup_flag("--bar")); + TEST_ASSERT_NULL(tl_get_flag("--bar")); +} + +static void test_tl_positional_terminator(void) { + char *argv[] = {"program", "command", "--foo", "--", "--baz", "--qux"}; + tl_parse_args(6, argv); + TEST_ASSERT_TRUE(tl_lookup_flag("--foo")); + TEST_ASSERT_NULL(tl_get_flag("--foo")); + TEST_ASSERT_FALSE(tl_lookup_flag("--baz")); + TEST_ASSERT_FALSE(tl_lookup_flag("--qux")); + TEST_ASSERT_EQUAL_UINT(3, tl_count_positional()); + TEST_ASSERT_EQUAL_STRING("command", tl_get_positional(0)); + TEST_ASSERT_EQUAL_STRING("--baz", tl_get_positional(1)); + TEST_ASSERT_EQUAL_STRING("--qux", tl_get_positional(2)); + TEST_ASSERT_NULL(tl_get_positional(3)); +} + +static void test_tl_positional_interleaved(void) { + char *argv[] = {"program", "foo", "--flag", "val", "bar"}; + tl_parse_args(5, argv); + TEST_ASSERT_EQUAL_STRING("val", tl_get_flag("--flag")); + TEST_ASSERT_EQUAL_UINT(2, tl_count_positional()); + TEST_ASSERT_EQUAL_STRING("foo", tl_get_positional(0)); + TEST_ASSERT_EQUAL_STRING("bar", tl_get_positional(1)); +} + +static void test_tl_quoted_value_from_argv(void) { + char *argv[] = {"program", "--foo", "bar baz qux"}; + tl_parse_args(3, argv); + TEST_ASSERT_EQUAL_STRING("bar baz qux", tl_get_flag("--foo")); +} + +static void test_tl_parse_line_quoted(void) { + TEST_ASSERT_TRUE(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_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_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)); + TEST_ASSERT_EQUAL_STRING("x y", tl_get_flag("--bar")); + TEST_ASSERT_EQUAL_UINT(2, tl_count_positional()); + TEST_ASSERT_EQUAL_STRING("cmd", tl_get_positional(0)); + TEST_ASSERT_EQUAL_STRING("--baz", tl_get_positional(1)); +} + +static void test_tl_parse_line_unterminated(void) { + TEST_ASSERT_FALSE(tl_parse_line("program --foo \"unterminated")); +} + +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_STRING("v", tl_get_flag("--flag")); + TEST_ASSERT_EQUAL_UINT(3, tl_count_positional()); + TEST_ASSERT_EQUAL_STRING("first pos", tl_get_positional(0)); + TEST_ASSERT_EQUAL_STRING("after dd", tl_get_positional(1)); + TEST_ASSERT_EQUAL_STRING("plain", tl_get_positional(2)); +} + +int main(void) { + UNITY_BEGIN(); + + RUN_TEST(test_tl_parse_args); + RUN_TEST(test_tl_lookup_flag); + RUN_TEST(test_tl_get_flag); + RUN_TEST(test_tl_get_flag_space); + RUN_TEST(test_tl_flag_exact_match); + RUN_TEST(test_tl_repeated_flag_equals); + RUN_TEST(test_tl_repeated_flag_mixed); + RUN_TEST(test_tl_repeated_boolean_flag); + RUN_TEST(test_tl_adjacent_space_flags); + RUN_TEST(test_tl_space_flag_trailing_boolean); + RUN_TEST(test_tl_positional_terminator); + RUN_TEST(test_tl_positional_interleaved); + RUN_TEST(test_tl_quoted_value_from_argv); + RUN_TEST(test_tl_parse_line_quoted); + 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_quoted_positional); + + return UNITY_END(); +}