diff options
| author | Mikhail Romanko <me@blankhex.com> | 2025-02-28 09:09:58 +0300 |
|---|---|---|
| committer | Mikhail Romanko <me@blankhex.com> | 2025-02-28 09:09:58 +0300 |
| commit | 37feb762cb627bb168ddfb72fcddc57b2089242d (patch) | |
| tree | b4513c1fa0ce3bf26ebf8b2c8becd9f994354f52 | |
| parent | cae66889a1ea21e4906f8d5b29719e9cc979bcaf (diff) | |
| download | bhlib-37feb762cb627bb168ddfb72fcddc57b2089242d.tar.gz | |
Add argument parsing
Added (IMHO) relatively simple argument parsing function (as well as helper
function for printing help information).
| -rw-r--r-- | CMakeLists.txt | 2 | ||||
| -rw-r--r-- | include/BH/Args.h | 70 | ||||
| -rw-r--r-- | src/Args.c | 219 | ||||
| -rw-r--r-- | test/src/TestArgs.c | 403 |
4 files changed, 694 insertions, 0 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index 82e6123..5e0a380 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -67,6 +67,7 @@ endif(TESTING) # Set library code set(BH_SOURCE src/Algo.c + src/Args.c src/Hashmap.c src/IO.c src/Math.c @@ -76,6 +77,7 @@ set(BH_SOURCE set(BH_HEADER include/BH/Common.h include/BH/Algo.h + include/BH/Args.h include/BH/Hashmap.h include/BH/IO.h include/BH/Math.h diff --git a/include/BH/Args.h b/include/BH/Args.h new file mode 100644 index 0000000..9932060 --- /dev/null +++ b/include/BH/Args.h @@ -0,0 +1,70 @@ +#ifndef BH_ARGS_H +#define BH_ARGS_H + + +#include "Common.h" + + +#define BH_ARGS_ARGUMENT 0 +#define BH_ARGS_UNKNOWN -1 + +#define BH_ARGS_VALUE 1 +#define BH_ARGS_OPTIONAL 2 + + +typedef struct BH_ArgsOption +{ + int key; + const char *name; + int flags; + const char *description; +} BH_ArgsOption; + + +typedef int (*BH_ArgsCallback)(int key, char *arg, void *data); + + +/** + * Parses command line \a options (given by \a argc and \a argv) and calls the + * specified \a callback with \a data. + * + * There are few internal rules: + * - If the "--" argument is specified, option parsing stops, and the rest of + * the arguments are processed as is. + * - If the "-" argument is specified, it is processed as is. + * - For long options, if a value is required, the string after the equal sign + * or the next argument will be used as the value ("--define=Value" or + * "--define Value"). + * - For short options, if a value is required, the string after the option + * letter or the next argument will be used as the value ("-DValue" or + * "-D Value"). + * + * Options array should have last element filled with zeros. + * + * \param argc Arguments count + * \param argv Arguments array + * \param options Options array pointer + * \param callback Callback function + * \param data Callback data + * + * \return On success, returns zero value. + * \return On failure, returns error code. + */ +int BH_ArgsParse(int argc, + char **argv, + BH_ArgsOption *options, + BH_ArgsCallback callback, + void *data); + + +/** + * Prints documentation for \a options (with specified \a padding). + * + * \param options Options array pointer + * \param padding Padding + */ +void BH_ArgsHelp(BH_ArgsOption *options, + int padding); + + +#endif /* BH_ARGS_H */
\ No newline at end of file diff --git a/src/Args.c b/src/Args.c new file mode 100644 index 0000000..e780fed --- /dev/null +++ b/src/Args.c @@ -0,0 +1,219 @@ +#include <BH/Args.h> +#include <stdio.h> +#include <ctype.h> +#include <string.h> +#include <memory.h> + + +static int BH_ArgsExtractArg(int argc, + char **argv, + BH_ArgsOption *option, + BH_ArgsCallback callback, + void *data, + int *i, + int isLong, + char *next) +{ + if (isLong && *next == '=') + return callback(option->key, next + 1, data); + + if (!isLong && *next) + return callback(option->key, next, data); + + if ((*i) + 1 < argc && !(argv[(*i) + 1][0] == '-' && argv[(*i) + 1][1])) + return callback(option->key, argv[++(*i)], data); + + if (option->flags & BH_ARGS_OPTIONAL) + return callback(option->key, NULL, data); + + return BH_ERROR; +} + + +static int BH_ArgsParseShort(int argc, + char **argv, + BH_ArgsOption *options, + BH_ArgsCallback callback, + void *data, + int *i) +{ + char *symbol; + BH_ArgsOption *option; + + for (symbol = argv[*i] + 1; *symbol; symbol++) + { + /* Find and process option */ + for (option = options; option->key || option->name; option++) + { + if (option->key > 128 || *symbol != option->key) + continue; + + if (option->flags & BH_ARGS_VALUE) + return BH_ArgsExtractArg(argc, argv, option, callback, data, i, 0, symbol + 1); + + if (callback(option->key, NULL, data)) + return BH_ERROR; + + break; + } + + /* Unknown option */ + if (!option->key && !option->name) + { + callback(BH_ARGS_UNKNOWN, argv[*i], data); + return BH_ERROR; + } + } + + return BH_OK; +} + + +static int BH_ArgsParseLong(int argc, + char **argv, + BH_ArgsOption *options, + BH_ArgsCallback callback, + void *data, + int *i) +{ + char *start, *end; + BH_ArgsOption *option; + + /* Extract option name from argv */ + start = argv[*i] + 2; + end = argv[*i] + 2; + while (isgraph(*end) && *end != '=') + end++; + + if (end == start) + return BH_ERROR; + + /* Find and process option */ + for (option = options; option->key || option->name; option++) + { + if (!option->name || strlen(option->name) != (size_t)(end - start)) + continue; + + if (memcmp(option->name, start, end - start)) + continue; + + if (option->flags & BH_ARGS_VALUE) + return BH_ArgsExtractArg(argc, argv, option, callback, data, i, 1, end); + + return callback(option->key, NULL, data); + } + + /* Unknown long option */ + callback(BH_ARGS_UNKNOWN, argv[*i], data); + return BH_ERROR; +} + + +int BH_ArgsParse(int argc, + char **argv, + BH_ArgsOption *options, + BH_ArgsCallback callback, + void *data) +{ + int i, j, ignoreRest, size; + char *arg; + + ignoreRest = 0; i = 1; size = argc; + while (i < argc) + { + arg = argv[i]; + + if (arg[0] == '-' && arg[1] && !ignoreRest) + { + /* Parse ingore, short or long option */ + if (arg[1] == '-') + { + if (arg[2] && BH_ArgsParseLong(argc, argv, options, callback, data, &i)) + return BH_ERROR; + else if (!arg[2]) + ignoreRest = 1; + + } + else if (arg[1] && BH_ArgsParseShort(argc, argv, options, callback, data, &i)) + return BH_ERROR; + + i++; + } + else + { + /* Shift arguments */ + for (j = i; j < size - 1; j++) + argv[j] = argv[j + 1]; + argv[size - 1] = arg; + + if (callback(BH_ARGS_ARGUMENT, arg, data)) + return BH_ERROR; + + argc--; + } + } + return BH_OK; +} + + +void BH_ArgsHelp(BH_ArgsOption *options, + int padding) +{ + int width = 0; + BH_ArgsOption *option; + + if (padding == 0) + padding = 30; + + /* Calculate option column width */ + for (option = options; option->key || option->name; option++) + { + const char *symbol; + width = 5; + + /* Print option column with padding */ + if (isgraph(option->key)) + { + fputs(" -", stdout); + fputc(option->key, stdout); + } + else + fputs(" ", stdout); + + if (isgraph(option->key) && option->name) + fputs(", ", stdout); + else + fputs(" ", stdout); + + if (option->name) + { + width += 3 + strlen(option->name); + fputs("--", stdout); + fputs(option->name, stdout); + fputc(' ', stdout); + } + + if (option->flags & BH_ARGS_VALUE) + { + width += 8; + if (option->flags & BH_ARGS_OPTIONAL) + { + width += 2; + fputs("[<value>] ", stdout); + } + else + fputs("<value> ", stdout); + } + for (; width++ < padding; fputc(' ', stdout)); + + /* Print description */ + for (symbol = option->description; symbol && *symbol; symbol++) + { + fputc(*symbol, stdout); + + if (*symbol == '\n') + for (width = 0; width++ < padding; fputc(' ', stdout)); + } + fputc('\n', stdout); + } +} diff --git a/test/src/TestArgs.c b/test/src/TestArgs.c new file mode 100644 index 0000000..4c2f6ae --- /dev/null +++ b/test/src/TestArgs.c @@ -0,0 +1,403 @@ +#include <BH/Args.h> +#include <BH/Unit.h> +#include <string.h> + + +typedef struct DBG_State +{ + int key[128]; + char *args[128]; + size_t size; +} DBG_State; + + +static int DBG_Process(int key, + char *arg, + void *data) +{ + DBG_State *state = (DBG_State *)data; + + state->key[state->size] = key; + state->args[state->size] = arg; + state->size++; + + return BH_OK; +} + + +static int DBG_AlwaysPass(int key, + char *arg, + void *data) +{ + BH_UNUSED(key); + BH_UNUSED(arg); + BH_UNUSED(data); + + return BH_OK; +} + + +static int DBG_AlwaysFail(int key, + char *arg, + void *data) +{ + BH_UNUSED(key); + BH_UNUSED(arg); + BH_UNUSED(data); + + return BH_ERROR; +} + + +BH_UNIT_TEST(Empty) +{ + int argc = 1; + + char *argv[] = { + "./app" + }; + + BH_ArgsOption options[] = { + {0, NULL, 0, NULL} + }; + + DBG_State state; + memset(&state, 0, sizeof(state)); + + BH_VERIFY(BH_ArgsParse(argc, argv, options, DBG_AlwaysFail, NULL) == BH_OK); + BH_VERIFY(BH_ArgsParse(argc, argv, options, DBG_AlwaysPass, NULL) == BH_OK); + BH_VERIFY(BH_ArgsParse(argc, argv, options, DBG_Process, &state) == BH_OK); + BH_VERIFY(state.size == 0); + BH_VERIFY(strcmp(argv[0], "./app") == 0); + + return 0; +} + + +BH_UNIT_TEST(Args) +{ + int argc = 4; + + char *argv[] = { + "./app", + "a", + "b", + "c" + }; + + BH_ArgsOption options[] = { + {0, NULL, 0, NULL} + }; + + DBG_State state; + memset(&state, 0, sizeof(state)); + + BH_VERIFY(BH_ArgsParse(argc, argv, options, DBG_Process, &state) == BH_OK); + BH_VERIFY(state.size == 3); + BH_VERIFY(state.key[0] == 0 && strcmp(state.args[0], "a") == 0); + BH_VERIFY(state.key[1] == 0 && strcmp(state.args[1], "b") == 0); + BH_VERIFY(state.key[2] == 0 && strcmp(state.args[2], "c") == 0); + + BH_VERIFY(strcmp(argv[0], "./app") == 0); + BH_VERIFY(strcmp(argv[1], "a") == 0); + BH_VERIFY(strcmp(argv[2], "b") == 0); + BH_VERIFY(strcmp(argv[3], "c") == 0); + + return 0; +} + + +BH_UNIT_TEST(Dash) +{ + int argc = 5; + + char *argv[] = { + "./app", + "a", + "--", + "-a", + "--b" + }; + + BH_ArgsOption options[] = { + {0, NULL, 0, NULL} + }; + + DBG_State state; + memset(&state, 0, sizeof(state)); + + BH_VERIFY(BH_ArgsParse(argc, argv, options, DBG_Process, &state) == BH_OK); + + BH_VERIFY(state.size == 3); + BH_VERIFY(state.key[0] == 0 && strcmp(state.args[0], "a") == 0); + BH_VERIFY(state.key[1] == 0 && strcmp(state.args[1], "-a") == 0); + BH_VERIFY(state.key[2] == 0 && strcmp(state.args[2], "--b") == 0); + + BH_VERIFY(strcmp(argv[0], "./app") == 0); + BH_VERIFY(strcmp(argv[1], "--") == 0); + BH_VERIFY(strcmp(argv[2], "a") == 0); + BH_VERIFY(strcmp(argv[3], "-a") == 0); + BH_VERIFY(strcmp(argv[4], "--b") == 0); + + return 0; +} + + +BH_UNIT_TEST(ShortOption) +{ + int argc = 12; + + char *argv[] = { + "./app", + "a", + "-2", + "b", + "c", + "-2d", + "e", + "-2", + "-", + "f", + "-113", + "-1121", + }; + + BH_ArgsOption options[] = { + {'1', NULL, 0, NULL}, + {'2', NULL, BH_ARGS_VALUE, NULL}, + {'3', NULL, BH_ARGS_VALUE | BH_ARGS_OPTIONAL, NULL}, + {0, NULL, 0, NULL}, + }; + + DBG_State state; + memset(&state, 0, sizeof(state)); + + BH_VERIFY(BH_ArgsParse(argc, argv, options, DBG_Process, &state) == BH_OK); + BH_VERIFY(state.size == 13); + BH_VERIFY(state.key[0] == 0 && strcmp(state.args[0], "a") == 0); + BH_VERIFY(state.key[1] == '2' && strcmp(state.args[1], "b") == 0); + BH_VERIFY(state.key[2] == 0 && strcmp(state.args[2], "c") == 0); + BH_VERIFY(state.key[3] == '2' && strcmp(state.args[3], "d") == 0); + BH_VERIFY(state.key[4] == 0 && strcmp(state.args[4], "e") == 0); + BH_VERIFY(state.key[5] == '2' && strcmp(state.args[5], "-") == 0); + BH_VERIFY(state.key[6] == 0 && strcmp(state.args[6], "f") == 0); + BH_VERIFY(state.key[7] == '1' && state.args[7] == NULL); + BH_VERIFY(state.key[8] == '1' && state.args[8] == NULL); + BH_VERIFY(state.key[9] == '3' && state.args[9] == NULL); + BH_VERIFY(state.key[10] == '1' && state.args[10] == NULL); + BH_VERIFY(state.key[11] == '1' && state.args[11] == NULL); + BH_VERIFY(state.key[12] == '2' && strcmp(state.args[12], "1") == 0); + + BH_VERIFY(strcmp(argv[0], "./app") == 0); + BH_VERIFY(strcmp(argv[1], "-2") == 0); + BH_VERIFY(strcmp(argv[2], "b") == 0); + BH_VERIFY(strcmp(argv[3], "-2d") == 0); + BH_VERIFY(strcmp(argv[4], "-2") == 0); + BH_VERIFY(strcmp(argv[5], "-") == 0); + BH_VERIFY(strcmp(argv[6], "-113") == 0); + BH_VERIFY(strcmp(argv[7], "-1121") == 0); + BH_VERIFY(strcmp(argv[8], "a") == 0); + BH_VERIFY(strcmp(argv[9], "c") == 0); + BH_VERIFY(strcmp(argv[10], "e") == 0); + BH_VERIFY(strcmp(argv[11], "f") == 0); + + return 0; +} + + +BH_UNIT_TEST(LongOptions) +{ + int argc = 12; + + char *argv[] = { + "./app", + "a", + "--def", + "b", + "c", + "--def=d", + "e", + "--def", + "-", + "f", + "--ghj", + "--abc", + }; + + BH_ArgsOption options[] = { + {1000, "abc", 0, NULL}, + {1001, "def", BH_ARGS_VALUE, NULL}, + {1002, "ghj", BH_ARGS_VALUE | BH_ARGS_OPTIONAL, NULL}, + {0, NULL, 0, NULL}, + }; + + DBG_State state; + memset(&state, 0, sizeof(state)); + + BH_VERIFY(BH_ArgsParse(argc, argv, options, DBG_Process, &state) == BH_OK); + BH_VERIFY(state.size == 9); + BH_VERIFY(state.key[0] == 0 && strcmp(state.args[0], "a") == 0); + BH_VERIFY(state.key[1] == 1001 && strcmp(state.args[1], "b") == 0); + BH_VERIFY(state.key[2] == 0 && strcmp(state.args[2], "c") == 0); + BH_VERIFY(state.key[3] == 1001 && strcmp(state.args[3], "d") == 0); + BH_VERIFY(state.key[4] == 0 && strcmp(state.args[4], "e") == 0); + BH_VERIFY(state.key[5] == 1001 && strcmp(state.args[5], "-") == 0); + BH_VERIFY(state.key[6] == 0 && strcmp(state.args[6], "f") == 0); + BH_VERIFY(state.key[7] == 1002 && state.args[7] == NULL); + BH_VERIFY(state.key[8] == 1000 && state.args[8] == NULL); + + BH_VERIFY(strcmp(argv[0], "./app") == 0); + BH_VERIFY(strcmp(argv[1], "--def") == 0); + BH_VERIFY(strcmp(argv[2], "b") == 0); + BH_VERIFY(strcmp(argv[3], "--def=d") == 0); + BH_VERIFY(strcmp(argv[4], "--def") == 0); + BH_VERIFY(strcmp(argv[5], "-") == 0); + BH_VERIFY(strcmp(argv[6], "--ghj") == 0); + BH_VERIFY(strcmp(argv[7], "--abc") == 0); + BH_VERIFY(strcmp(argv[8], "a") == 0); + BH_VERIFY(strcmp(argv[9], "c") == 0); + BH_VERIFY(strcmp(argv[10], "e") == 0); + BH_VERIFY(strcmp(argv[11], "f") == 0); + + return 0; +} + + +BH_UNIT_TEST(Complex) +{ + int argc = 15; + + char *argv[] = { + "./app", + "a", + "-i", + "--input", + "b", + "-i-", + "-i", + "--input", + "-", + "--input", + "--", + "-a", + "-i", + "--input", + "--invalid", + }; + + BH_ArgsOption options[] = { + {'a', "abc", 0, NULL}, + {'i', "input", BH_ARGS_VALUE | BH_ARGS_OPTIONAL, NULL}, + {0, NULL, 0, NULL}, + }; + + DBG_State state; + memset(&state, 0, sizeof(state)); + + BH_VERIFY(BH_ArgsParse(argc, argv, options, DBG_Process, &state) == BH_OK); + + BH_VERIFY(state.size == 11); + BH_VERIFY(state.key[0] == 0 && strcmp(state.args[0], "a") == 0); + BH_VERIFY(state.key[1] == 'i' && state.args[1] == NULL); + BH_VERIFY(state.key[2] == 'i' && strcmp(state.args[2], "b") == 0); + BH_VERIFY(state.key[3] == 'i' && strcmp(state.args[3], "-") == 0); + BH_VERIFY(state.key[4] == 'i' && state.args[4] == NULL); + BH_VERIFY(state.key[5] == 'i' && strcmp(state.args[5], "-") == 0); + BH_VERIFY(state.key[6] == 'i' && state.args[6] == NULL); + BH_VERIFY(state.key[7] == 0 && strcmp(state.args[7], "-a") == 0); + BH_VERIFY(state.key[8] == 0 && strcmp(state.args[8], "-i") == 0); + BH_VERIFY(state.key[9] == 0 && strcmp(state.args[9], "--input") == 0); + BH_VERIFY(state.key[10] == 0 && strcmp(state.args[10], "--invalid") == 0); + + BH_VERIFY(strcmp(argv[0], "./app") == 0); + BH_VERIFY(strcmp(argv[1], "-i") == 0); + BH_VERIFY(strcmp(argv[2], "--input") == 0); + BH_VERIFY(strcmp(argv[3], "b") == 0); + BH_VERIFY(strcmp(argv[4], "-i-") == 0); + BH_VERIFY(strcmp(argv[5], "-i") == 0); + BH_VERIFY(strcmp(argv[6], "--input") == 0); + BH_VERIFY(strcmp(argv[7], "-") == 0); + BH_VERIFY(strcmp(argv[8], "--input") == 0); + BH_VERIFY(strcmp(argv[9], "--") == 0); + BH_VERIFY(strcmp(argv[10], "a") == 0); + BH_VERIFY(strcmp(argv[11], "-a") == 0); + BH_VERIFY(strcmp(argv[12], "-i") == 0); + BH_VERIFY(strcmp(argv[13], "--input") == 0); + BH_VERIFY(strcmp(argv[14], "--invalid") == 0); + + return 0; +} + + +BH_UNIT_TEST(InvalidShort) +{ + int argc = 4; + + char *argv[] = { + "./app", + "a", + "-b", + "-c" + }; + + BH_ArgsOption options[] = { + {0, NULL, 0, NULL} + }; + + DBG_State state; + memset(&state, 0, sizeof(state)); + + BH_VERIFY(BH_ArgsParse(argc, argv, options, DBG_Process, &state) != BH_OK); + + BH_VERIFY(state.size == 2); + BH_VERIFY(state.key[0] == 0 && strcmp(state.args[0], "a") == 0); + BH_VERIFY(state.key[1] == BH_ARGS_UNKNOWN && strcmp(state.args[1], "-b") == 0); + + return 0; +} + + +BH_UNIT_TEST(InvalidLong) +{ + int argc = 4; + + char *argv[] = { + "./app", + "a", + "--b", + "--c" + }; + + BH_ArgsOption options[] = { + {0, NULL, 0, NULL} + }; + + DBG_State state; + memset(&state, 0, sizeof(state)); + + BH_VERIFY(BH_ArgsParse(argc, argv, options, DBG_Process, &state) != BH_OK); + + BH_VERIFY(state.size == 2); + BH_VERIFY(state.key[0] == 0 && strcmp(state.args[0], "a") == 0); + BH_VERIFY(state.key[1] == BH_ARGS_UNKNOWN && strcmp(state.args[1], "--b") == 0); + + return 0; +} + + +int main(int argc, char **argv) +{ + (void)argc; + (void)argv; + + BH_UNIT_ADD(Empty); + BH_UNIT_ADD(Args); + BH_UNIT_ADD(Dash); + BH_UNIT_ADD(ShortOption); + BH_UNIT_ADD(LongOptions); + BH_UNIT_ADD(Complex); + BH_UNIT_ADD(InvalidShort); + BH_UNIT_ADD(InvalidLong); + + return BH_UnitRun(); +} |
