aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMikhail Romanko <me@blankhex.com>2025-02-28 09:09:58 +0300
committerMikhail Romanko <me@blankhex.com>2025-02-28 09:09:58 +0300
commit37feb762cb627bb168ddfb72fcddc57b2089242d (patch)
treeb4513c1fa0ce3bf26ebf8b2c8becd9f994354f52
parentcae66889a1ea21e4906f8d5b29719e9cc979bcaf (diff)
downloadbhlib-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.txt2
-rw-r--r--include/BH/Args.h70
-rw-r--r--src/Args.c219
-rw-r--r--test/src/TestArgs.c403
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();
+}