aboutsummaryrefslogtreecommitdiff
path: root/snag/src
diff options
context:
space:
mode:
authorJack Jamison <jackqjamison@gmail.com>2026-07-22 09:09:18 -0400
committerJack Jamison <jackqjamison@gmail.com>2026-07-22 09:09:18 -0400
commit5365f8fa8d6ebfbf3ba92816abe55a99feff6f25 (patch)
tree563605dabaf611ebb0c1ee1b65aa56690b01b4fd /snag/src
parent165816f7854340c3ca09287aee66968777af61ad (diff)
clean up
Diffstat (limited to 'snag/src')
-rw-r--r--snag/src/libargs/args.c1416
-rw-r--r--snag/src/libargs/args.h217
-rw-r--r--snag/src/libargs/license.txt10
-rw-r--r--snag/src/libargs/readme.txt1
-rw-r--r--snag/src/main.c52
-rw-r--r--snag/src/pkg_db.c157
-rw-r--r--snag/src/pkg_db.h27
-rw-r--r--snag/src/util.c76
-rw-r--r--snag/src/util.h31
9 files changed, 1987 insertions, 0 deletions
diff --git a/snag/src/libargs/args.c b/snag/src/libargs/args.c
new file mode 100644
index 0000000..b004525
--- /dev/null
+++ b/snag/src/libargs/args.c
@@ -0,0 +1,1416 @@
+#include <stdbool.h>
+#include <string.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <limits.h>
+#include <errno.h>
+#include <ctype.h>
+#include <stdarg.h>
+#include <stdint.h>
+#include <assert.h>
+#include "args.h"
+
+
+/* ------------------ */
+/* Utility functions. */
+/* ------------------ */
+
+
+// Prints a message to stderr and exits with a non-zero status code.
+static void exit_with_error(const char* format_string, ...) {
+ fprintf(stderr, "error: ");
+
+ va_list args;
+ va_start(args, format_string);
+ vfprintf(stderr, format_string, args);
+ va_end(args);
+
+ fprintf(stderr, "\n");
+ exit(1);
+}
+
+
+// Prints to an automatically-allocated string. Returns NULL if an encoding
+// error occurs or if sufficient memory cannot be allocated.
+static char* str(const char* format_string, ...) {
+ va_list args;
+
+ va_start(args, format_string);
+ int len = vsnprintf(NULL, 0, format_string, args);
+ if (len < 0) {
+ return NULL;
+ }
+ va_end(args);
+
+ char *string = malloc(len + 1);
+ if (string == NULL) {
+ return NULL;
+ }
+
+ va_start(args, format_string);
+ vsnprintf(string, len + 1, format_string, args);
+ va_end(args);
+
+ return string;
+}
+
+
+// Duplicates a string, automatically allocating memory for the copy.
+// Returns NULL if memory cannot be allocated for the copy.
+static char* str_dup(const char* string) {
+ size_t len = strlen(string) + 1;
+ char *copy = malloc(len);
+ return copy ? memcpy(copy, string, len) : NULL;
+}
+
+
+// Hashes a string using the FNV-1a algorithm.
+static uint32_t str_hash(const char* string) {
+ uint32_t hash = 2166136261u;
+ size_t length = strlen(string);
+ for (size_t i = 0; i < length; i++) {
+ hash ^= (uint8_t)string[i];
+ hash *= 16777619;
+ }
+ return hash;
+}
+
+
+// Attempts to parse a string as an integer value, exiting on failure.
+static int try_str_to_int(const char* string) {
+ char *endptr;
+ errno = 0;
+ long result = strtol(string, &endptr, 0);
+ if (errno == ERANGE || result > INT_MAX || result < INT_MIN) {
+ exit_with_error("'%s' is out of range", string);
+ }
+ if (*endptr != '\0') {
+ exit_with_error("cannot parse '%s' as an integer", string);
+ }
+ return (int) result;
+}
+
+
+// Attempts to parse a string as a double value, exiting on failure.
+static double try_str_to_double(const char* string) {
+ char *endptr;
+ errno = 0;
+ double result = strtod(string, &endptr);
+ if (errno == ERANGE) {
+ exit_with_error("'%s' is out of range", string);
+ }
+ if (*endptr != '\0') {
+ exit_with_error("cannot parse '%s' as a floating-point value", string);
+ }
+ return result;
+}
+
+
+/* --------------------------------- */
+/* Vec: a dynamic array of pointers. */
+/* --------------------------------- */
+
+
+typedef struct {
+ int count;
+ int capacity;
+ void** entries;
+} Vec;
+
+
+static Vec* vec_new(void) {
+ Vec* vec = malloc(sizeof(Vec));
+ if (!vec) {
+ return NULL;
+ }
+ vec->count = 0;
+ vec->capacity = 0;
+ vec->entries = NULL;
+ return vec;
+}
+
+
+static void vec_free(Vec* vec) {
+ if (vec) {
+ free(vec->entries);
+ free(vec);
+ }
+}
+
+
+static bool vec_add(Vec* vec, void* entry) {
+ if (vec->count + 1 > vec->capacity) {
+ int new_capacity = vec->capacity < 8 ? 8 : vec->capacity * 2;
+ void** new_array = realloc(vec->entries, sizeof(void*) * new_capacity);
+ if (!new_array) {
+ return false;
+ }
+ vec->entries = new_array;
+ vec->capacity = new_capacity;
+ }
+ vec->entries[vec->count] = entry;
+ vec->count++;
+ return true;
+}
+
+
+/* ------------------------------------------------------------------- */
+/* Map: a linear-probing hash map with string-keys and pointer-values. */
+/* ------------------------------------------------------------------- */
+
+
+// The map automatically grows to keep count/capacity < MAP_MAX_LOAD.
+// This data structure performs optimally when it's less than half-full.
+#define MAP_MAX_LOAD 0.5
+
+
+typedef struct {
+ char* key;
+ void* value;
+ uint32_t key_hash;
+} MapEntry;
+
+
+typedef struct {
+ int count;
+ int capacity;
+ int max_load_threshold;
+ MapEntry* entries;
+} Map;
+
+
+static Map* map_new(void) {
+ Map* map = malloc(sizeof(Map));
+ if (!map) {
+ return NULL;
+ }
+ map->count = 0;
+ map->capacity = 0;
+ map->max_load_threshold = 0;
+ map->entries = NULL;
+ return map;
+}
+
+
+static void map_free(Map* map) {
+ if (map) {
+ for (int i = 0; i < map->capacity; i++) {
+ MapEntry* entry = &map->entries[i];
+ if (entry->key != NULL) {
+ free(entry->key);
+ }
+ }
+ free(map->entries);
+ free(map);
+ }
+}
+
+
+static MapEntry* map_find(Map* map, const char* key, uint32_t key_hash) {
+ // Capacity is always a power of 2 so we can use bitwise-AND as a fast
+ // modulo operator, i.e. this is equivalent to: index = key_hash % capacity.
+ size_t index = key_hash & (map->capacity - 1);
+
+ for (;;) {
+ MapEntry* entry = &map->entries[index];
+ if (entry->key == NULL) {
+ return entry;
+ } else if (key_hash == entry->key_hash && strcmp(key, entry->key) == 0) {
+ return entry;
+ }
+ index = (index + 1) & (map->capacity - 1);
+ }
+}
+
+
+static bool map_grow(Map* map) {
+ MapEntry* old_entries = map->entries;
+ int old_capacity = map->capacity;
+ int new_capacity = old_capacity < 8 ? 8 : old_capacity * 2;
+
+ MapEntry* new_entries = malloc(sizeof(MapEntry) * new_capacity);
+ if (!new_entries) {
+ return false;
+ }
+
+ for (int i = 0; i < new_capacity; i++) {
+ new_entries[i].key = NULL;
+ }
+
+ map->count = 0;
+ map->capacity = new_capacity;
+ map->max_load_threshold = new_capacity * MAP_MAX_LOAD;
+ map->entries = new_entries;
+
+ for (int i = 0; i < old_capacity; i++) {
+ MapEntry* src = &old_entries[i];
+ if (src->key == NULL) continue;
+
+ MapEntry* dst = map_find(map, src->key, src->key_hash);
+ dst->key = src->key;
+ dst->value = src->value;
+ dst->key_hash = src->key_hash;
+ map->count++;
+ }
+
+ free(old_entries);
+ return true;
+}
+
+
+// Returns true if the key was found.
+static bool map_get(Map* map, const char* key, void** value) {
+ if (map->count == 0) return false;
+
+ uint32_t key_hash = str_hash(key);
+ MapEntry* entry = map_find(map, key, key_hash);
+ if (entry->key == NULL) return false;
+
+ *value = entry->value;
+ return true;
+}
+
+
+// Adds a new entry to the map or updates the value of an existing entry.
+// (Note that the map stores its own internal copy of the key string.)
+static bool map_set(Map* map, const char* key, void* value) {
+ if (map->count == map->max_load_threshold) {
+ if (!map_grow(map)) {
+ return false;
+ }
+ }
+
+ uint32_t key_hash = str_hash(key);
+ MapEntry* entry = map_find(map, key, key_hash);
+ if (entry->key == NULL) {
+ char* key_copy = str_dup(key);
+ if (!key_copy) {
+ return false;
+ }
+ map->count++;
+ entry->key = key_copy;
+ entry->value = value;
+ entry->key_hash = key_hash;
+ } else {
+ entry->value = value;
+ }
+
+ return true;
+}
+
+
+// Convenience wrapper for map_set(). This splits the key string into space-
+// separated words and adds a separate entry to the map for each word.
+static bool map_set_splitkey(Map* map, const char* key_string, void* value) {
+ char *copy_of_key_string = str_dup(key_string);
+ if (!copy_of_key_string) {
+ return false;
+ }
+
+ char* word_start = copy_of_key_string;
+ char* string_end = word_start + strlen(copy_of_key_string);
+
+ while (word_start < string_end) {
+ if (*word_start == ' ') {
+ word_start++;
+ continue;
+ }
+
+ char* word_end = word_start;
+ while (true) {
+ if (*word_end == ' ' || *word_end == '\0') {
+ break;
+ }
+ word_end++;
+ }
+ *word_end = '\0';
+
+ if (!map_set(map, word_start, value)) {
+ free(copy_of_key_string);
+ return false;
+ }
+
+ word_start = word_end + 1;
+ }
+
+ free(copy_of_key_string);
+ return true;
+}
+
+
+/* -------- */
+/* Options. */
+/* -------- */
+
+
+typedef enum {
+ OPT_FLAG,
+ OPT_STR,
+ OPT_INT,
+ OPT_DBL,
+} OptionType;
+
+
+typedef union {
+ char* str_val;
+ int int_val;
+ double dbl_val;
+} OptionValue;
+
+
+typedef struct {
+ OptionType type;
+ int count;
+ int capacity;
+ OptionValue* values;
+ OptionValue fallback;
+ bool is_greedy;
+} Option;
+
+
+static void option_free(Option* opt) {
+ if (opt) {
+ free(opt->values);
+ free(opt);
+ }
+}
+
+
+static bool option_append_value(Option* opt, OptionValue value) {
+ if (opt->count + 1 > opt->capacity) {
+ int new_capacity = opt->capacity < 4 ? 4 : opt->capacity * 2;
+ OptionValue* new_array = realloc(opt->values, sizeof(OptionValue) * new_capacity);
+ if (!new_array) {
+ return false;
+ }
+ opt->capacity = new_capacity;
+ opt->values = new_array;
+ }
+ opt->values[opt->count] = value;
+ opt->count++;
+ return true;
+}
+
+
+static bool option_try_set(Option* opt, char* arg) {
+ if (opt->type == OPT_STR) {
+ return option_append_value(opt, (OptionValue){.str_val = arg});
+ }
+ else if (opt->type == OPT_INT) {
+ int value = try_str_to_int(arg);
+ return option_append_value(opt, (OptionValue){.int_val = value});
+ }
+ else if (opt->type == OPT_DBL) {
+ double value = try_str_to_double(arg);
+ return option_append_value(opt, (OptionValue){.dbl_val = value});
+ }
+ assert(false);
+ return false;
+}
+
+
+static Option* option_new(void) {
+ Option *option = malloc(sizeof(Option));
+ if (!option) {
+ return NULL;
+ }
+ option->count = 0;
+ option->capacity = 0;
+ option->values = NULL;
+ option->is_greedy = false;
+ return option;
+}
+
+
+static Option* option_new_flag(void) {
+ Option *opt = option_new();
+ if (!opt) {
+ return NULL;
+ }
+ opt->type = OPT_FLAG;
+ return opt;
+}
+
+
+static Option* option_new_str(char* fallback) {
+ Option *opt = option_new();
+ if (!opt) {
+ return NULL;
+ }
+ opt->type = OPT_STR;
+ opt->fallback = (OptionValue){.str_val = fallback};
+ return opt;
+}
+
+
+static Option* option_new_int(int fallback) {
+ Option *opt = option_new();
+ if (!opt) {
+ return NULL;
+ }
+ opt->type = OPT_INT;
+ opt->fallback = (OptionValue){.int_val = fallback};
+ return opt;
+}
+
+
+static Option* option_new_double(double fallback) {
+ Option *opt = option_new();
+ if (!opt) {
+ return NULL;
+ }
+ opt->type = OPT_DBL;
+ opt->fallback = (OptionValue){.dbl_val = fallback};
+ return opt;
+}
+
+
+static char* option_get_str(Option* opt) {
+ if (opt->count > 0) {
+ return opt->values[opt->count - 1].str_val;
+ }
+ return opt->fallback.str_val;
+}
+
+
+static int option_get_int(Option* opt) {
+ if (opt->count > 0) {
+ return opt->values[opt->count - 1].int_val;
+ }
+ return opt->fallback.int_val;
+}
+
+
+static double option_get_double(Option* opt) {
+ if (opt->count > 0) {
+ return opt->values[opt->count - 1].dbl_val;
+ }
+ return opt->fallback.dbl_val;
+}
+
+
+// Returns the option's values as a freshly-allocated array of string pointers.
+static char** option_get_str_list(Option* opt) {
+ if (opt->count == 0) {
+ return NULL;
+ }
+ char** list = malloc(sizeof(char*) * opt->count);
+ if (!list) {
+ return NULL;
+ }
+ for (int i = 0; i < opt->count; i++) {
+ list[i] = opt->values[i].str_val;
+ }
+ return list;
+}
+
+
+// Returns the option's values as a freshly-allocated array of integers.
+static int* option_get_int_list(Option* opt) {
+ if (opt->count == 0) {
+ return NULL;
+ }
+ int* list = malloc(sizeof(int) * opt->count );
+ if (!list) {
+ return NULL;
+ }
+ for (int i = 0; i < opt->count ; i++) {
+ list[i] = opt->values[i].int_val;
+ }
+ return list;
+}
+
+
+// Returns the option's values as a freshly-allocated array of doubles.
+static double* option_get_double_list(Option* opt) {
+ if (opt->count == 0) {
+ return NULL;
+ }
+ double* list = malloc(sizeof(double) * opt->count );
+ if (!list) {
+ return NULL;
+ }
+ for (int i = 0; i < opt->count ; i++) {
+ list[i] = opt->values[i].dbl_val;
+ }
+ return list;
+}
+
+
+// Returns a freshly-allocated state-string for debugging.
+static char* option_to_str(Option* opt) {
+ if (opt->type == OPT_FLAG) {
+ return str("%i", opt->count);
+ }
+
+ char *fallback = NULL;
+ if (opt->type == OPT_STR) {
+ fallback = str_dup(opt->fallback.str_val);
+ } else if (opt->type == OPT_INT) {
+ fallback = str("%i", opt->fallback.int_val);
+ } else if (opt->type == OPT_DBL) {
+ fallback = str("%f", opt->fallback.dbl_val);
+ }
+
+ char *values = str_dup("");
+ for (int i = 0; i < opt->count; i++) {
+ char *value = NULL;
+ if (opt->type == OPT_STR) {
+ value = str_dup(opt->values[i].str_val);
+ } else if (opt->type == OPT_INT) {
+ value = str("%i", opt->values[i].int_val);
+ } else if (opt->type == OPT_DBL) {
+ value = str("%f", opt->values[i].dbl_val);
+ }
+ char *old_values = values;
+ if (i == 0) {
+ values = str_dup(value);
+ } else {
+ values = str("%s, %s", old_values, value);
+ }
+ free(old_values);
+ free(value);
+ }
+
+ char *output = str("(%s) [%s]", fallback, values);
+ free(fallback);
+ free(values);
+ return output;
+}
+
+
+/* ----------------------------------------------------- */
+/* ArgStream: a wrapper for an array of string pointers. */
+/* ----------------------------------------------------- */
+
+
+typedef struct ArgStream {
+ int count;
+ int index;
+ char** args;
+} ArgStream;
+
+
+static ArgStream* argstream_new(int count, char** args) {
+ ArgStream* stream = malloc(sizeof(ArgStream));
+ if (!stream) {
+ return NULL;
+ }
+ stream->count = count;
+ stream->index = 0;
+ stream->args = args;
+ return stream;
+}
+
+
+static void argstream_free(ArgStream* stream) {
+ free(stream);
+}
+
+
+static char* argstream_next(ArgStream* stream) {
+ return stream->args[stream->index++];
+}
+
+
+static bool argstream_has_next(ArgStream* stream) {
+ return stream->index < stream->count;
+}
+
+
+/* ----------------- */
+/* ArgParser: setup. */
+/* ----------------- */
+
+
+struct ArgParser {
+ char* helptext;
+ char* version;
+ Vec* option_vec;
+ Map* option_map;
+ Vec* command_vec;
+ Map* command_map;
+ Vec* positional_args;
+ ap_callback_t cmd_callback;
+ int cmd_callback_exit_code;
+ char* cmd_name;
+ struct ArgParser* cmd_parser;
+ struct ArgParser* root_parser;
+ bool enable_help_command;
+ bool had_memory_error;
+ struct ArgParser* parent;
+ bool first_pos_arg_ends_option_parsing;
+ bool all_args_as_pos_args;
+ char* zeroth_root_arg;
+};
+
+
+ArgParser* ap_new_parser(void) {
+ ArgParser *parser = malloc(sizeof(ArgParser));
+ if (!parser) {
+ return NULL;
+ }
+
+ parser->helptext = NULL;
+ parser->version = NULL;
+ parser->cmd_callback = NULL;
+ parser->cmd_callback_exit_code = 0;
+ parser->cmd_name = NULL;
+ parser->cmd_parser = NULL;
+ parser->enable_help_command = false;
+ parser->had_memory_error = false;
+ parser->parent = NULL;
+ parser->first_pos_arg_ends_option_parsing = false;
+ parser->all_args_as_pos_args = false;
+ parser->option_vec = NULL;
+ parser->option_map = NULL;
+ parser->command_vec = NULL;
+ parser->command_map = NULL;
+ parser->positional_args = NULL;
+ parser->root_parser = parser;
+ parser->zeroth_root_arg = NULL;
+
+ parser->option_vec = vec_new();
+ if (!parser->option_vec) {
+ ap_free(parser);
+ return NULL;
+ }
+
+ parser->option_map = map_new();
+ if (!parser->option_map) {
+ ap_free(parser);
+ return NULL;
+ }
+
+ parser->command_vec = vec_new();
+ if (!parser->command_vec) {
+ ap_free(parser);
+ return NULL;
+ }
+
+ parser->command_map = map_new();
+ if (!parser->command_map) {
+ ap_free(parser);
+ return NULL;
+ }
+
+ parser->positional_args = vec_new();
+ if (!parser->positional_args) {
+ ap_free(parser);
+ return NULL;
+ }
+
+ return parser;
+}
+
+
+void ap_free(ArgParser* parser) {
+ if (!parser) {
+ return;
+ }
+
+ free(parser->helptext);
+ free(parser->version);
+
+ if (parser->option_map) {
+ map_free(parser->option_map);
+ }
+
+ if (parser->option_vec) {
+ for (int i = 0; i < parser->option_vec->count; i++) {
+ option_free(parser->option_vec->entries[i]);
+ }
+ vec_free(parser->option_vec);
+ }
+
+ if (parser->command_map) {
+ map_free(parser->command_map);
+ }
+
+ if (parser->command_vec) {
+ for (int i = 0; i < parser->command_vec->count; i++) {
+ ap_free(parser->command_vec->entries[i]);
+ }
+ vec_free(parser->command_vec);
+ }
+
+ if (parser->positional_args) {
+ vec_free(parser->positional_args);
+ }
+
+ free(parser);
+}
+
+
+static void ap_set_memory_error_flag(ArgParser* parser) {
+ parser->had_memory_error = true;
+
+ ArgParser* parent = parser->parent;
+ while (parent) {
+ parent->had_memory_error = true;
+ parent = parent->parent;
+ }
+}
+
+
+void ap_set_helptext(ArgParser* parser, const char* helptext) {
+ free(parser->helptext);
+ parser->helptext = NULL;
+
+ if (helptext) {
+ parser->helptext = str_dup(helptext);
+ if (!parser->helptext) {
+ ap_set_memory_error_flag(parser);
+ }
+ }
+}
+
+
+char* ap_get_helptext(ArgParser* parser) {
+ return parser->helptext;
+}
+
+
+void ap_set_version(ArgParser* parser, const char* version) {
+ free(parser->version);
+ parser->version = NULL;
+
+ if (version) {
+ parser->version = str_dup(version);
+ if (!parser->version) {
+ ap_set_memory_error_flag(parser);
+ }
+ }
+}
+
+
+char* ap_get_version(ArgParser* parser) {
+ return parser->version;
+}
+
+
+void ap_first_pos_arg_ends_option_parsing(ArgParser* parser) {
+ parser->first_pos_arg_ends_option_parsing = true;
+}
+
+
+void ap_all_args_as_pos_args(ArgParser* parser) {
+ parser->all_args_as_pos_args = true;
+}
+
+
+/* -------------------------------------- */
+/* ArgParser: register flags and options. */
+/* -------------------------------------- */
+
+
+static void ap_register_option(ArgParser* parser, const char* name, Option* opt) {
+ if (!opt) {
+ ap_set_memory_error_flag(parser);
+ return;
+ }
+
+ if (vec_add(parser->option_vec, opt)) {
+ if (map_set_splitkey(parser->option_map, name, opt)) {
+ return;
+ } else {
+ ap_set_memory_error_flag(parser);
+ parser->option_vec->count--;
+ option_free(opt);
+ return;
+ }
+ } else {
+ ap_set_memory_error_flag(parser);
+ option_free(opt);
+ return;
+ }
+}
+
+
+// Register a new flag.
+void ap_add_flag(ArgParser *parser, const char* name) {
+ Option* opt = option_new_flag();
+ ap_register_option(parser, name, opt);
+}
+
+
+// Register a new string-valued option.
+void ap_add_str_opt(ArgParser* parser, const char* name, const char* fallback) {
+ Option* opt = option_new_str((char*)fallback);
+ ap_register_option(parser, name, opt);
+}
+
+
+// Register a new greedy string-valued option.
+void ap_add_greedy_str_opt(ArgParser* parser, const char* name) {
+ Option* opt = option_new_str((char*)"");
+ if (opt) {
+ opt->is_greedy = true;
+ }
+ ap_register_option(parser, name, opt);
+}
+
+
+// Register a new integer-valued option.
+void ap_add_int_opt(ArgParser* parser, const char* name, int fallback) {
+ Option* opt = option_new_int(fallback);
+ ap_register_option(parser, name, opt);
+}
+
+
+// Register a new double-valued option.
+void ap_add_dbl_opt(ArgParser* parser, const char* name, double fallback) {
+ Option* opt = option_new_double(fallback);
+ ap_register_option(parser, name, opt);
+}
+
+
+/* ---------------------------------- */
+/* ArgParser: flag and option values. */
+/* ---------------------------------- */
+
+
+// Retrieve an Option instance by name.
+static Option* ap_get_opt(ArgParser* parser, const char* name) {
+ void* opt;
+ if (!map_get(parser->option_map, name, &opt)) {
+ exit_with_error("'%s' is not a registered flag or option name", name);
+ }
+ return (Option*)opt;
+}
+
+
+// Returns the number of times the specified flag or option was found.
+int ap_count(ArgParser* parser, const char* name) {
+ Option* opt = ap_get_opt(parser, name);
+ return opt->count;
+}
+
+
+// Returns true if the specified flag or option was found.
+bool ap_found(ArgParser* parser, const char* name) {
+ Option* opt = ap_get_opt(parser, name);
+ return opt->count > 0;
+}
+
+
+// Returns the value of the specified string option.
+char* ap_get_str_value(ArgParser* parser, const char* name) {
+ Option* opt = ap_get_opt(parser, name);
+ return (char*)option_get_str(opt);
+}
+
+
+// Returns the string value at the specified index.
+char* ap_get_str_value_at_index(ArgParser* parser, const char* name, int index) {
+ Option* opt = ap_get_opt(parser, name);
+ return opt->values[index].str_val;
+}
+
+
+// Returns the value of the specified integer option.
+int ap_get_int_value(ArgParser* parser, const char* name) {
+ Option* opt = ap_get_opt(parser, name);
+ return option_get_int(opt);
+}
+
+
+// Returns the integer value at the specified index.
+int ap_get_int_value_at_index(ArgParser* parser, const char* name, int index) {
+ Option* opt = ap_get_opt(parser, name);
+ return opt->values[index].int_val;
+}
+
+
+// Returns the value of the specified floating-point option.
+double ap_get_dbl_value(ArgParser* parser, const char* name) {
+ Option* opt = ap_get_opt(parser, name);
+ return option_get_double(opt);
+}
+
+
+// Returns the floating-point value at the specified index.
+double ap_get_dbl_value_at_index(ArgParser* parser, const char* name, int index) {
+ Option* opt = ap_get_opt(parser, name);
+ return opt->values[index].dbl_val;
+}
+
+
+// Returns an option's values as a freshly-allocated array of string pointers.
+// The array's memory is not affected by calls to ap_free().
+// Returns NULL if memory cannot be allocated for the array.
+char** ap_get_str_values(ArgParser* parser, const char* name) {
+ Option* opt = ap_get_opt(parser, name);
+ return (char**)option_get_str_list(opt);
+}
+
+
+// Returns an option's values as a freshly-allocated array of integers. The
+// array's memory is not affected by calls to ap_free().
+// Returns NULL if memory cannot be allocated for the array.
+int* ap_get_int_values(ArgParser* parser, const char* name) {
+ Option* opt = ap_get_opt(parser, name);
+ return option_get_int_list(opt);
+}
+
+
+// Returns an option's values as a freshly-allocated array of doubles. The
+// array's memory is not affected by calls to ap_free().
+// Returns NULL if memory cannot be allocated for the array.
+double* ap_get_dbl_values(ArgParser* parser, const char* name) {
+ Option* opt = ap_get_opt(parser, name);
+ return option_get_double_list(opt);
+}
+
+
+/* -------------------------------- */
+/* ArgParser: positional arguments. */
+/* -------------------------------- */
+
+
+// Returns true if the parser has found one or more positional arguments.
+bool ap_has_args(ArgParser* parser) {
+ return parser->positional_args->count > 0;
+}
+
+
+// Returns the number of positional arguments.
+int ap_count_args(ArgParser* parser) {
+ return parser->positional_args->count;
+}
+
+
+// Returns the positional argument at the specified index.
+char* ap_get_arg_at_index(ArgParser* parser, int index) {
+ return (char*)parser->positional_args->entries[index];
+}
+
+
+// Returns the positional arguments as a freshly-allocated array of string
+// pointers. The memory occupied by the returned array is not affected by
+// calls to ap_free().
+// Returns NULL if memory cannot be allocated for the array.
+char** ap_get_args(ArgParser* parser) {
+ int count = ap_count_args(parser);
+ char** args = malloc(sizeof(char*) * count);
+ if (!args) {
+ return NULL;
+ }
+ memcpy(args, parser->positional_args->entries, sizeof(char*) * count);
+ return args;
+}
+
+
+// Attempts to parse and return the positional arguments as a freshly
+// allocated array of integers. Exits with an error message on failure. The
+// memory occupied by the returned array is not affected by calls to
+// ap_free().
+// Returns NULL if memory cannot be allocated for the array.
+int* ap_get_args_as_ints(ArgParser* parser) {
+ int count = ap_count_args(parser);
+ int* args = malloc(sizeof(int) * count);
+ if (!args) {
+ return NULL;
+ }
+ for (int i = 0; i < count; i++) {
+ *(args + i) = try_str_to_int(parser->positional_args->entries[i]);
+ }
+ return args;
+}
+
+
+// Attempts to parse and return the positional arguments as a freshly
+// allocated array of doubles. Exits with an error message on failure. The
+// memory occupied by the returned array is not affected by calls to
+// ap_free().
+// Returns NULL if memory cannot be allocated for the array.
+double* ap_get_args_as_doubles(ArgParser* parser) {
+ int count = ap_count_args(parser);
+ double *args = malloc(sizeof(double) * count);
+ if (!args) {
+ return NULL;
+ }
+ for (int i = 0; i < count; i++) {
+ *(args + i) = try_str_to_double(parser->positional_args->entries[i]);
+ }
+ return args;
+}
+
+
+/* -------------------- */
+/* ArgParser: commands. */
+/* -------------------- */
+
+
+ArgParser* ap_new_cmd(ArgParser* parent_parser, const char* name) {
+ ArgParser* cmd_parser = ap_new_parser();
+ if (!cmd_parser) {
+ return NULL;
+ }
+
+ cmd_parser->root_parser = parent_parser->root_parser;
+
+ if (vec_add(parent_parser->command_vec, cmd_parser)) {
+ if (map_set_splitkey(parent_parser->command_map, name, cmd_parser)) {
+ parent_parser->enable_help_command = true;
+ return cmd_parser;
+ } else {
+ parent_parser->command_vec--;
+ ap_free(cmd_parser);
+ return NULL;
+ }
+ } else {
+ ap_free(cmd_parser);
+ return NULL;
+ }
+}
+
+
+void ap_set_cmd_callback(ArgParser* cmd_parser, ap_callback_t cmd_callback) {
+ cmd_parser->cmd_callback = cmd_callback;
+}
+
+
+bool ap_found_cmd(ArgParser* parent_parser) {
+ return parent_parser->cmd_name != NULL;
+}
+
+
+char* ap_get_cmd_name(ArgParser* parent_parser) {
+ return parent_parser->cmd_name;
+}
+
+
+ArgParser* ap_get_cmd_parser(ArgParser* parent_parser) {
+ return parent_parser->cmd_parser;
+}
+
+
+int ap_get_cmd_exit_code(ArgParser* parent_parser) {
+ return parent_parser->cmd_callback_exit_code;
+}
+
+
+void ap_enable_help_command(ArgParser* parent_parser, bool enable) {
+ parent_parser->enable_help_command = enable;
+}
+
+
+ArgParser* ap_get_parent(ArgParser* parser) {
+ return parser->parent;
+}
+
+
+/* --------------------------- */
+/* ArgParser: parse arguments. */
+/* --------------------------- */
+
+
+// Parse an option of the form --name=value or -n=value.
+static void ap_handle_equals_opt(ArgParser* parser, const char* prefix, char* arg, ArgStream* stream) {
+ char* array = str_dup(arg);
+ if (!array) {
+ ap_set_memory_error_flag(parser);
+ return;
+ }
+
+ *strchr(array, '=') = '\0';
+ char* name = array;
+ char* value = strchr(arg, '=') + 1;
+
+ Option* option;
+ bool found = map_get(parser->option_map, name, (void**)&option);
+
+ if (!found) {
+ /* free(array); */
+ exit_with_error("%s%s is not a recognised option name", prefix, name);
+ }
+
+ if (option->type == OPT_FLAG) {
+ /* free(array); */
+ exit_with_error("flag %s%s does not accept an argument", prefix, name);
+ }
+
+ if (strlen(value) == 0) {
+ /* free(array); */
+ exit_with_error("missing argument for %s%s", prefix, name);
+ }
+
+ if (!option_try_set(option, value)) {
+ ap_set_memory_error_flag(parser);
+ }
+
+ if (option->is_greedy) {
+ while (argstream_has_next(stream)) {
+ if (!option_try_set(option, argstream_next(stream))) {
+ ap_set_memory_error_flag(parser);
+ }
+ }
+ }
+
+ free(array);
+}
+
+
+// Parse a long-form option, i.e. an option beginning with a double dash.
+static void ap_handle_long_opt(ArgParser* parser, const char* arg, ArgStream* stream) {
+ Option* option;
+
+ if (map_get(parser->option_map, arg, (void**)&option)) {
+ if (option->type == OPT_FLAG) {
+ option->count++;
+ return;
+ }
+
+ if (argstream_has_next(stream) && option->is_greedy) {
+ while (argstream_has_next(stream)) {
+ if (!option_try_set(option, argstream_next(stream))) {
+ ap_set_memory_error_flag(parser);
+ }
+ }
+ return;
+ }
+
+ if (argstream_has_next(stream)) {
+ if (!option_try_set(option, argstream_next(stream))) {
+ ap_set_memory_error_flag(parser);
+ }
+ return;
+ }
+
+ exit_with_error("missing argument for --%s", arg);
+ }
+
+ if (strcmp(arg, "help") == 0 && parser->helptext != NULL) {
+ puts(parser->helptext);
+ exit(0);
+ }
+
+ if (strcmp(arg, "version") == 0 && parser->version != NULL) {
+ puts(parser->version);
+ exit(0);
+ }
+
+ exit_with_error("--%s is not a recognised flag or option name", arg);
+}
+
+
+// Parse a short-form option, i.e. an option beginning with a single dash.
+static void ap_handle_short_opt(ArgParser* parser, const char* arg, ArgStream* stream) {
+ for (size_t i = 0; i < strlen(arg); i++) {
+ char keystr[] = {arg[i], 0};
+ Option* option;
+
+ bool found = map_get(parser->option_map, keystr, (void**)&option);
+ if (!found) {
+ if (arg[i] == 'h' && parser->helptext != NULL) {
+ puts(parser->helptext);
+ exit(0);
+ }
+ if (arg[i] == 'v' && parser->version != NULL) {
+ puts(parser->version);
+ exit(0);
+ }
+ if (strlen(arg) > 1) {
+ exit_with_error("'%c' in -%s is not a recognised flag or option name", arg[i], arg);
+ }
+ exit_with_error("-%s is not a recognised flag or option name", arg);
+ }
+
+ if (option->type == OPT_FLAG) {
+ option->count++;
+ continue;
+ }
+
+ if (argstream_has_next(stream) && option->is_greedy) {
+ while (argstream_has_next(stream)) {
+ if (!option_try_set(option, argstream_next(stream))) {
+ ap_set_memory_error_flag(parser);
+ }
+ }
+ continue;
+ }
+
+ if (argstream_has_next(stream)) {
+ if (!option_try_set(option, argstream_next(stream))) {
+ ap_set_memory_error_flag(parser);
+ }
+ continue;
+ }
+
+ if (strlen(arg) > 1) {
+ exit_with_error("missing argument for '%c' in -%s", arg[i], arg);
+ }
+
+ exit_with_error("missing argument for -%s", arg);
+ }
+}
+
+
+// Parse a stream of string arguments.
+static void ap_parse_stream(ArgParser* parser, ArgStream* stream) {
+ if (parser->had_memory_error) {
+ return;
+ }
+
+ if (parser->all_args_as_pos_args) {
+ while (argstream_has_next(stream)) {
+ if (!vec_add(parser->positional_args, argstream_next(stream))) {
+ ap_set_memory_error_flag(parser);
+ }
+ }
+ return;
+ }
+
+ while (argstream_has_next(stream)) {
+ ArgParser* cmd_parser;
+ char* arg = argstream_next(stream);
+
+ // If we encounter a '--' argument, turn off option-parsing.
+ if (strcmp(arg, "--") == 0) {
+ while (argstream_has_next(stream)) {
+ if (!vec_add(parser->positional_args, argstream_next(stream))) {
+ ap_set_memory_error_flag(parser);
+ }
+ }
+ }
+
+ // Is the argument a long-form option or flag?
+ else if (strncmp(arg, "--", 2) == 0) {
+ if (strstr(arg, "=") != NULL) {
+ ap_handle_equals_opt(parser, "--", arg + 2, stream);
+ } else {
+ ap_handle_long_opt(parser, arg + 2, stream);
+ }
+ }
+
+ // Is the argument a short-form option or flag?
+ else if (arg[0] == '-') {
+ if (strlen(arg) == 1 || isdigit(arg[1])) {
+ if (!vec_add(parser->positional_args, arg)) {
+ ap_set_memory_error_flag(parser);
+ }
+ } else if (strstr(arg, "=") != NULL) {
+ ap_handle_equals_opt(parser, "-", arg + 1, stream);
+ } else {
+ ap_handle_short_opt(parser, arg + 1, stream);
+ }
+ }
+
+ // Is the argument a registered command?
+ else if (parser->positional_args->count == 0 && map_get(parser->command_map, arg, (void**)&cmd_parser)) {
+ parser->cmd_name = arg;
+ parser->cmd_parser = cmd_parser;
+ ap_parse_stream(cmd_parser, stream);
+ if (cmd_parser->cmd_callback && !parser->had_memory_error) {
+ parser->cmd_callback_exit_code = cmd_parser->cmd_callback(arg, cmd_parser);
+ }
+ }
+
+ // Is the argument the automatic 'help' command?
+ else if (parser->positional_args->count == 0 && parser->enable_help_command && strcmp(arg, "help") == 0) {
+ if (argstream_has_next(stream)) {
+ char* name = argstream_next(stream);
+ if (map_get(parser->command_map, name, (void**)&cmd_parser)) {
+ if (cmd_parser->helptext) {
+ puts(cmd_parser->helptext);
+ }
+ exit(0);
+ } else {
+ exit_with_error("'%s' is not a recognised command", name);
+ }
+ } else {
+ exit_with_error("the 'help' command requires an argument");
+ }
+ }
+
+ // Otherwise add the argument to our list of positionals.
+ else {
+ if (!vec_add(parser->positional_args, arg)) {
+ ap_set_memory_error_flag(parser);
+ }
+ if (parser->first_pos_arg_ends_option_parsing) {
+ while (argstream_has_next(stream)) {
+ if (!vec_add(parser->positional_args, argstream_next(stream))) {
+ ap_set_memory_error_flag(parser);
+ }
+ }
+ }
+ }
+ }
+}
+
+
+// Parse an array of string arguments. We assume that [argc] and [argv] are the arguments passed to
+// main(), i.e. we ignore the first element in the array. In some situations [argv] can be empty,
+// i.e. [argc == 0], which can lead to security vulnerabilities if not explicitly handled.
+bool ap_parse(ArgParser* parser, int argc, char** argv) {
+ if (parser->had_memory_error) {
+ return false;
+ }
+
+ if (argc == 0) {
+ return true;
+ }
+
+ parser->zeroth_root_arg = argv[0];
+
+ ArgStream* stream = argstream_new(argc - 1 , argv + 1);
+ if (!stream) {
+ return false;
+ }
+
+ ap_parse_stream(parser, stream);
+ argstream_free(stream);
+
+ return !parser->had_memory_error;
+}
+
+
+/* --------------------- */
+/* ArgParser: utilities. */
+/* --------------------- */
+
+
+bool ap_had_memory_error(ArgParser* parser) {
+ return parser->had_memory_error;
+}
+
+
+void ap_print(ArgParser* parser) {
+ puts("Flags/Options:");
+ if (parser->option_map->count > 0) {
+ for (int i = 0; i < parser->option_map->capacity; i++) {
+ MapEntry* entry = &parser->option_map->entries[i];
+ if (entry->key != NULL) {
+ Option* opt = entry->value;
+ char* opt_str = option_to_str(opt);
+ printf(" %s: %s\n", entry->key, opt_str);
+ free(opt_str);
+ }
+ }
+ } else {
+ puts(" [none]");
+ }
+
+ puts("\nArguments:");
+ if (parser->positional_args->count > 0) {
+ for (int i = 0; i < parser->positional_args->count; i++) {
+ printf(" %s\n", ap_get_arg_at_index(parser, i));
+ }
+ } else {
+ puts(" [none]");
+ }
+
+ puts("\nCommand:");
+ if (ap_found_cmd(parser)) {
+ printf(" %s\n", ap_get_cmd_name(parser));
+ } else {
+ puts(" [none]");
+ }
+}
+
+
+char* ap_get_zeroth_root_arg(ArgParser* parser) {
+ return parser->root_parser->zeroth_root_arg;
+}
diff --git a/snag/src/libargs/args.h b/snag/src/libargs/args.h
new file mode 100644
index 0000000..ea91142
--- /dev/null
+++ b/snag/src/libargs/args.h
@@ -0,0 +1,217 @@
+// -----------------------------------------------------------------------------
+// Args: a C99 library for parsing command line arguments.
+// Version: 3.3.0
+// -----------------------------------------------------------------------------
+
+#ifndef args_h
+#define args_h
+
+#include <stdbool.h>
+
+// -----------------------------------------------------------------------------
+// Types.
+// -----------------------------------------------------------------------------
+
+// An ArgParser instance stores registered flags, options and commands.
+typedef struct ArgParser ArgParser;
+
+// A callback function should accept two arguments: the command's name and the
+// command's ArgParser instance. It should return an integer status code.
+typedef int (*ap_callback_t)(char* cmd_name, ArgParser* cmd_parser);
+
+// -----------------------------------------------------------------------------
+// Initialization, parsing, teardown.
+// -----------------------------------------------------------------------------
+
+// Allocates and initializes a new ArgParser instance. Returns NULL if memory
+// allocation fails.
+ArgParser* ap_new_parser(void);
+
+// Specifies a helptext string for the parser. If [helptext] is not NULL, this
+// activates an automatic --help/-h flag. (Either --help or -h can be overridden
+// by explicitly registered flags.) The parser stores and manages its own copy
+// of the [helptext] string so [helptext] can be freed immediately after this
+// call if it was dynamically constructed.
+void ap_set_helptext(ArgParser* parser, const char* helptext);
+
+// Returns a pointer to the parser's helptext string.
+char* ap_get_helptext(ArgParser* parser);
+
+// Specifies a version string for the parser. If [version] is not NULL, this
+// activates an automatic --version/-v flag. (Either --version or -v can be
+// overridden by explicitly registered flags.) The parser stores and manages its
+// own copy of the [version] string so [version] can be freed immediately after
+// this call if it was dynamically constructed.
+void ap_set_version(ArgParser* parser, const char* version);
+
+// Returns a pointer to the parser's version string.
+char* ap_get_version(ArgParser* parser);
+
+// Parses an array of string arguments.
+// - Exits with an error message and a non-zero status code if the arguments are
+// invalid.
+// - The parameters are assumed to be [argc] and [argv] as supplied to main(),
+// i.e. the first element of the array is assumed to be the binary name and
+// is therefore ignored.
+// - Returns true if the arguments were successfully parsed.
+// - Returns false if parsing failed because sufficient memory could not be
+// allocated.
+bool ap_parse(ArgParser* parser, int argc, char** argv);
+
+// Frees the memory associated with the parser and any subparsers.
+void ap_free(ArgParser* parser);
+
+// -----------------------------------------------------------------------------
+// Parsing modes.
+// -----------------------------------------------------------------------------
+
+// If set, the first positional argument ends option-parsing; all subsequent
+// arguments will be treated as positionals.
+void ap_first_pos_arg_ends_option_parsing(ArgParser* parser);
+
+// If set, all arguments will be treated as positionals.
+void ap_all_args_as_pos_args(ArgParser* parser);
+
+// -----------------------------------------------------------------------------
+// Register flags and options.
+// -----------------------------------------------------------------------------
+
+// Registers a new flag.
+void ap_add_flag(ArgParser* parser, const char* name);
+
+// Registers a new string-valued option.
+void ap_add_str_opt(ArgParser* parser, const char* name, const char* fallback);
+
+// Registers a new integer-valued option.
+void ap_add_int_opt(ArgParser* parser, const char* name, int fallback);
+
+// Registers a new double-valued option.
+void ap_add_dbl_opt(ArgParser* parser, const char* name, double fallback);
+
+// Registers a new greedy string-valued option.
+void ap_add_greedy_str_opt(ArgParser* parser, const char* name);
+
+// -----------------------------------------------------------------------------
+// Inspect flags and options.
+// -----------------------------------------------------------------------------
+
+// Returns the number of times the specified flag or option was found.
+int ap_count(ArgParser* parser, const char* name);
+
+// Returns true if the specified flag or option was found.
+bool ap_found(ArgParser* parser, const char* name);
+
+// Returns the value of a string option.
+char* ap_get_str_value(ArgParser* parser, const char* name);
+
+// Returns the string value at the specified index.
+char* ap_get_str_value_at_index(ArgParser* parser, const char* name, int index);
+
+// Returns the value of an integer option.
+int ap_get_int_value(ArgParser* parser, const char* name);
+
+// Returns the integer value at the specified index.
+int ap_get_int_value_at_index(ArgParser* parser, const char* name, int index);
+
+// Returns the value of a floating-point option.
+double ap_get_dbl_value(ArgParser* parser, const char* name);
+
+// Returns the floating-point value at the specified index.
+double ap_get_dbl_value_at_index(ArgParser* parser, const char* name, int index);
+
+// Returns an option's values as a freshly-allocated array of string
+// pointers. The array's memory is not affected by calls to ap_free().
+// Returns NULL if memory allocation fails.
+char** ap_get_str_values(ArgParser* parser, const char* name);
+
+// Returns an option's values as a freshly-allocated array of integers.
+// The array's memory is not affected by calls to ap_free().
+// Returns NULL if memory allocation fails.
+int* ap_get_int_values(ArgParser* parser, const char* name);
+
+// Returns an option's values as a freshly-allocated array of doubles.
+// The array's memory is not affected by calls to ap_free().
+// Returns NULL if memory allocation fails.
+double* ap_get_dbl_values(ArgParser* parser, const char* name);
+
+// -----------------------------------------------------------------------------
+// Positional arguments.
+// -----------------------------------------------------------------------------
+
+// Returns true if the parser has found one or more positional arguments.
+bool ap_has_args(ArgParser* parser);
+
+// Returns the number of positional arguments.
+int ap_count_args(ArgParser* parser);
+
+// Returns the positional argument at the specified index.
+char* ap_get_arg_at_index(ArgParser* parser, int index);
+
+// Returns the positional arguments as a freshly-allocated array of string
+// pointers. The memory occupied by the returned array is not affected by
+// calls to ap_free(). Returns NULL if memory allocation fails.
+char** ap_get_args(ArgParser* parser);
+
+// Attempts to parse and return the positional arguments as a freshly allocated
+// array of integers. Exits with an error message on failure. The memory
+// occupied by the returned array is not affected by calls to ap_free().
+// Returns NULL if memory allocation fails.
+int* ap_get_args_as_ints(ArgParser* parser);
+
+// Attempts to parse and return the positional arguments as a freshly allocated
+// array of doubles. Exits with an error message on failure. The memory
+// occupied by the returned array is not affected by calls to ap_free().
+// Returns NULL if memory allocation fails.
+double* ap_get_args_as_doubles(ArgParser* parser);
+
+// -----------------------------------------------------------------------------
+// Commands.
+// -----------------------------------------------------------------------------
+
+// Registers a new command. Returns the ArgParser instance for the command.
+// Returns NULL if sufficient memory cannot be allocated for the new parser.
+ArgParser* ap_new_cmd(ArgParser* parent_parser, const char* name);
+
+// Registers a callback function on a command parser.
+void ap_set_cmd_callback(ArgParser* cmd_parser, ap_callback_t cmd_callback);
+
+// Returns true if [parent_parser] has found a command.
+bool ap_found_cmd(ArgParser* parent_parser);
+
+// If [parent_parser] has found a command, returns its name, otherwise NULL.
+char* ap_get_cmd_name(ArgParser* parent_parser);
+
+// If [parent_parser] has found a command, returns its ArgParser instance,
+// otherwise NULL.
+ArgParser* ap_get_cmd_parser(ArgParser* parent_parser);
+
+// If [parent_parser] has found a command, and if that command has a callback
+// function, returns the exit code from the callback, otherwise 0.
+int ap_get_cmd_exit_code(ArgParser* parent_parser);
+
+// This boolean switch toggles support for an automatic 'help' command that
+// prints subcommand helptext. The value defaults to false but gets toggled
+// automatically to true whenever a command is registered. You can use this
+// function to disable the feature if required.
+void ap_enable_help_command(ArgParser* parent_parser, bool enable);
+
+// If [parser] has a parent -- i.e. if [parser] is a command parser -- returns
+// its parent, otherwise NULL.
+ArgParser* ap_get_parent(ArgParser* parser);
+
+// -----------------------------------------------------------------------------
+// Utilities.
+// -----------------------------------------------------------------------------
+
+// Dumps a parser instance to stdout for debugging.
+void ap_print(ArgParser* parser);
+
+// Returns true if an attempt to allocate memory failed.
+bool ap_had_memory_error(ArgParser* parser);
+
+// Returns the argument supplied at index-zero to the root parser. Typically
+// this is the filepath of the binary. This function can be called on the root
+// parser or any command sub-parser.
+char* ap_get_zeroth_root_arg(ArgParser* parser);
+
+#endif
diff --git a/snag/src/libargs/license.txt b/snag/src/libargs/license.txt
new file mode 100644
index 0000000..d5710a2
--- /dev/null
+++ b/snag/src/libargs/license.txt
@@ -0,0 +1,10 @@
+Permission to use, copy, modify, and/or distribute this software for any purpose
+with or without fee is hereby granted.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
+FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
+OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
+TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
+THIS SOFTWARE.
diff --git a/snag/src/libargs/readme.txt b/snag/src/libargs/readme.txt
new file mode 100644
index 0000000..d725590
--- /dev/null
+++ b/snag/src/libargs/readme.txt
@@ -0,0 +1 @@
+args by Darren Mulholland, https://github.com/dmulholl/args \ No newline at end of file
diff --git a/snag/src/main.c b/snag/src/main.c
new file mode 100644
index 0000000..0429fda
--- /dev/null
+++ b/snag/src/main.c
@@ -0,0 +1,52 @@
+#include <stdio.h>
+
+#include "util.h"
+#include "libargs/args.h"
+#include "pkg_db.h"
+
+int cmd_install(char* cmd_name, ArgParser* parser);
+
+int main(int argc, char* argv[]) {
+
+ // create argument parser
+ ArgParser* parser = ap_new_parser();
+ if (parser == NULL) die("failed to initialize cli parsing");
+ ap_set_helptext(parser, "Usage: snag <command>");
+ ap_set_version(parser, "0.0");
+
+ // add install subcommand
+ ArgParser* install = ap_new_cmd(parser, "install inst i");
+ if (install == NULL) die("failed to initialize install cli subcommand");
+ ap_set_helptext(install, "Usage: snag install [packages]");
+ ap_set_cmd_callback(install, cmd_install);
+
+ // parse the arguments (automatically executes subcommands)
+ if (!ap_parse(parser, argc, argv)) die("failed to parse arguments");
+
+ // if no subcommand was specified, parse and execute install as the root command
+ if (!ap_found_cmd(parser)) {
+ if (!ap_parse(install, argc, argv)) die("failed to parse arguments");
+ cmd_install("install", install);
+ }
+
+ ap_free(parser);
+ return 0;
+}
+
+int cmd_install(char* cmd_name, ArgParser* parser) {
+
+ // install each package specified
+ for (int i = 0; i < ap_count_args(parser); i++) {
+ if (i != 0) printf("\n");
+
+ // load package description
+ char* pkgid = ap_get_arg_at_index(parser, i);
+ package_info_t info;
+ if (load_package_info(pkgid, &info)) {
+ printf("Installing %s - %s\n", info.attrs.name, info.attrs.desc);
+ run_package_install(&info);
+ }
+ }
+
+ return 0;
+}
diff --git a/snag/src/pkg_db.c b/snag/src/pkg_db.c
new file mode 100644
index 0000000..c35e47b
--- /dev/null
+++ b/snag/src/pkg_db.c
@@ -0,0 +1,157 @@
+#include "pkg_db.h"
+#include "util.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <errno.h>
+#include <sys/wait.h>
+
+#define PKG_INFO_CMD "./scripts/get_pkg_attrs.sh"
+#define PKG_INST_CMD "./scripts/run_pkg_install.sh"
+
+// note: fixed location of package database for now
+const char pkg_db[] = "../packages";
+const size_t pkg_db_len = sizeof(pkg_db) - 1;
+
+bool load_package_info(char* pkgid, package_info_t* info) {
+
+ memset(info, 0, sizeof(*info));
+ size_t pkgid_len = strlen(pkgid);
+
+ // construct package description directory path and ensure it exists
+ // pkg_dir = "pkg_db/pkgid"
+ size_t pkg_dir_len = pkg_db_len + pkgid_len + 1;
+ char* pkg_dir = emalloc(pkg_dir_len + 1, "pkg_dir");
+ {
+ memcpy(pkg_dir, pkg_db, pkg_db_len);
+ pkg_dir[pkg_db_len] = '/';
+ memcpy(pkg_dir + pkg_db_len + 1, pkgid, pkgid_len);
+ pkg_dir[pkg_dir_len] = '\0';
+ if (access(pkg_dir, F_OK) != 0) {
+ print("Package '%s' not found\n", pkgid);
+ free(pkg_dir);
+ return false;
+ }
+ }
+
+ // construct package description script path and ensure it exists
+ // script_path = "pkg_dir/pkgid.sh"
+ size_t script_path_len = pkg_dir_len + pkgid_len + 4;
+ info->script_path = emalloc(script_path_len + 1, "info->script_path");
+ {
+ memcpy(info->script_path, pkg_dir, pkg_dir_len);
+ info->script_path[pkg_dir_len] = '/';
+ memcpy(&info->script_path[pkg_dir_len + 1], pkgid, pkgid_len);
+ memcpy(&info->script_path[script_path_len - 3], ".sh", 3);
+ info->script_path[script_path_len] = '\0';
+ free(pkg_dir);
+ if (access(info->script_path, F_OK) != 0) {
+ print("Package '%s' broken, doesn't have description script\n", pkgid);
+ free(info->script_path);
+ return false;
+ }
+ }
+
+ // PIPE and FORK to run pkg attrs script
+ int pipefds[2] = {0};
+ if (pipe(pipefds) < 0) die("failed to pipe: %s", strerror(errno));
+ pid_t pid = fork();
+ if (pid < 0) die("failed to fork: %s", strerror(errno));
+
+ if (pid == 0) {
+ // child proc
+ // connect my stdout to the pipe
+ close(pipefds[0]);
+ dup2(pipefds[1], STDOUT_FILENO);
+ close(pipefds[1]);
+ // exec into get attr script
+ char* argv[] = { PKG_INFO_CMD, info->script_path, NULL };
+ char* envp[] = { NULL };
+ execve(PKG_INFO_CMD, argv, envp);
+ // what are you still doing here?
+ die("CHILD - execve to %s failed: %s", PKG_INFO_CMD, strerror(errno));
+ } else {
+ // parent proc, close write end of pipe
+ close(pipefds[1]);
+ }
+
+ // wait for child to finish
+ int wstatus;
+ if (waitpid(pid, &wstatus, 0) < 0) die("waitpid failed: %s", strerror(errno));
+ if (WEXITSTATUS(wstatus) != 0) die("the child failed to execute get_pkg_attr script");
+
+ // read all bytes from pipe into attr buffer
+ ssize_t bytes_read = read(pipefds[0], info->attrs.buffer, sizeof(info->attrs.buffer));
+ if (bytes_read < 0) die("failed to read from pipe: %s", strerror(errno));
+ close(pipefds[0]);
+
+ // read attributes from attr buffer
+ // attribute format: type (char), data value (null terminated str)
+ // todo(jqj): switch over to dynamically allocated buffer
+ for (int i = 0; i < bytes_read;) {
+ char atype = info->attrs.buffer[i];
+ i++;
+ switch (atype) {
+ case 'i':
+ info->attrs.id = &info->attrs.buffer[i]; break;
+ case 'n':
+ info->attrs.name = &info->attrs.buffer[i]; break;
+ case 'd':
+ info->attrs.desc = &info->attrs.buffer[i]; break;
+ case 'w':
+ info->attrs.webpage = &info->attrs.buffer[i]; break;
+ case 'l':
+ info->attrs.license = &info->attrs.buffer[i]; break;
+ case 'v':
+ info->attrs.version = &info->attrs.buffer[i]; break;
+ }
+ for(;info->attrs.buffer[i] && i < bytes_read; i++);
+ i++;
+ }
+
+ return true;
+}
+
+void run_package_install(const package_info_t* info) {
+
+ // PIPE and FORK to run install script
+ int pipefds[2] = {0};
+ if (pipe(pipefds) < 0) die("failed to pipe: %s", strerror(errno));
+ pid_t pid = fork();
+ if (pid < 0) die("failed to fork: %s", strerror(errno));
+
+ if (pid == 0) {
+ // child proc
+ // connect my stdout and stderr to the pipe
+ close(pipefds[0]);
+ dup2(pipefds[1], STDOUT_FILENO);
+ dup2(pipefds[1], STDERR_FILENO);
+ close(pipefds[1]);
+ // exec into install script
+ char* argv[] = { PKG_INST_CMD, info->script_path, NULL };
+ char* envp[] = { NULL };
+ execve(PKG_INST_CMD, argv, envp);
+ // what are you still doing here?
+ die("CHILD - execve to %s failed: %s", PKG_INST_CMD, strerror(errno));
+ } else {
+ // parent proc, close write end of pipe
+ close(pipefds[1]);
+ }
+
+ // read from the pipe until EOF
+ char buf[4096];
+ ssize_t bytes_read = 0;
+ while ((bytes_read = read(pipefds[0], buf, sizeof(buf))) > 0) {
+ // print build out, can log and stuff in future
+ printf("%.*s", (int)bytes_read, buf);
+ }
+ if (bytes_read < 0) die("failed to read from pipe: %s", strerror(errno));
+ close(pipefds[0]);
+
+ // reap the child to finish
+ int wstatus;
+ if (waitpid(pid, &wstatus, 0) < 0) die("waitpid failed: %s", strerror(errno));
+ if (WEXITSTATUS(wstatus) != 0) die("the child failed to execute install script");
+}
diff --git a/snag/src/pkg_db.h b/snag/src/pkg_db.h
new file mode 100644
index 0000000..0d34a61
--- /dev/null
+++ b/snag/src/pkg_db.h
@@ -0,0 +1,27 @@
+#ifndef PKG_DB_H
+#define PKG_DB_H
+
+#include <stdbool.h>
+
+// loads and allocates package attributes for the info struct. does not do
+// excessive validation, so this should be fine when loading lots of package
+// data
+typedef struct {
+ char* id;
+ char* name;
+ char* desc;
+ char* webpage;
+ char* license;
+ char* version;
+ char buffer[4069];
+} package_attrs_t;
+
+typedef struct {
+ char* script_path;
+ package_attrs_t attrs;
+} package_info_t;
+
+bool load_package_info(char* pkgid, package_info_t* info);
+void run_package_install(const package_info_t* info);
+
+#endif
diff --git a/snag/src/util.c b/snag/src/util.c
new file mode 100644
index 0000000..8cc9b18
--- /dev/null
+++ b/snag/src/util.c
@@ -0,0 +1,76 @@
+#include <errno.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdio.h>
+
+#include "util.h"
+
+#if !DEBUG
+void log(const char* format, ...) {
+#else
+void _log(int line, const char* file, const char* format, ...) {
+ fprintf(stderr, "%s:%d: info: ", file, line);
+#endif
+ va_list args;
+ va_start(args, format);
+ vfprintf(stderr, format, args);
+ va_end(args);
+}
+
+#if !DEBUG
+void die(const char* format, ...) {
+#else
+void _die(int line, const char* file, const char* format, ...) {
+ fprintf(stderr, "%s:%d: error: ", file, line);
+#endif
+ va_list args;
+ va_start(args, format);
+ vfprintf(stderr, format, args);
+ va_end(args);
+ fprintf(stderr, "\n");
+ exit(1);
+}
+
+#if !DEBUG
+void* emalloc(size_t size, char* alloc_reason) {
+ void* d = malloc(size);
+ if (d == NULL) _die("failed to allocate '%s': %s", alloc_reason, strerror(errno));
+ return d;
+}
+#else
+void* _emalloc(int line, const char* file, size_t size, char* alloc_reason) {
+ void* d = malloc(size);
+ if (d == NULL) _die(line, file, "failed to allocate '%s': %s", alloc_reason, strerror(errno));
+ return d;
+}
+#endif
+
+char* vastrcat_(int dummy, ...) {
+
+ va_list args;
+ va_list args2;
+ va_start(args, dummy);
+ va_copy(args2, args);
+
+ // get len of output string
+ size_t len = 0;
+ char* arg = NULL;
+ while ((arg = va_arg(args, char*))) {
+ len += strlen(arg);
+ }
+ va_end(args);
+
+ // allocate and build output
+ char* output = malloc(len + 1);
+ if (!output) return NULL;
+ char* p = output;
+ while ((arg = va_arg(args, char*))) {
+ size_t alen = strlen(arg);
+ memcpy(p, arg, alen);
+ p += alen;
+ }
+ output[len] = '\0';
+ va_end(args2);
+
+ return output;
+}
diff --git a/snag/src/util.h b/snag/src/util.h
new file mode 100644
index 0000000..3813903
--- /dev/null
+++ b/snag/src/util.h
@@ -0,0 +1,31 @@
+#ifndef UTIL_H
+#define UTIL_H
+
+#include <stdarg.h>
+#include <stddef.h>
+
+// utility for errors
+#if !DEBUG
+ void log(const char* format, ...) __attribute__((format(printf, 1, 2)));
+ void die(const char* format, ...) __attribute__((format(printf, 1, 2)));
+ void* emalloc(size_t size, char* alloc_reason);
+#else
+ #define log(fmt, ...) _log(__LINE__, __FILE__, fmt, ##__VA_ARGS__)
+ void _log(int line, const char* file, const char* format, ...) __attribute__((format(printf, 3, 4)));
+
+ #define die(fmt, ...) _die(__LINE__, __FILE__, fmt, ##__VA_ARGS__)
+ void _die(int line, const char* file, const char* format, ...) __attribute__((format(printf, 3, 4)));
+
+ #define emalloc(size, alloc_reason) _emalloc(__LINE__, __FILE__, size, alloc_reason)
+ void* _emalloc(int line, const char* file, size_t size, char* alloc_reason);
+#endif
+
+// functions for outputting and displaying text
+#define printout(fmt, ...) fprintf(stdout, fmt, ##__VA_ARGS__)
+#define print(fmt, ...) fprintf(stderr, fmt, ##__VA_ARGS__)
+
+// string functions
+char* vastrcat_(int dummy, ...);
+#define vastrcat(...) vastrcat_(0, __VA_ARGS__, NULL)
+
+#endif